SQLの窓

2017年06月27日


ファイルを一つアップロード : FORM の target を IFRAME にして、PHP に JavaScript を書き出させて元のページにメッセージを表示させる

昨今、ファイルをアップロードするならば、JavsScript を駆使して UI を作成し、 $.ajax で送信したほうが簡潔になりますが、ベタな FORM の記述のみで送信してそれなりのレスポンスを得ようという処理です。

デモページ


画像をクリックするとデモページへ移動します

当然ですが、デモページで実際のアップロードはできません。しかし、結果を表示するにチェックを入れて送信すると、上の画像のように実際と同じ状態で情報をシミュレートします。

UI の構築を気楽に工夫できるように、twitter-bootstrap の 4.0.0-alpha.6 を組み込んでいます

IFRAME は本来非表示で、ここに JavaScript で右上のメッセージ(toastr.js)を表示するようにしていますが、デバッグ用としても使えるようにしており、その場合は PHP 側の $_FILES の中身が見れるようになっています。

type="file" の name 属性は target で固定して php 側で使用しています。
送信データの制限 データの制限はサーバに対する無意味なアクセスを遮断するアプローチと、アプリから見たデータの制限という二つのアプローチがあります。前者がもっとも重要で、それがなければ php.ini の upload_max_filesize 内であれば結果的にそのサイズまでは送信を許してしまいます。 ※ upload_max_filesize を超えると $_FILES そのものが空になるようです。 後者は、FORM 内の name="MAX_FILE_SIZE" の値で指定できますが、完全な対策ではありません。そこで、対象ページのあるディレクトリの .htaccess 内に 『LimitRequestBody 102400』のように制限値を設定し、さらに『ErrorDocument 413』 を設定してそのページの中でエラー処理の対応をしています ▼ 413.html
<!DOCTYPE html>
<html lang="ja">
<style>
* {
	font-size: 32px;
}
</style>
<script>

	try {
		parent.$("iframe").show();
		parent.toastr.info( "ファイル のアップロードに失敗しました");
		parent.toastr.info( "エラー内容 : Request Entity Too Large" );
	}
	catch (e) {}

</script>
</head>
<body>

JavaScript の処理用に IFRAME を定義している場合で、
413 Request Entity Too Large が発生した場合にこのページが IFRAME 内に表示されます。

</body>
</html>


file_upload_html.php

