Walrus,Digit. | 一覧 | 検索 | 更新履歴(RSS) | 新規作成
はてなブックマークに追加 はてなブックマークを表示 編集 | 編集(管理者用) | 差分

Perlメモ/CGIモジュール

編集

フォームデコードなどの機能を持つ、CGI作成者のためのPerlモジュール。

CGIモジュールはHTMLやフォームの生成、フォームデータのパース、Cookieの取得と出力などの機能を持つモジュールです。 いささか重厚長大のきらいはありますが、CGI::Liteモジュールと異なりCGIモジュールは多くの環境でデフォルトインストールされていることなど、CGIモジュールを選ぶべき理由も多くあります。

CGIモジュールのドキュメント

編集

CGIモジュールには多くのドキュメントが付属しています。 この内いくつかは、Perldoc.jpで和訳した文書を見ることができます。

IBMのサイトには「便利なCGIスクリプト作成のためのヒント~CGI.pmモジュールを綿密に検討する」という文書があります。 いささか古いのですが、CGIモジュールを使ったファイルアップローダを検討するのに、CGIモジュール自身のドキュメントを読む前にこちらを読んだのは理解の助けになりました。

Perl初心者の部屋にも「CGI.pmを使ったファイルのアップロードの簡単なサンプル」という文書があります。 こちらも同程度には古いのですが...。

util.plが必要とありますが、exitErrorサブルーチンぐらいしか使われていないようです。 この行をdieなどにしてしまい、require 'util.pl';を外せば動きそうです。

CGIモジュールによるデータの取得

CGIモジュールでは、フォームデータを受け取るのにparamというメソッドを使います。 例えば、次のようにしてフォームの'user'というフィールドに入力された値を受け取ることができます。

use CGI;
my $query = CGI->new;
my $user = $query->param('user');

全てのパラメータ名を取得するには、param()を引数なしで呼びます。

my @params = $query->param();
foreach my $param (@params) {
  print $param, "\t", $query->param($param), "\n";
}

同様に、Cookieを取得するにはcookieというメソッドを使います。 たとえば、'session_id'というCookieを取得するには、次のようにします。

use CGI;
my $query = CGI->new;
my $session = $query->cookie('session_id');

データに"%uXXXX"が混じる場合の対応

JavaScriptのescapeメソッドを使用したURL生成(BookmarkletやAJAXなど)やCookie設定を行うと、日本語部分などが%uXXXXのようにエスケープされることがあります。さらに、paramメソッドなどで取得した値にもそのまま入ってくることがあります。

これはUnicodeをエスケープしたものです。CGIモジュール2.74版までに同梱されているCGI::Utilモジュール1.1版では、この形式のUnicode対応がされていないため、アンエスケープに失敗します。CGIモジュール2.751版(同梱されているCGI::Utilモジュールは1.2版)以降を試してみてください。

Active Perlでは初期インストールされるCGIモジュールのバージョンが古いことがあるため、上記に注意が必要です。2005年4月1日現在、5.6系のActivePerlでも以下のppmコマンドでCGIモジュール3.00版をインストールできます。これに同梱されてくるCGI::Utilモジュールは1.3版ですので、Unicode対応が済んでいます。

ppm install CGI

CGIを配布する場合、ユーザが利用する環境のCGIモジュールの版数がわからないので、問題のない版を同梱したいと考えるかもしれません。その際はCGIモジュールの同梱物すべてを添付するべきですが、CGI/Util.pmのみでも上記は解決するようでした。

データに"%uXXXX"と"%XX%XX%XX"のどちらが来るか分からない場合の対応

JavaScript 1.5から、encodeURI、encodeURIComponentというメソッドがサポートされています。これらは、マルチバイト文字などはUTF-8に変換してからエンコードされる点と、UTFのエンコード文字列が"%uXXXX"ではなく"%XX%XX%XX"の形式になる点が、escapeと異なります。以下が参考になります。

困ってしまうのが、"%uXXXX"と"%XX%XX%XX"の形式では、元の文字列が同じでもCGIモジュールの返す値が違うことです。例えば、以下のように書かれています。

マルチバイトなコードについては、たとえば、UTF-8の「あ」は escape() だと %u3042、encodeURL()およびencodeURIComponent() だと %E3%81%82。
javascript: escape(), encodeURI(), encodeURIComponent() 比較 (groundwalker.com)

しかし次のようなスクリプトを動かしてみると、query中の値が'%u3042'の時と、'%E3%81%82'の時では、paramメソッドで取得した値は一致しません。

use CGI;
my $query = CGI->new('by_escape=%u3042&by_encodeURI=%E3%81%82');
my $escaped = $query->param('by_escape');
my $encoded = $query->param('by_encodeURI');
print "escaped : $escaped\nencoded : $encoded\n";

