@blog.justoneplanet.info

日々勉強

PHP Sessions(PHPにおけるセッションの基本)

HTTPプロトコルはステートレスなプロトコルであるため、状態を維持することができない。従って、サーバは2つの異なったリクエストが同じユーザによるものなのか判別できない。そこで登場したのがセッションである。

■セッション

セッションとは

  • 基本的にはクッキーにIDを保存する形で実現される(但し、GETクエリにIDを保持する形式の場合もある)
  • 一般的にはサーバ上にユーザ毎のファイルが生成されて管理される(但し、session_set_save_handler関数でDBなどに変更することもできる)
  • セッションファイルの有効期限はphp.iniのsession.gc_maxlifetiomeディレクティブで変更する
  • session_start関数を呼ぶと、デフォルトでキャッシュされないようになる(HTTPヘッダにCache-control: no-cacheが付加される)
  • DNSラウンドロビンさせると維持できない。(NFSなどで共有する必要がある)

URLにIDを付加する形式にする方法

php.iniで以下のように記述する。(セキュリティリスクからデフォルトではoff)

session.use_trans_sid = on

セッションをスタートする方法

以下のように、session_start関数を使用する。但し、全ての出力よりも先に記述せねばならない。可読性の観点から、スクリプトの先頭部分に記述することをお勧めしたい。また、session_regenerate_id関数を使用することにより、セッションIDの固定化を防ぎ、よりセキュアなアプリケーションになる(セッションフィグゼーション)。

<?php
session_start();
session_regenerate_id(true);
?>

また、以下のようにphp.iniのsession.auto_startディレクティブでも変更が可能である(デフォルトは0)。但し、この方法ではスクリプトが実行される前にセッションがスタートするため、セッションデータにユーザ定義オブジェクトのインスタンスを含めることはできない。

session.auto_start = 1

セッションに保存したデータを取り出す方法

以下のように、スーパーグローバル変数$_SESSIONを使用する。

<?php
session_start();
session_regenerate_id(true);//setcookie('PHPSESID', 'new_id')のような効果
if(isset($_SESSION['key'])){
    $_SESSION['key']++;//IDと対になったファイルへの変数の記録
}
else{
    $_SESSION['key'] = 1;//IDと対になったファイルへの変数の記録
}
if(isset($_SESSION['key']) && $_SESSION['key'] % 2 === 0){
    print('偶数' . $_SESSION['key']);
}
else{
    print('奇数' . $_SESSION['key']);
}
?>

あらゆるデータ型を保持(代入)できる。イメージとしてはスクリプトが終わっても保持される、ユーザベースの変数といった感じだ。デフォルトで簡単にセッション管理できるのが、個人的にPHPの好きなところだ。

PHP to HTTP Headers(PHPによるHTTPヘッダの操作)

基本的に、PHPではHTTPヘッダが自動的に生成され、クライアントに送信される。ユーザ側で任意のHTTPヘッダを加えるときには、以下のようにheader関数を使う。また、header関数はあらゆる出力の前に記述しなくてはならない。

<?php
header('Location: http://google.com');//redirect to google
?>

■リダイレクト

リダイレクトを行いたいときは、header関数の引数に以下のような記述をする。またスクリプトの実行中にユーザが読み込みを中止するなども考えられるので、exit関数を使用してスクリプトの終了を明示したほうが安全である。

<?php
header('Location: http://yahoo.com');
exit;
?>

HTTPの仕様では、Locationヘッダには(相対パスや絶対パスでなく)完全な形のURLを指定しなくてはならない。

■ページのキャッシュと有効期限

通常プロキシやブラウザはページをキャッシュするが、その有効期限を以下のように明示することができる。

<?php
$date = date("D, j M Y H:i:s", time() + (60 * 60 * 24 * 10));//10 days from now
header("Expires: {$date} UTC");
header('Cache-Control: Public');
header('Pragma: Public');
?>

また、キャッシュさせたくない場合は、以下のようにキャッシュをさせない指定とページの有効期限を過去にする指定の両方を用いるのが一般的である。