$debug 変数で、実際のアップロードの可否を切り替えています。if ( !$_FILES ) { の部分は、upload_max_filesize を超えてしまった状態の対処ですが、ここでは LimitRequestBody 102400 を設定しているので発生する事はありません。
<?php
session_cache_limiter('nocache');
session_start();

header( "Content-Type: text/html; charset=utf-8" );

// *************************************
// 変数初期値
// *************************************
$debug = 0;
$upload_dir = "./upload";
if ( !$_FILES ) {
	// php.ini の upload_max_filesize を超えて、
	// Apache に制限が無いと $_FILE が空になる
	$_FILES['target']['error'] = 5;	// ユーザエラーメッセージ
}

// *************************************
// アップロード処理
// フィールド名 : target で固定
// *************************************
if ( $_SERVER['REQUEST_METHOD'] == "POST" ) {

	// 公開状態では実行しない
	if ( $debug == 1 ) {

		$upload = realpath ( $upload_dir );
		$upload .= ( DIRECTORY_SEPARATOR . $_FILES['target']['name'] );
		if ( move_uploaded_file(
			$_FILES['target']['tmp_name'], $upload ) ) {
			$_POST['result']  = "アップロードに成功しました";
		}
		else {
			if ( !$_FILES ) {
				$_FILES['target']['error'] = 5;	// ユーザエラーメッセージ
			}
			$_POST['result']  = "アップロードに失敗しました";
		}

	}
	else {
		if ( $_FILES['target']['error'] == 0 ) {
			$_POST['result']  = "アップロードに成功しました";
		}
		else {
			$_POST['result']  = "アップロードに失敗しました";
		}
	}

}
else {
	$_POST['result']  = "POST メソッドを使用して下さい";
}

$_POST['files'] = $_FILES;

// *************************************
// この JSON 文字列を HTML 内の json
// オブジェクト定義として埋め込みます
// *************************************
$json = json_encode($_POST, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE );


// *************************************
// 画面定義
// *************************************
?>
<!DOCTYPE html>
<html lang="ja">
<script>
var message = <?php require("error.json.php") ?>
var json = <?= $json ?>;

if ( json.files.target.error == 0 ) {
	parent.toastr.info(json.files.target.name + " がアップロードされました");
	parent.toastr.info("ファイルサイズ : " + json.files.target.size);
}
else {
	parent.toastr.info(json.files.target.name + " のアップロードに失敗しました");
	parent.toastr.info("エラー内容 : " + message.error[json.files.target.error]);
}

<?php if( $_POST["disp_result"] == "1" ) { ?>

	parent.$("iframe[name='upload']")
		.css("border", "1px solid #c0c0c0")
		.show();

<?php } else { ?>

	parent.$("iframe[name='upload']").hide();

<?php } ?>

</script>
</head>
<body>
<?php if( $_POST["disp_result"] == "1" ) { ?>
<textarea style='width:90%;height:350px;'><?= $json ?></textarea>

<?php } ?>
</body>
</html>


error.json.php は、PHP のエラーメッセージです。ここでは部品として埋め込んでいますが、$.ajax バージョンでも使用(error.json.php?type=json)しています。

error.json.php
<?php
if ( $_GET["type"] == "json" ) {
	session_cache_limiter('nocache');
	session_start();
	header( "Content-Type: application/json; charset=utf-8" );
}

?>
{
	"error": [
		"0 - 正常終了",
		"1 - アップロードされたファイルは、php.ini の upload_max_filesize ディレクティブの値を超えています",
		"2 - アップロードされたファイルは、HTML フォームで指定された MAX_FILE_SIZE を超えています",
		"3 - アップロードされたファイルは一部のみしかアップロードされていません",
		"4 - ファイルはアップロードされませんでした",
		"5 - 『アップロードに失敗しました』",
		"6 - テンポラリフォルダがありません。PHP 5.0.3 で導入されました",
		"7 - ディスクへの書き込みに失敗しました。PHP 5.1.0 で導入されました",
		"8 - PHP の拡張モジュールがファイルのアップロードを中止しました"
	]
}


upload.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta content="width=device-width initial-scale=1.0 minimum-scale=1.0 maximum-scale=1.0 user-scalable=no" name="viewport">
<title>HTML の FORM のみでファイルを一つアップロードする</title>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<link id="link" rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.10.1/themes/base/jquery-ui.css">
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-alpha.6/css/bootstrap.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-alpha.6/js/bootstrap.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.3/toastr.min.css">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.3/toastr.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.qrcode/1.0/jquery.qrcode.min.js"></script>

<!-- 共通の表示 -->
<link rel="stylesheet" href="common.css">

<!-- CSS の指定 -->
<style>
/* PC 用の表示 */
@media screen and ( min-width:480px ) {
	#content {
		margin: 20px;
	}
}
/* スマホ用の表示 */
@media screen and ( max-width:479px ) {
	#content {
		margin: 0px;
	}
	body {
		width: 100%!important;
		margin: 0px;
	}
	.unit {
		width: 100%;
	}
}
</style>
<script>
// 簡易的なスマホチェックを jQuery のプロパティとして登録
jQuery.isMobile = (/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(navigator.userAgent.toLowerCase()));
if ( $.isMobile ) {
	// スマホの場合は表示画面下の中央
	toastr.options.positionClass = "toast-bottom-center";
}

$(function(){

	// このページ自身の QRコードの表示
	$('#qrcode')
		.css({ "margin" : "20px" })
		.qrcode({width: 160,height: 160,text: location.href });
		

});
</script>
</head>
<body>
<div id="head">
	<div id="title">
		<a href="./">One File アップロード</a>
	</div>
</div>

<div id="content">

	<form target="upload" enctype="multipart/form-data" action="../../php_upload/file_upload_html.php" method="POST">
		<input type="hidden" name="MAX_FILE_SIZE" value="40000">
		<input name="target" type="file" class="btn btn-info">
		<input type="submit" value="ファイルを送信" class="btn btn-success ">

		<br>
		<label for="disp_result">結果を表示する</label> <input id="disp_result" type="checkbox" name="disp_result" value="1" class="mt-5">
	</form>

</div>

<div id="qrcode"></div>

<iframe src="about:blank" name="upload" style="display:none;border:0px solid #c0c0c0;margin-left:20px;width:90%;height:400px;"></iframe>



</body>
</html>


HTML 側のダウンロード



PHP 側のダウンロード





posted by lightbox at 2017-06-27 12:55 | PHP + 通信 | このブログの読者になる | 更新情報をチェックする

2017年06月20日


Android での保存用テキストデータの扱いを okio で簡素化する

OkHttp はとても簡単にインターネットへのアクセスを提供してくれます。その Okhttp が依存しているストリームの処理が okio です。