'%uXXXX'形式を切り捨ててしまえればよいのですが、そうではなく、どちらで値が来ても対応したい時もあります。この時は以下のように、一度CGI::Util::escapeでエスケープしてから、再度CGI::Util::unescapeでアンエスケープすると同じ値になるようです(CGI::Utilはuse CGIした時点で利用可能になっています)。

my $escaped = CGI::Util::unescape(CGI::Util::escape($query->param('by_escape')));
my $encoded = CGI::Util::unescape(CGI::Util::escape($query->param('by_encodeURI')));

ちょっとしたことですが、CGI::Utilを明示的にuseし、escapeとunescapeメソッドを取り込んでおいても良いでしょう。

use CGI;
use CGI::Util qw(escape unescape);
my $query = CGI->new('by_escape=%u3042&by_encodeURI=%E3%81%82');
my $escaped = unescape(escape($query->param('by_escape')));
my $encoded = unescape(escape($query->param('by_encodeURI')));
print "escaped : $escaped\nencoded : $encoded\n";

実際には、escapeした時点で両者の値は同じになっています。このことは、CGIモジュールのquery_string関数が、元々のクエリストリングがどちらのエスケープ方式だった値に対しても、"%XX%XX%XX"形式の値を返していることで気がつきました。

CGIモジュールによるアップロード・ファイルの受け取り

ファイルをアップロードするためのフォームの書き方については、HTMLに関する文書やページをご覧ください。 おおざっぱに言ってしまえば、ポイントは2点だけです。

以下で、最小限のアップロードフォームができあがります。

<form action="$script" method="post" enctype='multipart/form-data'>
  <input type="file" name="file" /> <input type="submit" /> <input type="reset" />
</form>

CGIモジュールでは、param関数を呼ぶことで、アップロードされたファイルのファイルパスを返します。 これは、これを読み込むためのファイルハンドルとしても扱えます。 読み込んだファイルは、別ファイルに出力するでしょうから、次のようにして受信ファイルを扱うことができます。

