tips – PHP をストリームエディタとして使う

php -R オプションを使うと、perl の -ne オプションと同様に入力行単位で任意のコードを実行することができます。

php -R “実行するコード”

標準入力から一行ずつ $argn に格納されます。末尾の改行文字は取り除かれるため、必要であれば改行を自分で付加します。
 
例: HTMLの特殊文字をエスケープする

cat foo.html | php -R ‘echo htmlspecialchars($argn).”\n”;’

セッションフィクセーション問題

ITpro の記事で Session Fixation 問題について書かれていたので PHP での対策例を書いておきます。
 
PHP ではこの問題を解決するために、session_regenerate_id() という関数が提供されています。

<?php
 
session_start();
 
$old = session_id();
session_regenerate_id();
$new = session_id();
 
echo “$old -> $new<br>”;
// 結果:
// d4aa3b6cfaedca70dc09262dfbf8c39e -> 53c031b85fe89e00d3b151eddfd91733

具体的な利用方法はこんな感じです。

<?php
 
session_start();
 
$algo = “sha256”;
 
// パスワードは生で保存せずハッシュで持つ(5.1.2 以前は hash() 関数を使えないため mhash() または md5() で代用する)
$password_hash = “d74ff0ee8da3b9806b18c877dbf29bbde50b5bd8e4dad7a3a725000feb82e8f1”; // hash($algo, “pass”);
$bAuthorized = !empty($_SESSION[‘authorized’]);
 
if(!$bAuthorized){
        if(empty($_GET[‘p’]) || hash($algo, $_GET[‘p’]) !== $password_hash){
                // ログインページ表示
?>
                未認証(session_id = <?=session_id()?>)<br>
                <form action=”” method=”GET”>
                        <input type=”password” name=”p”>
                        <input type=”submit” value=”ログイン”>
                </form>
 
<?php
                exit;
        }
        $bAuthorized = $_SESSION[‘authorized’] = true;
        // セッションフィクセーション対策(Session Fixation)
        session_regenerate_id(); // ログインが成功した段階でセッションID を更新垢
 
}
 
?>
認証済み(session_id = <?=session_id()?>)

このサンプルを実行すると、パスワードに “pass” という文字列を入れて認証すると session_id が書き換わるのを確認できます。
 
この関数を使うと、透過的にセッションIDが書き換わるので関数呼び出し側はセッションフィクセーション対策を簡単に施すことができます。ただし当然ですが既に何かを

tips – Net_URL で URL を解決する

相対 URL から絶対 URL に変換するには PEAR::Net_URL が利用できます。
URL の要素を分解して取得することができるため、便利そうです。
 
インストール:

$ pear install Net_URL

HTTP_Request が依存しているので、ネットワーク関係の PEAR パッケージを入れたことがあれば既に入っていることが多いです。
 
例:とりあえず使ってみる

<?php
// http://example.com/foo/boo/file.php で実行する
 
require_once “Net/URL.php”;
 
$url =& new Net_URL(“next.html”);
echo “<PLAINTEXT>”;
var_dump($url);
echo $url->getURL();
?>

 
例: GET クエリで渡された URL が、自サービス内 URL かどうかを調べる
※外部サイトへの踏み台になるのは防ぎますが、内部のリファラチェック回避の踏み台にはなり得る事に注意。

$url =& new Net_URL($_GET[‘url’]);
if($url->host != $_SERVER[‘SERVER_NAME’]){ // 相対パスまたは同じドメインの絶対URLなら HTTP_HOST や SERVER_NAME と一致する。
echo “error!”;exit; //
}
echo “ok”;

 
参考:
PEAR::Net_URL(pear.php.net)

デッドロックのテスト用コード

test_lock.php:

<?php
error_reporting(E_ALL);
$f = array();
$f[0] = null;
$f[] = fopen(“lock_1”, “r+”);
$f[] = fopen(“lock_2”, “r+”);
 
if(@intval($_SERVER[‘argv’][1]) >= 2){
$task = array(2, 1);
}else{
$task = array(1 ,2);
}
 
foreach($task as $n){
echo “Locking $n … “;
 
echo flock($f[$n], LOCK_EX, $t=true)? “OK”: “Fail”;
echo “\n”;
echo “waiting some seconds…”;
sleep (5);
echo “OK\n”;
}
echo “Finished!\n”;