当然 okio は一般的なテキストファイルのアクセスにも簡単に使えて便利なので、Android の内部で扱われる『位置付け』別に処理を実際に使用して整理してみました。

★ 全体のソースコード
build.gradle への記述 ▼ 2017/06/20 時点 compile 'com.squareup.okio:okio:1.13.0' Runtime Permission の対応 ▼ 以下のテンプレートを使用しています Android 6.0 : テストの為の Runtime Permission の対応を自動化するテンプレート okio を使用した2つのメソッドを用意 okio には、BufferedSource と BufferedSink というクラスがあり、それぞれ 入力と出力を担当しています。一応サンプルとして OkioTest.java を読むと雰囲気がつかめると思いますが、欲しい機能はそれほど複雑では無いので以下の2つのメソッドだけでも十分使いこなせると思います。
	// *************************
	// テキストファイル一括読込
	// "UTF-8" or "windows-31j"
	// *************************
	private String readTextAll(InputStream inputStream, String charset) throws Exception {
		String result;

		BufferedSource bufferedSource = Okio.buffer(Okio.source(inputStream));
		result = bufferedSource.readString(Charset.forName(charset));

		bufferedSource.close();
		inputStream.close();

		return result;
	}

	// *************************
	// ファイルコピー
	// *************************
	private void copy(InputStream inputStream, OutputStream outputStream) throws Exception {

		Source source = Okio.source(inputStream);
		BufferedSink bufferedSink = Okio.buffer(Okio.sink(outputStream));
		bufferedSink.writeAll(source);

		bufferedSink.close();
		source.close();

	}

assets に syain.csv ファイルを置いて他の場所にコピー(書込み)



syain.csv は、Windows の一般的な SHIFT_JIS のテキストファイル(CSV) です。このデータを パッケージの領域と 外部ストレージにコピーして、後でテキストファイルの読み込みをテストします。

 



他にバリエーションとして、okio で直接パッケージの領域にテキストファイル(test.txt)を "UTF-8" として書き込んでいます(23〜30)。
1) UTF-8 を指定して書き込み

Charset charset = Charset.forName("UTF-8");
sink.writeString("ストレージ情報\n",charset);

2) 専用メソッドで UTF-8 として書き込み

sink.writeUtf8(Manifest.permission.CAMERA + "\n");

※ sink は BufferedSink です
さらに、プリファレンスの書き込みも行います(36〜46)。
			try {

				InputStream inputStream;
				AssetManager assetManager = MainActivity.this.getResources().getAssets();

				// ****************************************
				// syain.csv を assets から プライベートエリアへコピー
				// ****************************************
				inputStream = assetManager.open("syain.csv");
				copy(inputStream, openFileOutput("syain.csv", MODE_PRIVATE));

				// ****************************************
				// syain.csv を assets から 外部ストレージへコピー
				// ****************************************
				createDir();
				inputStream = assetManager.open("syain.csv");
				String csvPath = Environment.getExternalStorageDirectory().getPath() + "/textfiletest/syain.csv";
				copy(inputStream, new FileOutputStream(csvPath));

				// ****************************************
				// プライベートなテキストファイルの作成と書き込み
				// ****************************************
				BufferedSink sink = Okio.buffer(Okio.sink(openFileOutput("test.txt", MODE_PRIVATE)));
				Charset charset = Charset.forName("UTF-8");
				sink.writeString("ストレージ情報\n",charset);
				sink.writeUtf8(String.format("%s\n", Environment.getExternalStorageDirectory().getPath()));
				sink.writeUtf8(Manifest.permission.CAMERA + "\n");
				sink.writeUtf8(Manifest.permission.WRITE_EXTERNAL_STORAGE + "\n");
				sink.writeUtf8(PackageManager.PERMISSION_GRANTED + "\n");
				sink.close();

				// ****************************************
				// プライベートな小さな情報を保存する為の本来の方法
				// private_data.xml の作成
				// ****************************************
				SharedPreferences data = getSharedPreferences("private_data", MODE_PRIVATE);
				SharedPreferences.Editor editor = data.edit();

				// 現在の年月日・時分秒を取得する為の準備
				Calendar cal = Calendar.getInstance();
				SimpleDateFormat sf = new SimpleDateFormat("yyyyMMdd_HHmmss");

				// データの書き込み
				editor.putString("now", sf.format(cal.getTime()));
				editor.putString("japan", "日本語");
				editor.commit();

			} catch (Exception e) {
				e.printStackTrace();
			}

外部ストレージにフォルダの作成