use CGI;
my $buffer;
my $query = CGI->new;
my $file = $query->param('file');
my $file_name = ($file =~ /([^\\\/:]+)$/) ? $1 : 'uploaded.bin';
open(OUT, ">$file_name") or die(qq(Can't open "$file_name".));
binmode OUT;
while (read($file, $buffer, 1024)) {
  print OUT $buffer;
}
close OUT;

空ファイル等への対処

ユーザーが、必ずファイルを指定してからsubmitをしているとは限りません。 この点について、IBMの「CGI.pmモジュールを綿密に検討する」は$fileがファイルハンドルになっているかをチェックしています。

現在では、CGIモジュールのバージョン2.47でuploadというメソッドが用意されました。 これはファイルハンドルか、問題があった時にはundefを返します。 paramメソッドで取得した値だと、ファイル名をファイルハンドルとして扱っているというあまり良くない方式になってしまうという話もあり、こちらを使うことをお勧めします。

use CGI;
my $buffer;
my $query = CGI->new;
my $fh    = $query->upload('file') or die(qq(Invalid file handle returned.)); # Get $fh
my $file  = $query->param('file');
my $file_name = ($file =~ /([^\\\/:]+)$/) ? $1 : 'uploaded.bin';
open(OUT, ">$file_name") or die(qq(Can't open "$file_name".));
binmode OUT;
while (read($fh, $buffer, 1024)) { # Read from $fh insted of $file
  print OUT $buffer;
}
close OUT;

エラーハンドリングとアップロードサイズ制限

CGIモジュールによるリクエストハンドリングでは、何らかのエラーがあった時に次のようにしてエラーメッセージを取得することができます。

my $message = $query->cgi_error;
die($message) if ($message);

例えば、大きなファイルのアップロードが行われ、実行中にブラウザの「読込停止」が行われた場合には、"400 Bad Request (malformed multipart POST)"が返されます。 エラーがない時には、$messageは空です。

エラーによるdieが行われた時に、その情報がクライアント側に伝わるようにするには、エラー出力がブラウザに行われるようにするのが一つの手段でしょう。 CGI::Carpモジュールを早い時点で組み込んでおくことで、これを実現できます。 もちろん、もっと手間をかけてやって、ユーザーフレンドリーなエラーを返してやるのも良いことです。

use CGI::Carp qw(fatalsToBrowser);

もう一点重要なのは、CGIモジュールにリクエストで受け付ける最大データサイズを設定し、それを超えたデータが送られてきた時にはエラーを検出できることです。 アップロードサイズの制限は、できるだけスクリプトの先頭近く(use CGI;のできるだけ直後)で、以下の様にバイト数で指定します。

$CGI::POST_MAX = 1024 * 1024;           # 1024 * 1KBytes = 1MBytes.

これが指定されていると、paramメソッドを呼び出した時点でデータサイズが超過している時には以下が行われます。

  • paramメソッドの返り値が空になる。
  • $query->cgi_errorの返り値が'413 Request entity too large'になる。

以下の様に、CGIモジュールを組み込む時点で、paramメソッドの空呼びをしてエラーの有無を確認することが正しいかもしれません。

use CGI;
use CGI::Carp qw(fatalsToBrowser);
$CGI::POST_MAX = 1024 * 1024;           # 1024 * 1KBytes = 1MBytes.
my $query = CGI->new;
$query->param;
die($query->cgi_error) if ($query->cgi_error);
my $buffer;
my $fh    = $query->upload('file') or die(qq(Invalid file handle returned.)); # Get $fh
my $file  = $query->param('file');
my $file_name = ($file =~ /([^\\\/:]+)$/) ? $1 : 'uploaded.bin';
open(OUT, ">$file_name") or die(qq(Can't open "$file_name".));
binmode OUT;
while (read($fh, $buffer, 1024)) { # Read from $fh insted of $file
  print OUT $buffer;
}
close OUT;

このほかの留意点

このほかの留意点としては、大きいのはCGI::Liteの前身、CGI_Liteでも問題にしたファイル名でしょう。 以下のようなチェック項目があります。

  • ファイル名の文字コード統一
  • ファイル名の半角カナ禁止
  • ファイル名の禁則文字の排除
  • '.cgi'などの危険な拡張子の排除

ひとまずここでは取り上げないことにしますが、CGI::LiteやCGI_Liteに比べれば、保存ファイルは意識的にファイル名をつけて開く分、対処には特別な知識は不要でずっと容易でしょう。

また、既存のファイルと同じ名前のファイルがアップロードされたときのハンドリングも考える必要があります。 この部分は、上書きを許すのか、条件付で許すのか、拒否するのかなど、ルールを決めてCGI内で適切に処理していく部分です。

ファイルの送信中に停止がかかった場合と似て非なる状況として、ファイルの保存中にCGIプロセスが停止させられた場合があります。 この場合、特に上の同名ファイルの上書きをしているときに発生してしまうと、古いデータは失われていて新しいデータは途中までしかない破損状態、ということになってしまいます。 これを防ぐには、一時ファイルに全データを書き出してから、保持するためのファイル名にrenameするのが古典的な解決方法です。 ただし、一時ファイル名が重複したら、などの問題が起こらないように、他所の検討が必要でしょう。

CGIモジュールでリダイレクトする。

CGIモジュールには、通常のヘッダを生成するheaderメソッドの他に、リダイレクト用のriderectというメソッドがあります。リダイレクトをしたい時には、次のようにするだけで済みます。

print $CGI->redirect('http://www.example.com/');

次のような出力になります。

Status: 302 Moved
Location: http://www.goo.ne.jp

Statusはデフォルトでは'302 Moved'ですが、302は一時的なリダイレクト(Moved Temporary)を示します。もし、例えば恒久的なリソースの移動(Moved Permanenty)を示す301を返したい、と思ったら次のようにします。

print $CGI->redirect(-url => 'http://www.example.com/', -status => '301 Moved');

'-url'は、代わりに'-location'、'-uri'でも構いません。'-url'や'-status'以外には、'-target'(ターゲット・ウィンドウ)、'-cookie'(クッキー)などを指定することができます。

CGIモジュールでスクリプトのURLを取得する。

CGIモジュールには、スクリプト自身のURLを取得するためのurlというメソッドがあります。 urlメソッドの説明は、perldoc(またはperdoc.jpにある和訳)を確認してください。

urlメソッドが返す値は、オプションによってさまざまです。例えば、以下のURLに試験スクリプトを設置したとします。

http://localhost/dir/url.cgi

これに、次のようにPATH_INFOとクエリをつけてアクセスしてみます。

http://localhost/dir/url.cgi/path/info?query=string

オプションの組み合わせを変えながらurlメソッドを呼び出した場合、返り値は次のようになります。

呼び出し方返り値
$CGI->url()http://localhost/dir/url.cgi
$CGI->url(-full, 1)http://localhost/dir/url.cgi
$CGI->url(-relative, 1)url.cgi
$CGI->url(-absolute, 1)/dir/url.cgi
$CGI->url(-path_info, 1)http://localhost/dir/url.cgi/path/info
$CGI->url(-path_info, 1,-query, 1)http://localhost/dir/url.cgi/path/info
$CGI->url(-base , 1)http://localhost/dir/url.cgi

なお、"$CGI->url"と"&CGI::url"はほぼ同じ結果を返しますが、"CGI->url"としてしまうと、クエリ・ストリングが落ちるなどの違う結果になります(そしてこれは、おそらく呼び出し方を間違えています)。

試験に使用したCGIスクリプトについてはPerlメモ/CGIモジュール/urlメソッドの試験参照。

CGIモジュールによるヘッダ、フッタの出力

CGIモジュールには、HTTPヘッダを出力するheader、HTMLヘッダとbodyタグを出力するstart_html、HTMLフッタを出力するend_htmlというメソッドがあります。 これらを使うと、本文部分以外はほとんど手を抱えずに、CGIが出力しなくてはいけないデータをそろえることができます。 各メソッドの説明は、perldoc(またはperdoc.jpにある和訳)を確認してください。

例えば、次のようにすれば、エラーの起きない、一通りのCGI出力をすることができます。

use CGI;
my $query = CGI->new;
print $query->header;                                      # httpヘッダが出力される
print $query->start_html(-title => "CGIモジュールテスト"); # <html><header>~</header><body>部が出力される
print "CGIモジュールのテストです。";
print $query->end_html;                                    # </body></html>部が出力される

文字コードの指定

上の出力だと、エラーは起きないものの、ブラウザが文字コードを(例えば西ヨーロッパ言語などと)誤認識したかもしれません。 文字化けを避けるには、headerメソッド、start_htmlメソッドに、それぞれ言語やキャラクタ・セットを指定するオプションを与えます。

use CGI;
my $query = CGI->new;
print $query->header(-charset => 'EUC-JP');  # キャラクタ・セットを指定
print $query->start_html(
  -title    => "CGIモジュールテスト",
  -charset  => 'EUC-JP',                     # キャラクタ・セットを指定
  -encoding => 'EUC-JP',                     # キャラクタ・セットを指定
  -lang     => 'ja-JP'                       # 言語を指定
);
print "CGIモジュールのテストです。";
print $query->end_html;

次のようにすると、Jcodeモジュールでの変換用、CGIモジュールでのヘッダ出力用の文字コードの指定を、$kanjicode変数一つで済ませられるので便利でしょう。

use CGI;
use Jcode;

my $kanjicode = 'sjis';       # jis/sjis/euc/utf8
my $title     = 'CGIモジュールテスト';
my $body      = 'CGIモジュールのテストです。';

# $kanjicodeから適切に、キャラクタ・セットと言語の決定、データの文字コード変換を行う。
my $charsets  = {'euc' => 'EUC-JP', 'jis' => 'ISO-2022-JP', 'sjis' => 'Shift_JIS', 'utf8' => 'UTF-8'};
my $charset   = $charsets->{$kanjicode} ? $charsets->{$kanjicode} : undef;
my $lang      = $charsets->{$kanjicode} ? 'ja-JP' : 'en-US';
$title        = (Jcode->can($kanjicode)) ? jcode($title)->$kanjicode : 'invalid kanjicode';
$body         = (Jcode->can($kanjicode)) ? jcode($body)->$kanjicode  : "invalid kanjicode '$kanjicode'";

# キャラクタ・セットと言語を考慮したCGI出力
my $query     = CGI->new;
print $query->header(-charset => $charset);
print $query->start_html(-title => $title, -charset => $charset, -encoding => $charset, -lang => 'ja-JP');
print $body;
print $query->end_html;

CGIモジュールでHTMLフォームを生成する。

CGIモジュールには、フォームの作成を助けるメソッドがあります。 代表的なところでは、<form>タグを生成するstart_form、テキストフィールドを生成するtextfield、</form>タグを生成するend_formなどが挙げられます。

これらのメソッドの便利な点の一つに、URLには現在のURL、フィールド値は前の画面から送られてきた値など、デフォルト値を設定してくれることがあります。 次のCGIを実行すると、formタグにURLが自動的にセットされること、'text'の値がデフォルト値として自動的に使われることが確認できるでしょう。

#!perl
print $CGI->start_html('Form test');
print $CGI->start_form;
print $CGI->textfield('-name' => 'text');
print $CGI->submit, $CGI->reset;
print $CGI->end_form;
print $CGI->end_html;

ただし、start_formで自動的に補われるURLは、QUERY_STRINGも含めたURLです。 "start_form"の行を、GETメソッドを指定した以下で置き換えてみると確認できます。

print $CGI->start_form('-method' => 'GET');

一般には、POSTメソッドでもGETメソッドでも、必要な値はhiddenフィールドで受け渡し、formタグにはQUERY_STRINGを省いたURLを指定することの方が多いでしょう。 次のように、start_formで'-action'を指定してやることで、そうすることができます。

print $CGI->start_form('-action' => $CGI->url);

コメント

コメントスパムがひどいためこのページのコメント欄を削除しました。コメントしたい方は暫定的に「掲示板」のページへお願いいたします。

my $file_name =
 ($file =~ /[???/:]*(.+)?.(aiff|mp3|m4a|sit|sitx)$/) ?
 "$1.$2" : 'uploaded.bin';