実行:

$ php test_lock.php &
$ php test_lock.php 2 &

 
ひとつのみ起動した場合 Finished! と表示されますが、上のように並列で実行した場合は正しく動作しなくなります。
 
手元の環境では FreeBSD 6.2 と Windows XP SP2 の場合はデッドロックして固まり、Linux 2.4.21-50.ELsmp の場合は 2番目のロックに fail し、デッドロックにはなりませんでした。
 
参考:
– flock のモード切替え時の挙動は環境によって異なる[2008-01-08-1]

flock のモード切替え時の挙動は環境によって異なる

flock() でロック獲得中にロック状態を切り替える(LOCK_SH から LOCK_EX にする、など)と、環境や状況によってロックが保持されない場合があります。
 
検証用コード:
A… ロック切り替えプロセス(keeper.php )

<?php
$f=fopen(“t.txt”,”r+”);
for($i = 0; $i < 10; $i++){
  // ロック切り替えまたは取得(LOCK_EX -> LOCK_SH)
  echo “shared($i)\n”;
  flock($f, LOCK_SH);
  sleep(3);
  // ロック切り替え(LOCK_SH -> LOCK_EX)
  echo “exclusive($i)\n”;
  flock($f, LOCK_EX);sleep(3);
}
echo “Done! the treasure was protected!\n”; // ここまで theif.php にロックがとられなければ期待通り。
?>

B…ロック奪取プロセス(thief.php)

<?php
$f=fopen(“t.txt”, “r+”);
flock($f, LOCK_EX); // ロック待ち
// ロック奪取成功
echo “lock obtained!\n”;
echo “im doing something, hehehe.”;
for($i=0;$i<10;$i++){
  sleep(1);
  echo “.”;
}
echo “\nfinished! bye ;P\n”;’
fclose($f); // ロック開放
?>

実行:

$ touch t.txt
$ nice php locker.php &
$ php thief.php

結果:
Windows XP SP2 + Cygwin + PHP 5.1.2 (FS: NTFS) … ロック保持されない(thief won)
Linux 2.4.21-50.ELsmp + PHP 4.4.6 (FS: ext3) … ロック保持される(keeper won)
FreeBSD 6.2 + PHP 5.2.5 (FS: ufs) … ロック保持される(keeper won)
 
Windows XP (FS: NTFS) はロック自体はできますが、LOCK_EX <-> LOCK_SH の切り替えでいったんロックが解除されるという挙動になりました。
 
なお、PHP マニュアルの flock() の項にもあるとおりファイルシステムが FAT の場合など同じ Windows でも環境によっては結果が変わるため、この結果を鵜呑みにせず必ずご自身の環境でテストしてください。


追記(2008-01-09):
ちなみに、PHP 5.2.5 でソースをたどってみたところ PHP の flock() は ext/standard/flock_compat.c で flock (2) ではなく fcntl (2) を使って実装されていました。Linux や BSD など fcntl でロック変換がアトミックにできる場合は問題なく動作しそうです。
なお Windows で、かつ fcntl がない(Cygwin など UNIX 擬似環境ビルドではない)場合は、 UNIX と互換性があるように LockFileEx() で実装されています。

CVS 入出力 を支援する PEAR パッケージ

PHP5.1 以降には fgetcsv(), fputcsv() という関数が標準で提供されていますが、別の選択肢として PEAR でも File_CSV というクラスが提供されています。
 
このクラスは File パッケージに含まれています。

pear install File

 
■利用例(tests/parser.php を改変したもの)
ファイルから読み込む

<?php
require_once ‘File/CSV.php’;
 
$file = “test.csv”;
 
$conf = File_CSV::discoverFormat($file); // ファイルからデータ構造を判別(区切文字やクォーテーションなど)
while ($fields = File_CSV::read($file, $conf)) { // 取得したデータ構造を利用して行単位で読み取る
    print_r($fields);
}

ファイルから配列に読み込む

<?php
require_once ‘File/CSV.php’;
 
$file = “test.csv”;
 