Runtime Permission の対象処理です

	// *************************
	// フォルダ作成
	// *************************
	private void createDir() {
		String imageDir = Environment.getExternalStorageDirectory().getPath() + "/textfiletest";
		Log.i("lightbox", imageDir );
		File file = new File(imageDir);
		// ディレクトリ初期作成
		if (!file.exists()) {
			if (file.mkdir() == false) {
				Log.i("lightbox", "ディレクトリを作成できませんでした");
				return;
			}
		}
	}

画面にファイルを読込み(1)

まずは、パッケージ内の領域から読み込んで画面に表示します( openFileInput )

※ 画面定義


			StringBuilder sb = new StringBuilder();

			InputStream inputStream;
			String text;

			Log.i("lightbox", "読み込み1");
			try {
				// ****************************************
				// プライベートなテキストファイルの読み込み
				// "UTF-8" or "windows-31j"
				// ****************************************
				inputStream = openFileInput("test.txt");
				text = readTextAll(inputStream,"UTF-8");

				sb.append(text);

				// ****************************************
				// プライベートなテキストファイルの読み込み
				// "UTF-8" or "windows-31j"
				// ****************************************
				inputStream = openFileInput("syain.csv");
				text = readTextAll(inputStream,"windows-31j");

				sb.append(text);

				EditText editText = (EditText) MainActivity.this.findViewById(R.id.editText);
				editText.setText(sb.toString());
				TextView textView = (TextView) MainActivity.this.findViewById(R.id.textView);
				textView.setText(sb.toString());

				// ****************************************
				// private_data.xml からデータの取得
				// ****************************************
				SharedPreferences data = getSharedPreferences("private_data", MODE_PRIVATE);
				Log.i("lightbox", data.getString("now", "このデータはありません"));
				Log.i("lightbox", data.getString("japan", "このデータはありません"));

			} catch (Exception e) {
				e.printStackTrace();
			}
UTF-8 と SHIFT_JIS で保存してあるファイルの一括読み込みです。キャラクタセットに関する Oracle のドキュメントはこちらになります。

※ プリファレンスは Log.i で表示しています

画面にファイルを読込み(2)

外部ストレージより読み込みます( Runtime Permision の対象処理 )
new FileInputStream(path)

※ test.txt は、画面表示の都合で読み込んでいます
			StringBuilder sb = new StringBuilder();

			InputStream inputStream;
			BufferedSource bufferedSource;
			String text;

			Log.i("lightbox", "読み込み2");
			try {
				// ****************************************
				// プライベートなテキストファイルの読み込み
				// "UTF-8" or "windows-31j"
				// ****************************************
				inputStream = openFileInput("test.txt");
				text = readTextAll(inputStream,"UTF-8");

				sb.append(text);

				// ****************************************
				// 外部ストレージからテキストファイルの読み込み
				// "UTF-8" or "windows-31j"
				// ****************************************
				String csvPath = Environment.getExternalStorageDirectory().getPath() + "/textfiletest/syain.csv";
				inputStream = new FileInputStream(csvPath);
				text = readTextAll(inputStream,"windows-31j");

				sb.append(text);

				EditText editText = (EditText) MainActivity.this.findViewById(R.id.editText);
				editText.setText(sb.toString());
				TextView textView = (TextView) MainActivity.this.findViewById(R.id.textView);
				textView.setText(sb.toString());


			} catch (Exception e) {
				e.printStackTrace();
			}


関連する記事 : 一般的な Java のテキスト読込み

Java : テキストファイルを読み込んで正規表現で置換


posted by lightbox at 2017-06-20 21:17 | 2017 Android Studio | このブログの読者になる | 更新情報をチェックする

Android 6.0 : テストの為の Runtime Permission の対応を自動化するテンプレート

Runtime Permission の対応はしないとアプリが動作しないのでせざるを得ないですが、本来の処理とは無関係なのでかなり面倒な上にソースコードそのものが第三者への説明の邪魔になります。

そこで、ほぼ定型化して処理する為のテンプレートとして MainActivity への配置例と、そのために必要なクラスを用意しました。
手順1 ▼ Runtime Permission の処理は、以下から permission_170610.zip をダウンロードして下さい。 ※ CheckMyPermission の内容 手順2 ▼ 解凍して、プロジェクトの "app\src\main\java" の下に com フォルダをコピーして下さい ▼ エクスプローラから見た状態 手順3 使用したい Permission を AndroidManifest.xml に記述します
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.example.lightbox.runtimepermissiontest">

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.CAMERA"/>
    <uses-permission android:name="android.permission.WRITE_CALENDAR"/>

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>