<?php
header('Cache-Control: no-cache, must-revalidate');
header('Expires: Thu, 31 May 1984 04:35:00 GMT');
?>

但し、ブラウザ側で無視するような設定にすることもできるため、上述の設定が正しく反映されている事を前提としたプログラムを実装してはならない。

■圧縮

HTTPプロトコルはgzipによる圧縮と解凍をサポートしていて、一般的に5KBを超えるようなページに有用であり、ほとんどのブラウザでサポートされている。以下のようにスクリプトの先頭に出力バッファリング(output buffer)を開始する関数、ob_startを使用することで対応する。

<?php
ob_start('ob_gzhandler');
?>

ob_gzhandlerはページを圧縮するための組み込み関数である。また、以下のようにphp.iniファイルの記述を変更する方法でも対応できる。

zlib.output_compression = on
zlib.output_compression_level = 6

■クッキー

クッキーはWebアプリケーションがブラウザに保存する小さなテキストデータである。

クッキーは

  • 1つのクッキーは4KB以下とする
  • 1つのドメインで最大20個とする
  • 1つのクライアントで最大300個しか保持できない

クッキーをブラウザにセットする

クッキーをブラウザにセットするには、以下のようにsetcookie関数を使用する。

<?php
setcookie(name[, value[, expire[, path[, domain[, secure]]]]]]);
?>
引数について
name
クッキーの名前(空白やセミコロンは使用できない)
value
任意の文字列値
expire
有効期限。1970年1月1日午前0時から秒数でカウントした値(=time関数)。セットしなかった場合は、ブラウザが閉じられると削除される。
path
指定の配下のページのみクッキーを送信する。デフォルトは発行ページのディレクトリ以下
domain
指定のドメインのみクッキーを送信する。発行サイトのサブドメインのみ指定が可能。デフォルトは現在のサーバのホスト名
secure(bool値)
trueにするとhttps通信時のみ、クッキーを送信する。デフォルトはfalse
使用例
<?php
setcookie('key', 'value', time() + (60 * 60 * 24 * 30));
?>

クッキーをブラウザから読み取る

厳密にはブラウザは対象の全てのリクエストでクッキーを送信しているので、PHP側でのデータの受け取り方ということになる。以下のようにスーパーグローバル変数$_COOKIEを使用するとアクセスできる。

<?php
if($_COOKIE['key'] === 'value'){
    //code
}
?>

また、以下のようにクッキーに配列リテラルが含まれる場合

<?php
setcookie('name[nick]', 'value', time() + (60 * 60 * 24 * 30));
setcookie('name[john]', 'value', time() + (60 * 60 * 24 * 30));
setcookie('name[mike]', 'value', time() + (60 * 60 * 24 * 30));
?>

以下のように、$_COOKIE[‘key’]の値も配列となる。

<?php
var_dump($_COOKIE['name']);
/*
array(3) {
  ["nick"]=>
  string(5) "value"
  ["john"]=>
  string(5) "value"
  ["mike"]=>
  string(5) "value"
}
*/
?>

PHPにおいてクライアントリクエストに含まれる配列リテラルは、PHP内部で配列に変換される。COOKIEの扱い方について個人的には、PHPはPerlよりも簡単で扱いやすいと思う。

PHP Forms and URLs(フォームとURLの処理)

HTTPメソッドにはGETとPOST(の他のあるが今回は触れない)がある。GETメソッドはURLの一部としてデータを送信できるに過ぎず、データの容量やタイプに制限がある。POSTメソッドはプログラムやサーバの設定が許す範囲の大きさのデータを扱え、ファイルのアップロードなども行える。

■GETメソッド

以下のように、form要素のmethod属性にGETと記述する。

<form acton="index.php" method="get">
<input type="text" name="name" />
<input type="text" name="tel" />
<input type="submit" name="submit" value="submit" />
</form>

上述のGETメソッドを使用したフォームで送信を行うと、入力が「John Norton」「0120123456」のとき、以下のURLでブラウザはリクエストを行う。