$conf = File_CSV::discoverFormat($file); // ファイルからデータ構造を判別(区切文字やクォーテーションなど)
while ($fields = File_CSV::read($file, $conf)) { // 取得したデータ構造を利用して行単位で読み取る
    print_r($fields);
}

CSV ファイルの中身をすべて配列として読み込む

<?php
require_once ‘File/CSV.php’;
 
$file = “test.csv”;
 
$conf = File_CSV::discoverFormat($file); // ファイルからデータ構造を判別(区切文字やクォーテーションなど)
while ($fields = File_CSV::read($file, $conf)) { // 取得したデータ構造を利用して行単位で読み取る
    print_r($fields);
}

ファイルから配列に読み込む

<?php
require_once ‘File/CSV.php’;
 
$file = “test.csv”;
 
$conf = File_CSV::discoverFormat($file); // ファイルからデータ構造を判別(区切文字やクォーテーションなど)
$data = File_CSV::readAll($file, $conf); // 取得したデータ構造を利用し、配列として全て取得する。
var_dump($data);
<<
ファイルに書きこむ例:

<?php
require_once ‘File/CSV.php’;
 
$file = “test.csv”;
 
// 書き込む内容
$data = array(
array(“foo”, “boo”, “woo”),
array(“fo\”o”, “bo,o”, “woo”), // エスケープが必要な文字を含むレコード
);
// CSV 書式
$conf = array(
    ‘fields’ => count($data[0]), // カラム数
    ‘sep’ => “,”, // 区切り子
    ‘quote’ => ‘”‘, // 囲み文字
    // ‘header’ => false, // NULL 以外を指定すると, read 時に一行目をヘッダとみなして各要素のキーとして扱う
);
// 書きこみ実行
foreach($data as $row) {
    File_CSV::write($file, $row, $conf); //
}

 
ファイルの書きこみ制御ができなかったり、設計上気になる点もありますが、
単純に読み書きするのには十分そうです。

文字列への文字単位のアクセスで波括弧が非推奨にかわった

ふと PHP のドキュメントを眺めていたら、次のように書かれていました
PHP: 文字列(English)

注意: $str{42} のように波括弧を使用してアクセスすることも可能です。 しかし、角括弧を使用する方法のほうが推奨されます。 なぜなら、{波括弧} 形式は PHP 6 で廃止される予定だからです。
 
Note: They may also be accessed using braces like $str{42} for the same purpose. However, using square array-brackets is preferred because the {braces} style is deprecated as of PHP 6.

PHP5 リリース前は

$str = “foo”; echo $str[0];

が非推奨で

$str = “foo”; echo $str{0};

が推奨されていましたが、方針転換をしたようです。
 
参考:
2004 年 3 月のマニュアル同項(English)(archive.org)

波括弧の後に任意の文字をゼロから始まるオフセットで指定することに より、文字列内の文字にアクセスすることが可能です。
 
    注意: 過去の互換性のため、配列括弧を使用することが可能です。しかし、 この構文はPHP 4に依存しています。
    
 Characters within strings may be accessed and modified by specifying the zero-based offset of the desired character after the string in curly braces.
  
    Note: For backwards compatibility, you can still use array-braces for the same purpose. However, this syntax is deprecated as of PHP 4.

以前はこうなっていました。

バグパターン – switch のデータ型は複数種類にすると予期しない結果になる

まずはサンプルコードを見て、結果を予想してみてください。

<?php
$result = true;
switch($result){
  case “error”: // エラー
    echo “failure”;
    exit;
  case “ok”: // 成功
  case true: //
    echo “success”;
    exit;
  default:
    echo “unexpected”;
    exit;
}
?>

一見、このコードを実行すると success が出力されるように思いますが、実際に実行してみると意外なことに failure が出力されてしまいます。
PHP の switch 文は、switch の引数と case で指定した値とを= ではなく) でチェックしているため、switch 引数が複数の種類の型になる場合は型変換などにより予想外の値と一致することになってしまうのが原因です。
 
実際どの値が何と一致するかについては以下の検証コードを実行してみると分かります。

// 検証コード:
<?php
// values to test
$targets = array(
   0,
   1,
   true,
   false,
   null,
   “”,
   “0.00e5”,
   “10f”,
   “0f”,
   array(),
   array(1),
   array(“key”=>”foo”),
);
 