</manifest>

手順4

MainActivity を以下のように記述して下さい
package com.example.lightbox.runtimepermissiontest;

import android.Manifest;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

import com.example.lightbox.permission.CheckMyPermission;
import com.example.lightbox.permission.MyPermission;

import java.util.ArrayList;

public class MainActivity extends AppCompatActivity {

	private CheckMyPermission checkMyPermission;

	@Override
	public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
		// 全ての必要なパーミッションが許可された場合
		if ( checkMyPermission.checkPermission( requestCode,  grantResults )) {
			// onCreate で初期処理できるように、MainActivity をリスタート
			Intent intent = MainActivity.this.getIntent();
			MainActivity.this.finish();
			MainActivity.this.startActivity(intent);
		}
	}

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		// 必要なパーミッションのリスト
		ArrayList<MyPermission> myPermission_list = new ArrayList<MyPermission>();
		// AndroidManifest.xml に記述した Permission の一覧
		myPermission_list.add(new MyPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE,100));
		myPermission_list.add(new MyPermission(Manifest.permission.CAMERA,101));
		myPermission_list.add(new MyPermission(Manifest.permission.WRITE_CALENDAR,102));
		// Runtime Permission 用のクラスのインスタンス
		checkMyPermission = new CheckMyPermission(MainActivity.this,myPermission_list);
		if ( checkMyPermission.checkPermission() ) {
			// ここから通常の初期処理
			initSettings();
		}
	}

	private void initSettings() {

	}

}


手順5

実行します。すると、初回は許可ダイアログが指定した Permission と同数表示されて、onRequestPermissionsResult が実行されます。すると、Activity が再起動され、checkMyPermission.checkPermission() が true になって initSettings() を実行する事になります。

2回目以降(全て許可した場合)は onCreate で initSettings() が常に実行される事になります。(許可ダイアログを再度表示したい場合は、アプリを削除して再度実行します)

許可ダイログ

  


関連する記事

『Android 6.0 エミュレータで 複数の Runtime Permission の対応を簡潔に吸収するクラス( CheckMyPermission )』

Android Studio : Runtime Permission 等の裏方作業を MainActivity にさせて、本来の処理は継承したサブクラスで行う( カメラを呼び出して画像を保存させ、ImageView に表示する )


posted by lightbox at 2017-06-20 19:35 | 2017 Android Studio | このブログの読者になる | 更新情報をチェックする
Seesaa の各ページの表示について
Seesaa の 記事がたまに全く表示されない場合があります。その場合は、設定> 詳細設定> ブログ設定 で 最新の情報に更新の『実行ボタン』で記事やアーカイブが最新にビルドされます。

Seesaa のページで、アーカイブとタグページは要注意です。タグページはコンテンツが全く無い状態になりますし、アーカイブページも歯抜けページはコンテンツが存在しないのにページが表示されてしまいます。

また、カテゴリページもそういう意味では完全ではありません。『カテゴリID-番号』というフォーマットで表示されるページですが、実際存在するより大きな番号でも表示されてしまいます。

※ インデックスページのみ、実際の記事数を超えたページを指定しても最後のページが表示されるようです

対処としては、このようなヘルプ的な情報を固定でページの最後に表示するようにするといいでしょう。具体的には、メインの記事コンテンツの下に『自由形式』を追加し、アーカイブとカテゴリページでのみ表示するように設定し、コンテンツを用意するといいと思います。


※ エキスパートモードで表示しています

アーカイブとカテゴリページはこのように簡単に設定できますが、タグページは HTML 設定を直接変更して、以下の『タグページでのみ表示される内容』の記述方法で設定する必要があります

<% if:page_name eq 'archive' -%>
アーカイブページでのみ表示される内容
<% /if %>

<% if:page_name eq 'category' -%>
カテゴリページでのみ表示される内容
<% /if %>

<% if:page_name eq 'tag' -%>
タグページでのみ表示される内容
<% /if %>
この記述は、以下の場所で使用します
container 終わり

フリーフォントで簡単ロゴ作成
フリーフォントでボタン素材作成
フリーフォントで吹き出し画像作成
フリーフォントではんこ画像作成
ほぼ自由に利用できるフリーフォント
フリーフォントの書体見本とサンプル
画像を大きく見る為のウインドウを開くボタンの作成

CSS ドロップシャドウの参考デモ
BUTTONS (CSS でボタン)
イラストAC
ぱくたそ
写真素材 足成
フリーフォント一覧
utf8 文字ツール
右サイド 終わり
base 終わり