http://sample.org/index.php?name=John+Norton&tel=0120123456&submit=submit

スペースは「+」に、「&」は「%26」に、「=」は「%3d」といった具合に自動でエンコードされる。index.php側でデータを受け取るには以下のようにスーパーグローバル変数$_GETを使用する。

<?php
$name = $_GET['name'];
?>

URLのエンコードとデコードについて

前述のようにデータを送信するときには、自動でエンコード処理されるが、PHPでデータを受け取るときはインタプリタが自動でデコードしてくれる。従って例えば「John+Norton」でリクエストされた場合でも以下のように、$_GET[‘name’]内には「John Norton」が格納されている。

<?php
print($_GET['name']);//John Norton
?>

但し、リンク先のURLを文字列として出力する必要がある場合などは、以下のようにプログラムでエンコードしてあげる必要がある。

<?php
$name = 'John Norton';
?>
<a href="<?php print('http://sample.org/index.php?name=' . urlencode($name)); ?>">詳細はこちら</a>

■POSTメソッド

以下のように、form要素のmethod属性にPOSTと記述する。

<form acton="index.php" method="post">
<input type="text" name="name" />
<input type="text" name="tel" />
<input type="submit" name="submit" value="submit" />
</form>

上述のフォームでPOSTメソッドを使いリクエストをした場合、index.php側でデータを受け取るには以下のようにスーパーグローバル変数$_POSTを使用する。

<?php
$name = $_POST['name'];
?>
通常フォームのデータを受け取る場合

相当に簡略化しているが、submitボタンが押されたリクエストか判断するために、以下のようにif構文を用いることが多い。

<?php
if(isset($_POST['submit'])){
    //code
}
?>

また、以下のようなコードもしばしば見つけるが、「Notice: Undefined index・・・」と注意が表示される原因にもなる。submitキーのデータが無い状態でページをリクエストすると、$_POSTの配列にsubmitのキーが存在しないことに起因するものである。

<?php
if($_POST['submit']){
    //code
}
?>

■チェックボックスのデータをPHPで受け取る方法

name属性に重複名がある場合、PHPでは以下のように、任意の名前の後に[]を付加しなければ正しくデータを受信できない。

<?php
if(isset($_POST['submit']) && isset($_POST['language']) && $_POST['language']){
	foreach($_POST['language'] as $lang){
		print($lang . PHP_EOL);
	}
}
?>
<form action="<?php print($_SERVER['SCRIPT_NAME']); ?>" method="post">
<p>What languages do you use?</p>
<ul>
<li><input type="checkbox" name="language[]" value="javascript" />JavaScript</li>
<li><input type="checkbox" name="language[]" value="php" />PHP</li>
<li><input type="checkbox" name="language[]" value="mysql" />MySQL</li>
</ul>
<input type="submit" name="submit" value="submit" />
</form>

このコードでは条件文で$_POST[‘language’]をテストしているが、チェックボックスが一つもチェックされていないときに、$_POSTにlanguageキーが存在しなくなるためである。また、$_POST[‘language’]が配列となる仕組みは以下の特性に起因する。

<?php
if(isset($_POST['submit'])){
    print($_POST['dat']['name']);
    print($_POST['dat']['tel']);
}
?>
<form action="<?php print($_SERVER['SCRIPT_NAME']); ?>" method="get">
<ul>
<li><input type="text" name="dat[name]" /></li>
<li><input type="text" name="dat[tel]" /></li>
</ul>
<input type="submit" name="submit" value="submit" />
</form>

つまりHTTPクエリのキー内の配列リテラルはそのままPHPに引き継がれるということになる。そして同名キーが存在する場合は、データを正しく取得する方法でもある。

■スーパーグローバル変数$_REQUEST

スーパーグローバル変数$_REQUEST[‘key’]を使用すると、$_POST、$_GET、$_COOKIEにアクセスが出来るが、データのどういったものなのか分からずセキュリティを下げる要因になる。従って、$_REQUESTは使用しないことをお勧めする。

■ファイルのアップロード

フォームについて