// —-
foreach($targets as $target){
  if(is_bool($target))
      $s = $target? “true”: “false”;
  elseif(is_null($target))
      $s = “null”;
  elseif(is_string($target))
      $s = “‘{$target}'”;
  else
      $s = $target;
  echo $s .”(“.gettype($target).”)”.”:\t”;
  switch($target){ //”0.00e5″
    case ‘a’: echo “case a”; break;
    case ‘b’: echo “case b”; break;
    case ‘0’: echo “case zero”; break;
    case 10: echo “case int 10”; break;
    case 0: echo “case int 0”; break;
    default: echo “default”; break;
  }
  echo “\n”;
}

php 4.4.6 および php 5.2.3 で試したところ、実行結果次のようになりました。

0(integer): case a
1(integer): default
true(boolean): case a
false(boolean): case zero
null(NULL): case int 0
”(string): case int 0
‘0.00e5′(string): case zero
’10f'(string): case int 10
‘0f'(string): case int 0
Array(array): default
Array(array): default
Array(array): default

同じ empty 値でも 0, null, false, “” で結果が全く異なってしまいました。
また、 == について挙げられる数値文字列の比較時の問題(“0.00e5” == “0” が真になる,など)の影響も受けてしまっています。
 
この問題は、型チェックを厳密にしたり、strval(), intval() などで明示的に文字列,数値キャストを行い、switch 文の引数の型が一種類になることを保証することである程度回避できます。
しかし依然として数値として有効な文字列同士の比較の問題があるため、数値を含む文字列について正確さが必要な場合や、本当に複数型を判別させたい場合は= 演算子を使い if – elseif – else を使うほうが賢明でしょう。

if($result = true || $result= “ok”){
  echo “success”;
}elseif(empty($result) || $result
= “error”){
  // empty($result) は以下の式と等価。
  // ( !isset($result) || ($result= false || $result = null ||
  // $result= “” || $result = 0 )
  echo “failure”;
}else{
  echo “unexpected”;
}

 
参考:
switch 構文(php.net)
比較演算子(php.net)
strval()(php.net)

サンプルコード – Cache_Lite のファクトリメソッド(マルチユーザ対応)

Cache_Lite は大変便利なのですが、Linux/BSD などで、複数実行ユーザが同じ cacheDir (たとえば /tmp/ )を使った場合に、他ユーザが作ったキャッシュへのアクセス権限が得られないため正しく動作しません。
Cache_Lite の hashedDirectoryUmask オプションと umask() 関数で 0777 を指定すれば十分な権限が得られますが、キャッシュの盗聴やかいざんの可能性が出てくるためセキュリティ的によろしくありません。
 
簡単な対応として思い付くのは

‘cacheDir’ => ‘~/tmp’,

のようにユーザのホームディレクトリにフォルダを分けてやることですが、
今回はより汎用的にするため、ファクトリメソッド内部でユーザ判別をしてフォルダを分けることでマルチユーザ対応させてみました。
 
# 暫定的な実装なので、不備があるかもしれません。

// This source is made available under the terms of the BSD license.
 
/**
 * Cache_Lite 用汎用ファクトリメソッド。
 * (PHP 4, 5 対応)
 * @version 2007-11-08
 */
class CacheLiteFactory {
  /**
   * @access private
   */
  function CacheLiteFactory(){
      // no-op
  }
  /**
   * static.
   * @access private
   * @param array $newOptions
   * @return options
   */
  function _defaultOptions($newOptions = null){
     // 初期デフォルト設定
     static $defaultOptions = array(
         // 必要なら設定可。
      );
      if(!empty($newOptions)){
          $defaultOptions = $newOptions;
      }
      return $defaultOptions;
  }
  
   /**
    * インスタンス化に使うデフォルトのオプションを設定します。
    * 過去に設定していたオプションは全て消去されます。
    * 一部のオプションのみ追加、変更したい場合は getDefaultOption() で現在のデフォルトオプションを取得して再設定してください。
    *
    * static
    * @access public
    * @param array $newOptions 置き換えるオプション
    * @return void
    */
   function setDefaultOptions($newOptions){
      CacheLiteFactory::_defaultOptions($newOptions);
   }
   
