iOS6 では Safari でファイルのアップロードか可能になったらしいのでサンプルを作ってみました。
単にアップロードできるサンプルではつまらないので、jQuery Mobile と jQuery.upload を使い、ファイルを非同期にアップロードするサンプルにしました。環境はこんな感じです。
- クライアント
- jQuery Mobile 1.1.1
- jQuery.upload 1.0.2
- サーバー
- Amon2
Amon2は Perl の軽量フレームワークです。この環境で作ったサンプルを見ながら、ハマった点をまとめます。
jQuery Mobile でページを作る
さくっと簡単なページを作ってみました。jQuery Mobile を使ったのは、単に iPhone で見たときに要素の見た目を良くするためで、他の機能は使っていません。
[% WRAPPER 'include/layout.tt' %] <div id="my-form"> <div data-role="fieldcontain"> <fieldset data-role="controlgroup"> <label for="my-text1">テキスト1</label> <input name="text1" id="my-text1" placeholder="テキスト1" type="text"> </fieldset> </div> <div data-role="fieldcontain"> <fieldset data-role="controlgroup"> <label for="my-file1">ファイルの指定</label> <input name="file1" id="my-file1" type="file"> </fieldset> </div> <input type="hidden" name="csrf_token" value="[% c().get_csrf_defender_token() %]"> </div> <button id="my-upload" data-theme="b" data-icon="check" data-iconpos="right">アップロード開始!</button> [% END %]
ファイルをアップロードするページなのに <form>
タグを使っていません。この理由は後ほど。ハイライトした謎のタグも後で説明します。
iPhone で見てみると右のような感じです。
jQuery.upload で非同期にアップロードする
ファイルのアップロードというのは Web アプリにとって鬼門の一つです。アップロードボタンを押したが最後、別のページに遷移するまで一切の動作が出来ないという、Ajax によるサイト作成者にとっては悪夢のような存在でした。
今回使用した jQuery.upload はこれを解決してくれる非常にシンプルなプラグイン。ファイルのアップロード先を <iframe>
にすることで動作を非同期にし、かつ、サーバーからのレスポンスを XML や JSON で返してくれる優れものです。
今回は <button id="my-upload">
をタップ(クリック)したときに発動し、<div id="my-form">
の中身をサーバーに非同期に POST してくれるように書いてみます。
$('#my-upload').on('click', function(e){ // ローディングインジケータ(くるくる廻るアレ)を表示する $.mobile.showPageLoadingMsg(); // #my-form の中身を全部 POST $('#my-form').upload('/upload', function(data) { // ローディングインジケータを隠す $.mobile.hidePageLoadingMsg(); // レスポンスを表示 window.alert("アップロードされました!\n" + 'テキスト1 : ' + data.text1 + "\n" + '画像形式 : ' + data.content_type + "\n" + 'サイズ : ' + data.size + "\n" + 'ファイル名 : ' + data.basename + "\n"); // サーバーからの返値は JSON で受け取る }, 'json'); return false; });
$.fn.upload()
が発動すると、<form>
と <iframe>
がページに挿入されてアップロードが実行されます。既にページに <form>
があると二重に POST されてしまう(ことがある)ために、<form>
は HTML に記述しない方が良いです。
後は、サーバーサイドでこれを受け取るプログラムを書きます。
アップロードを受け付けるサーバースクリプト
今回は Amon2 を使っています。スクリプトの肝となる部分は次の通り。
package UploadSample::Web::Dispatcher; use strict; use warnings; use utf8; use Amon2::Web::Dispatcher::Lite; use Encode; use JSON; get '/' => sub { my ($c) = @_; $c->render('index.tt'); }; post '/upload' => sub { my ($c) = @_; my $p = $c->req->parameters; my $file1 = $c->req->upload('file1'); my %res = (text => $p->{text1}); if ($file1) { $res{size} = $file1->size; $res{basename} = $file1->basename; $res{content_type} = $file1->content_type; } $c->create_response( 200, ['Content-Type' => 'text/html; charset=UTF-8'], [encode(utf8 => to_json(\%res))], ); }; 1;
POST された通常のパラメータは $c->req->parameters
で取得できますが、アップロードされたファイルは $c->req->upload('名前')
のようにして取り出します。
upload()
メソッドの返値は Plack::Response::Upload
オブジェクトになっており、ファイルを簡単に扱うことが出来ます。
注意すべきはハイライトした 28 行目です。返値は JSON なのだからといって application/json
で返そうとするとハマります。
この返値は直接 <iframe>
内に書き込まれるのですが、ブラウザによってはダウンロードダイアログが出現したり、ご丁寧にも <pre>
タグで囲んでくれるため、うまく動作しなくなってしまうのです。
バッドノウハウっぽいですが、ここは HTML 文書として JSON を返すのが適切です。
Amon2 特有の問題
ここからは Amon2 特有の問題になりますが、以下の点に気をつける必要があります。
X-Frame-Options
ヘッダーに注意
普通に Amon2 でアプリを作ると、各ページに X-Frame-Options
ヘッダーが付加されます。これは、この Web アプリを別のサイトの <iframe>
に入れられることを拒否するヘッダーです。
The X-Frame-Options response header | MDN
https://developer.mozilla.org/ja/docs/The_X-Frame-Options_response_header
多くの場合においてこのヘッダーを付加するのは正しいことなのですが、今回のようにアップロードのために <iframe>
を使う場合は一手間必要です。
具体的には、UploadSample::Web
を次のように修正します。
diff --git a/lib/UploadSample/Web.pm b/lib/UploadSample/Web.pm index 4af8f8e..99e936b 100644 --- a/lib/UploadSample/Web.pm +++ b/lib/UploadSample/Web.pm @@ -59,7 +59,9 @@ __PACKAGE__->add_trigger( $res->header( 'X-Content-Type-Options' => 'nosniff' ); # http://blog.mozilla.com/security/2010/09/08/x-frame-options/ - $res->header( 'X-Frame-Options' => 'DENY' ); + my $frame_policy = + $c->req->path_info eq '/upload' ? 'SAMEORIGIN' : 'DENY'; + $res->header( 'X-Frame-Options' => $frame_policy ); # Cache control. $res->header( 'Cache-Control' => 'private' );
通常は X-Frame-Options: DENY
を指定するのですが、アップロードに使う URL へのアクセスの場合にだけ SAMEORIGIN
というポリシーを使います。これはドメインが同じ場合にだけ <iframe>
内への表示を許可するものです。
CSRF 対策
Amon2 はセキュリティに配慮して、セッションを利用した不正対策が組み込まれています。これはクロスサイトリクエストフォージェリ(長い……)を防ぐためのものです。
利用者がサイトに訪れた際、一意な文字列(ワンタイムトークン)が作成され、<form>
でのリクエスト時にはこのトークンをやりとりすることで、第三者に攻撃されにくくすることが出来ます。
Amon2 はページに <form>
タグを発見すると、このトークンを自動的に挿入してくれるのですが、今回は <form>
タグを HTML 内に使用しないために自分でトークンを書いてあげる必要があります。
<input type="hidden" name="csrf_token" value="[% c().get_csrf_defender_token() %]">
最後に
以上です。長々とおつきあい頂きありがとうございました。なんか後半は最早 iPhone あんまり関係ありませんでしたね。
今回作成したコードは Github に置いてありますので気になった方はご覧ください。
delphinus35/UploadSample · GitHub
https://github.com/delphinus35/UploadSample