ファイルのアップロードを行う場合のフォームは、以下のようにenctype属性を記述せねばならない。そして、MAX_FILE_SIZEでは転送できるファイルの最大容量をバイトで指定できるがtype=”file”の前に記述しないと効果がない。また、upload_max_filesizeより大きな値を指定した場合は無視される。

<form enctype="multipart/form-data" action="index.php" method="post">
<input type="hidden" name="MAX_FILE_SIZE" value="50000">
<input type="file" name="file" />
<input type="submit" name="submit" value="submit" />
<form>

但し、MAX_FILE_SIZEの値はクライアント側で編集可能な値であり、これを信頼したアプリケーションを作ってはならない。但し、ファイルの転送が終わってから容量制限の判断をする、といった無駄な時間をユーザに与えてしまうといった愚行を防ぐには非常に有効である。

スクリプトについて

上述のHTMLから送信されたファイルを受け取るには、以下のように$_FILESを用いる。

<?php
$dat = $_FILES['file'];
var_dump($dat);
/*
array(5) {
  ["name"]=>
  string(12) "20090428.txt"
  ["type"]=>
  string(10) "text/plain"
  ["tmp_name"]=>
  string(14) "/tmp/phpGxxRLV"
  ["error"]=>
  int(0)
  ["size"]=>
  int(140)
}
*/
?>

以上を見てみると、$_FILESには実際のファイルの情報でなく、以下のような情報が格納されている。

name
送信したファイルのクライアント上の名前
type
ブラウザが判断したMIMEタイプ
tmp_name
サーバ上に保存された一時的なファイルの名前
error
エラー番号
size
ファイルのサイズ(バイト)

アップロードしたファイルをアプリケーションで使用する場合は、以下のようにmove_uploaded_file関数を使う。move_uploaded_file関数では、第一引数にテンポラリーファイル名、第二引数に保存するファイル名を指定する。

<?php
$filename = 'sample.txt';
if(move_uploaded_file($_FILES['file']['tmp_name'], "/www/sample.org/httpdocs/doc/$filename"){
    //code
}
?>

move_uploaded_file関数は指定ファイルがユーザーによりアップロードされたものか判定する処理も含まれている。また、アップロードされたファイルをサーバー上に保存する際に、クライアントの送信に依存した$_FILES[‘file’][‘name’]をそのまま使うのは、セキュリティ上の問題となり得る。ちなみにファイルがアップロードできたか判定するには、以下のようにis_uploaded_file関数を使用する。

<?php
if(is_uploaded_file($_FILES['file']['tmp_name'])){
    //code
}
else{
    print($_FILES['file']['error']);
}
?>

ファイルアップロードに関連するphp.iniの設定

post_max_size
POSTメソッドで許可するデータのサイズ。upload_max_filesizeより大きな値を設定する必要がある。
max_input_time
POST、GET 、ファイルアップロードなどの入力を パースする最大の時間を設定。秒単位で指定。
upload_max_filesize
アップロードできる最大ファイル容量。

■GETとPOSTの性質

DBを更新するような、サーバに変更を加えるような処理にGETリクエストを使ってはならない。ファイルをはじめとした、大量のデータ送信する場合はPOSTを使わなければならない。

GET POST
データの送信 フォームのパラメータをURL形式にエンコードして、クエリ文字列として送信する フォームのパラメータをHTTPリクエストに直接、書き込む
ブックマーク 可能 不可能
仕様上の冪等性
任意のURLで何回リクエストしても結果が同じというような実装をすべきある。またブラウザは結果をキャッシュする
×
リクエストの結果が場合によって異なるなど、結果をキャッシュしないような場面に適している

冪等性について

冪等性とは「ある操作を複数回行っても動作が同じである事」である。

また、POSTリクエストでも、実際には一般ユーザーが「戻る」ボタンを押し「リロード(再送信)」する事もある。従って、アプリケーションを制作する際は考慮しなくてはならない。

フォーム要素について

  • method属性の属性値は大文字で書いた方が良い(理論上、大文字/小文字の両方を受け付けるが、ブラウザのバグを考慮)