   /**
    * インスタンス化に使われるデフォルトのオプションを取得します。
    * static
    * @access public
    * @return array 現在設定されているデフォルトのオプション
    */
   function getDefaultOptions(){
      return CacheLiteFactory::_defaultOptions();
   }
  
  /**
   * Cache_Lite インスタンスを取得する。
   * 引数を指定した場合、デフォルトオプションをベースに引数のオプションを追加,変更します。
   *
   * static
   * @access public
   * @param array $overrideOptions 上書きするオプションの連想配列
   * @return Cache_Lite
   */
   function factory($overrideOptions = array()){
       require_once “Cache/Lite.php”;
       // デフォルト値。
       $options = CacheLiteFactory::getDefaultOptions();
       
       if($overrideOptions && is_array($overrideOptions)){
            $options = array_merge($options, $overrideOptions);
       }
       if(empty($options[‘cacheDir’])){
           $options[‘cacheDir’] = “/tmp/”;
       }
        
       // 他ユーザのキャッシュはアクセス権限が十分にない(umask 0777 にしても削除はできない)ので
       // posix_getuid() で uid が取得できる場合はユーザごとにフォルダを分けるようにしておく。
      // ※ Windows 環境または –disable-posix オプション付きでビルドした場合は取得できないことに留意
       if(function_exists(“posix_getuid”)){
           if(@mkdir($options[‘cacheDir’], 0777)){ // PHP4 互換のため recursive なし
               @chmod($options[‘cacheDir’], 0777);
           }
           $options[‘cacheDir’].=”cache_uid_”.posix_getuid().DIRECTORY_SEPARATOR;
       }
       @mkdir($options[‘cacheDir’]); // PHP4 互換のため recursive はなし
       return new Cache_Lite($options);
    }
}

 

// 用例
/* —
  // デフォルトを与えるかは任意
CacheLiteFactory::setDefaultOptions(array(
    // ‘cacheDir’=>’/path/to/tmp/’,
    ‘lifeTime’ => 24 * 60 * 60, // set 1 day by default
    ‘hashedDirectoryLevel’ => 1,
    ‘automaticCleaningFactor’=>100,
  ));
— */
 
$cache = CacheLiteFactory::factory();
$value = $cache->get(“foo”);
if($value === false){
  $value = date(‘Y/m/d H:i:s’);
  $cache->save($value);
}
echo $value . “\n”;

PHP4 と PHP5 の Sigleton Pattern

PHP4 での Singleton

error_reporting(E_ALL);
class Foo {
    function &getInstance(){
        static $_singleton;
        if(empty($_singleton)){
            $_singleton = new Foo();
        }
        return $_singleton;
    }
    var $_n;
    function add(){
        echo (++$_n).”\n”;
    }
}
$o1 =& Foo::getInstance();
$o1->add(); // 1
$o2 =& Foo::getInstance();
$o1->add(); // 2
$o2->add(); // 3
$o1->add(); // 4

 & と static の使い方が肝です。
static キーワードは、指定したローカル変数のライフサイクルを延長するためのキーワードです(注1)。
 メソッド定義とメソッド呼び出しの両方に & を付けないと、参照でなくコピーになってしまい、個別のインスタンスになるために結果が 1,2,1,3 になってしまいます。
 
PHP5 の場合も上のコードを変更なしで Singleton Pattern として動作させることが可能です。
さらに、メソッドの戻り値が標準で参照になるため、& を外しても singleton を実現することができ、クライアントコードが誤用する可能性がなくなったといえます(注2)。
 
このように PHP4 ではクライアントに特殊なメソッド呼び出し方をしないと意図しない結果になるため、Singleton な動作が必要な箇所については極力非公開にして内部利用にとどめるほうが賢明かも、です。
 
注1:
static キーワードには、スコープ(可用範囲)を広げる効果はありません。
ローカル変数がフィールドになるわけではないため、異なるメソッドで同じ名前の変数を指定しても、別個の変数になります。
注2:
ただし、クライアントコードで

$o1 = clone Foo::getInstance();

のように clone を使って意図的にコピーを行った場合、PHP4 で & を付けなかったときと同様の結果になります。
この場合はクライアント側で結果が予測できるため、問題にはならないでしょう。
 
参考:
PHP4 でデザインパターン(Do You PHP?)