PHP on Ruby (on PHP on Ruby on…)

同僚がphpDocumentorのソースに変なことが書いてあるというので見ると、確かに変だ。長いコメント部分を除くとこうなっている。

#!/bin/sh
if [ -z "$PHP" ]; then
   PHP=`which php`
fi
(exec $PHP -C -q -d output_buffering=1 "$0" "$@") 2>/dev/null
if [ "$?" -ne "0" ]; then
        echo "FAILED:  Bad environment variable \$PHP (set to \"$PHP\")"
        exit 1
fi
exit 0
<?php

ob_end_clean(); // make output from autofind php code disapear

$test = @include "phpDocumentor/find_phpdoc.php";
if ($test == 6) {
    // find_phpdoc.php returns 6
    include "phpDocumentor/phpdoc.inc";
} else {
    echo "Error: phpDocumentor not found" . PHP_EOL;
}
?>

シェルスクリプトが冒頭にきて、その中から今度はPHPとして実行されるように自分自身を呼び出して、しかもご丁寧に出力バッファリングでシェルのコード部分が出力されないようにしているよ!PHPはHTMLに埋め込まれる言語でもあるので、開始タグ「<?php」と終了タグ「?>」で囲まれていない部分はそのまま出力されるから、これでもちゃんと実行される。

それにしても、この手が使えるなら、シェルスクリプト以外の言語だって動かせるんじゃなかろうか。

さっそく実験。

#!/usr/bin/ruby

puts "This is Ruby!"
system("php -d output_buffering=1 -q #{__FILE__}")
exit
__END__
<?php
ob_end_clean();

print("This is PHP!\n");
exit;
?>

実行結果:

This is Ruby!
This is PHP!

なんと!

この手を使えば、PHP on Ruby on Railsだって夢じゃない!PHP4からの移行が大変?冗談でしょ。これからはPHPはあらゆるスクリプト言語の上(下)で動く時代ですよ。

不自然言語処理にはPHPをどうぞ

自然言語処理はPythonがいちばんという記事を読んで思い出したのだが、自分でもWordnetのPHP版フロントエンドをさくっと(Ruby版をコピーして)作っていた。ほんとまるコピーなのでほったらかしにしていたのだが、イテレータのところの醜さに悶絶してもいいなら責任は取らないが読むのは許可するので公開しておく。都市の空気は自由だ。

<?php
class Wordnet{

  public $db;

  function __construct($dsn){
    $this->db =& $this->_connect($dsn);
  }

  function __destruct(){
  }

  public function singleton($dsn){
    static $obj = null;
    if(is_null($obj)){
      $name = __CLASS__;
      $obj =& new $name($dsn);
    }
    return $obj;
  }

  private function _connect($dsn){
    static $dbh = null;
    if(is_null($dbh)){
      try {
        $dbh = new PDO($dsn);
      } catch(Exception $e) {
        echo $e->getMessage();
      }
    }
    return $dbh;
  }

  private function _exception(&$e){
    echo $e->getMessage();
  }


  private function _select($sql, $param=array()){
    $statement = $this->db->prepare($sql);
    $statement->setFetchMode(PDO::FETCH_ASSOC);
    if($statement->execute($param)){
      return $statement->fetchAll();
    }else{
      $info = $statement->errorInfo();
      foreach($info as $k => $v){
        $message .= $k . ":" . $v . "\n";
      }
      throw new Exception($message);
    }
  }

  private function _get($sql, $param){
    try {
      $array = $this->_select($sql, $param);
    }catch(Exception $e){
      $this->_exception($e);
      return null;
    }
    return $array;
  }

  public function get_words($lemma){
    $sql = "SELECT * FROM word WHERE lemma=?";
    $array = $this->_get($sql, array($lemma));
    return $array;
  }

  public function get_word($wordid){
    $sql = "SELECT * FROM word WHERE wordid=?";
    $array = $this->_get($sql, array($wordid));
    return $array;
  }

  public function get_senses($wordid){
    $sql = "SELECT * FROM sense WHERE wordid=?";
    $array = $this->_get($sql, array($wordid));
    return $array;
  }

  public function get_sense($synset, $lang='jpn'){
    $sql = "SELECT * FROM sense WHERE synset=? AND lang=?";
    $array = $this->_get($sql, array($synset, $lang));
    return $array;
  }

  public function get_synset($synset){
    $sql = "SELECT * FROM synset WHERE synset=?";
    $array = $this->_get($sql, array($synset));
    return $array;
  }

  public function get_syn_links($synset1, $link){
    $sql = "SELECT * FROM synlink WHERE synset1=? AND link=?";
    $array = $this->_get($sql, array($synset1, $link));
    return $array;
  }

  public function get_sys_links_recursive($senses, $link, $lang='jpn', $depth=0){
    if(is_array($senses)){
      foreach($senses as $sense){
        $syn_links = $this->get_syn_links($sense['synset'], $link);
        if(count($syn_links) > 0){
          $get_word = $this->get_word($sense['wordid']);
          $synset = $this->get_synset($sense['synset']);
          $space = str_repeat(' ', $depth);
          print($space . $get_word[0]['lemma'] . ' ' . $synset[0]['name'] . "\n");
          $tmp_links = array();
          foreach($syn_links as $k => $syn_link){
            $tmp_sense = $this->get_sense($syn_link['synset2'], $lang);
            if($tmp_sense != ''){
              $tmp_links[] = $tmp_sense[0];
            }
          }
          $this->get_sys_links_recursive($tmp_links, $link, $lang, $depth + 1);
        }
      }
    }
  }

  public function main($word, $link, $lang='jpn'){
    if($words = $this->get_words($word)){
      $senses = $this->get_senses($words[0]['wordid']);
      $this->get_sys_links_recursive($senses, $link, $lang);
    }else{
      return "nothing found";
    }
  }

  public function usage(){

      $usage =  >>>EOS
usage: wordnet.php word link [lang]
  word  word to investigate

  link
  syns - Synonyms
  hype - Hypernyms
  inst - Instances
  hypo - Hyponym
  hasi - Has Instances
  mero - Meronyms
  mmem - Meronyms --- Member
  msub - Meronyms --- Substance
  mprt - Meronyms --- Part
  holo - Holonyms
  hmem - Holonyms --- Member
  hsub - Holonyms --- Substance
  hprt - Holonyms -- Part
  attr - Attributes
  sim - Similar to
  entag - Entails
  causg - Causes
  dmncg - Domain --- Category
  dmnug - Domain --- usage
  dmnrg - Domain --- Region
  dmtcg - In Domain --- Category
  dmtug - In Domain --- usage
  dmtrg - In Domain --- Region
  antsg - Antonyms

  lang (default: jpn)
  jpn - Japanese
  eng - English

EOS;
    print($usage);
  }

}
$dsn = 'sqlite:./wnjpn-0.9.db';
$obj = Wordnet::singleton($dsn);
$word = $_SERVER['argv'][1];
$link = $_SERVER['argv'][2];
$lang = $_SERVER['argv'][3] ? $_SERVER['argv'][3] : 'jpn';
if($word == '' || $link == ''){
  $obj->usage();
  exit;
}

$obj->main($word, $link, $lang);

?>

ではテスト。

$ ruby wordnet.rb  'うんこ' hype
うんこ dung
  大便 stool
うんこ turd
  大便 stool
$ php -q wordnet.php 'うんこ' hype
うんこ dung
 大便 stool
うんこ turd
 大便 stool

そもそもlinkとか意味がわかってないのでこれ以上テストする気がないのだが、動いていそうな雰囲気ではある。うんこしかテストしていないので、不自然な言語処理にぜひともおすすめ。

テストしてない上にローカルで使ってたのでセキュリティとか一切考慮してないから、これを使って何をしようとも愚挙としかいいようがないので念のため。

PHP-5.3.0beta1でtimezone関連のWarning

PHP-5.3.0beta1を動かすと、strtotimeを使った際にWarningが出る。

$ php -r 'print(strtotime("2009/01/01 00:00:00"));'
PHP Warning:  strtotime(): It is not safe to rely on the system's timezone settings. 
You are *required* to use the date.timezone setting or the date_default_timezone_set()
function. 
In case you used any of those methods and you are still getting this warning, 
you most likely misspelled the timezone identifier. We selected 'Asia/Tokyo' 
for 'JST/9.0/no DST' instead in Command line code on line 1
PHP Stack trace:
PHP   1. {main}() Command line code:0
PHP   2. strtotime() Command line code:1

いわれたとおりに

$ php -r 'date_default_timezone_set("Asia/Tokyo"); 
print(strtotime("2009/01/01 00:00:00"));'

date_default_timezone_set()関数を使うと何もいわれない。また、php.iniを

[Date]
; Defines the default timezone used by the date functions
date.timezone = 'Asia/Tokyo'

のように変更すると何もいわれなくなる。これが正しいかどうかは不明。ググレカスにお伺いを立てると結構ヒットする。知らなかったなあ。

Peclのspidermonkey

jsが入っているのにJavascriptのインタプリタ以外に使い道がないなあ、と思っていたらPeclにspidermonkeyというのが追加されていた。検索するともう記事が見つかったりして、楽しそうなのでさっそくインストール。PHP5.3beta0以上が必要なので、テスト環境のPHPもついでに更新。php.iniのextension_dirを

extension_dir=/usr/lib/php/exntesions/no-debug-non-zts-20090115

のように更新しておかないといけない。

で、pecl install spidermonkey-alphaを実行したら見事にこける。ヘッダファイルjsapi.hがないとのこと。でもそのファイルは/usr/include/直下にある。で、手動でspidermonkey拡張モジュールをダウンロードしてconfigureの中身を見ると、

for i in $PHP_SPIDERMONKEY /usr/local /usr; do
    for j in js mozjs; do
      test -f $i/include/$j/jsapi.h && SPIDERMONKEY_BASEDIR=$i 
&& SPIDERMONKEY_INCDIR=$i/include/$j && SPIDERMONKEY_LIBNAME=$j 
&& break
    done
    test -f $i/include/$j/jsapi.h && break
  done
  if test -z "$SPIDERMONKEY_INCDIR"; then
    { { echo "$as_me:$LINENO: error: jsapi.h not found. Please reinstall libjs." >&5
echo "$as_me: error: jsapi.h not found. Please reinstall libjs." >&2;}
   { (exit 1); exit 1; }; }
  fi

こんな箇所があり、spidermonkeyのヘッダファイルは/usr/includeか/usr/local/include直下ではなくその下のjsかmozjsディレクトリでないといけないらしい。なんなんだ。以前spidermonkeyをインストールしたときの手順に問題があるんだろうか。

まあ、ヘッダファイルはみんなjs*.hという名前だったので、さくっと

# mkdir /usr/include/js && cd /usr/include/js
# for i in `ls ../js*.h`; do ln -s $i `echo $i | sed "s/..\///"`; done
# pecl install spidermonkey-alpha

適当なことをやって、ようやくインストール完了。ちなみにmozjsにするとやっぱりpeclからのインストールは出来なかった。なぜだ?

memcached(repcached)のデータをバックアップ

memcachedの評価中に、対障害性を高める方法についてあれこれ考えていたのだが、あまりいい案が浮かばなった。

とりあえずストレージの冗長化であればrepcachedでなんとかなる。でも、memcachedのストレージがいっぱいになったらアクセスの古いデータから順に捨てられるので、予想を超える事態になったときにそれを検知してなんとかDBにでも保存しておく方法はないものかと考えたのだが、なかなかスマートな解決方法がない。

たとえば、セッション情報をmemcachedに保存しているとする。期限が設定されていたら、少なくともその間はデータを保持したい。でも、データ量が予想を上回ると上記のロジックで古いセッションが破棄されてしまう。よほどケチった予想をしていない限りは実害はほとんどないのかもしれないが、保障されているセッションの期限が守れない可能性があるのは変わりない。

そこで、現在memcachedのストレージ上に保存されているデータを定期的にDBにバックアップできないかと考えた。でも、よく考えたら、セッションのユニークIDをキーにしてデータを保存している場合、現在どんなIDが保存されているのかを知る方法がどこにもない。RDBMSだと取得先テーブル名はたいていわかるので、条件を書けばいくらでもデータを取得できるが、そもそも前提となる条件がユニークIDであるセッションのような情報だと難しい。

RDBMS風にいえばこんな感じ。
  table      |  data                                        |
----------+----------------------------------+
  yagi       | I still consider myself rather likable |
----------+----------------------------------+
  soey      | Size don't matter                     |
----------+----------------------------------+
  chobee   | Baseball games are like snowflakes |
----------+----------------------------------+

そこで、memcached上にシーケンステーブルのようなものを用意してみた。セッションの期限は24時間だから


$myname = 'yagi';
$mydata = 'I still consider myself rather likable.';

$today = date("Y-m-d", time());
$new_id = $today . '_' . $memcached->increment($today);

$memcache->set($new_id, $myname);
$memcache->set($myname, $mydata);

これで、RDBMS風にいえば

sequence_name | last_value | increment_by |
----------------+-----------+--------------+
2009-02-06       |            1  |                 1 |

id                  | myname |
--------------+---------+
2009-02-06_1 |       yagi |

myname | mydata                                      |
---------+----------------------------------+
   yagi    | I still consider myself rather likable |

みたいなテーブルが出来たことになる。そこで、


$yesterday = date("Y-m-d", mktime(0, 0, 0, date("m"), date("d") - 1, date("Y")));
$max = $memcache->get($yesterday);
$i = 0;
$data = array();
while($max >= $i){
    $save = $memcache->get($yesterday . '_' . $i);
    if($save){//件数が多すぎたら配列に入れられないけどまあいい
        $data[$save] = $memcache->get($save);
    }
    $i++;
}

という感じでバックアップは取れる。特にセッション情報があまり変わらないアフィリエイトのID保存のような目的に使われる場合は、成果発生時のチェックにmemcachedから読み込んだデータとDBのデータを使えばまあ確実になる。

でも、動くことは動くが1件につき2つのデータが必要になり、効率は悪い。

もっとうまい方法があれば教えてほしいと思うのであった。

なぜPHPが勝ったのか、についての反論

このサイトに来てくれている人たちの代表的なモデルは、平日の業務時間中にアクセスしてくる20代中頃から30代前半くらいの男性らしい。たぶん何かGoogleで検索しているときにうっかりひっかけてしまうのだろう。

非常に反応の薄い「なぜPHPが勝ったのか」について、海外ではそれなりの反響があったようだ(「ときどきの雑記帖」さんは面白い情報が多くてよく読むんだけどトップページの2009年1月分のリンクが間違っているのでなんとかお知らせしたいのだが画像が表示されないのでメールアドレスがわからない)。

ただ、反論の内容は、もともとこれは経営者の立場から述べられたものなのだから、誰もが出来る言語なんてやってもそれで自分がありふれた存在になっちゃうよといわれても、当たり前のことなのでもうちょっと冷静になってもいいんじゃないかと思った。経営者対被雇用者という煽りの方が面白そうだし。

なぜPHPが勝ったのか

何日か前に「なぜPHPが勝ったのか」と題するエントリを読んだ。勝ったというのが、PHPがここまでポピュラーになり、ポピュラーであり続けているということを指すなら、まさにそうだろう。著者はスタートアップ企業のCTOで、その立場からなぜ自分がPHPを開発プラットフォームとして選択するのかを説明している。言語としてではなく、プラットフォームとしてというのが味噌だ。

このエントリをざっとまとめると:PHPの言語仕様のまずさこそが勝利の原因だ。

前提として、PHPにはそれなりの実績がある大量のコードが既にあり、そのユーザ数も多く、サポートするコミュニティも大きい。その上で、こんな理由を挙げている。

以下、要約。

PHPが勝った理由その1:イテレーションの早さ

PHPなら最初からApacheモジュールとして動作して、再読み込みしても軽快に動作するし、ウェブサーバの再起動なしに再読み込みさえすれば変更内容を確認できる。

PHPが勝った理由その2:貧弱なマッピングとコードとプレゼンテーションの混在

URLをファイルにマッピング、コードとプレゼンテーションをごちゃまぜにする。PHPのアプリケーションではこんなことも許される。普通、頭のいい開発者ならバックエンドの言語が何かURL部分に埋め込んだりはしない。edit.phpが編集画面でregister.phpが会員登録画面だ、なんて間抜けのすることだ。

でも、このやり方だと、ある機能の改修が必要になったとき、どこのコードをいじればいいのか考える必要は全くない。会員登録画面でエラーが出るようになったと連絡が入れば、URLマッピング用の正規表現を一生懸命読んでどのファイルがこの機能を担当しているか探したりせず、あわてずregister.phpを開いて、そこから修正箇所を探せばいい。ボタンのサイズを変えて登録ボタンをもっと目立つようにしたいなら、やっぱりregister.phpを開けばそこにはHTMLとPHPのコードがごちゃごちゃ混在しているはずだから、HTML部分をいじっておしまいだ。ある規模までのアプリケーションなら実はこれでも動くっちゃ動くし、十分っちゃ十分だ。HTMLならわかるけれどもプログラミングには不慣れな人だって、どこかからコピーしてきたコードさえあれば、あとはどこに貼りつけるかの問題にしかならない。

PHPが勝った理由3:ぐちゃぐちゃだが膨大な標準ライブラリ

今日ではプログラミングというのは名人芸のアルゴリズム作成というよりは、ほとんどが半可通のプログラマによるたんなるデータ処理であって、データを上手に処理できる言語にはたくさんの標準ライブラリが必要だ。PHPには、データベースドライバや正規表現、HTTP通信など、とりあえずなんでも一通りそろっていて、さらにプログラミング言語としては悪いことに、まともなパッケージ配布システムやモジュール化も進まずに、単一のインタプリタにすべてが同じように放り込まれている。システム管理者、セキュリティコンサルタント、プログラミング言語の純粋主義者、公開したくないライブラリをバンドルしたいサードパーティ企業をいらつかせるこのやり方は、開発者にはありがたいことで、どこでもたいてい同じような環境になり、コードの再利用も進む。

PHPが勝った理由4:オブジェクト指向プログラミングのサポートが貧弱

叩きどころ満載のPHPだが、特にオブジェクト指向プログラミングのサポートが貧弱であると叩かれることが多い。今日ではかなり改善された一方で、PHPのオブジェクトは長いこと辞書に毛が生えたようなものでしかなかった。

オブジェクト指向の長所の一つにカプセル化がある。しかし、実際のところ、PHPのアプリケーションではオブジェクトはリクエスト毎に作成され、利用され、そして捨てられるものだ。ステートレスに動作しているアプリケーションでオブジェクトの状態がどうこうというのは、実地にはあまり意味をなさない。委譲については、PHPはストレージ部分はMySQLなりmemcachedなり他のプロセスにやってもらうわけだから、外部のプロセスへの簡単なラッパさえあれば事足りてしまう。

ゆえに、これといってオブジェクト指向でないプログラムでも十分ということになり、これまた非プログラマを引き寄せることになる。

そもそもウェブ自体がコードの存在はURLの背後に隠れたものであり、ブラウザからのリクエストをベースにしたステートレスなものなわけだから、PHPのスクリプト自体が一個のオブジェクトみたいなものであり、HTTPが定義するインタフェースを実装したカプセルなのだ。

最終的にPHPが(ウェブプログラミングの世界で)勝利したのは、ウェブの基礎に絡み合ってその写し鏡のようになった言語であったからで、古典的な意味でのエレガントさにそれらを落とし込もうとしたからではない。

もちろん、セキュリティはどうなる!?とか、大規模開発ではこうはいかん!!とか、やっぱPHPは馬鹿用言語だよな、とか、いろいろな異論反論はあるだろう。それらも間違いではないと思う。でも、PHPは非プログラマに最適だとか、言語仕様としてまずいのがその手の人たちに受け入れられた理由だというのは説得力がある。もちろん、PHPでプログラマが仕事をすることは可能だし、すごいものを作る人もいる。もちろん、とか言い出すときは本当はその後にくることが本当に言いたいことなわけだが、ここでもその慣習に従うと、しかしながら、ポピュラーな言語になるには、非プログラマに非プログラマであるままで仕事をさせてしまうという手もあり、それは今のところ結構有効だ、ということだ。もちろん言語設計者としてそれは許し難いという考え方もあるだろうし、ポピュラーになること自体で勝負するのは無意味というのもひとつの知見としてそれはそれで正しいのだが。

高速が売りのYii

新年早々、Yii PHP Frameworkを試している。Ruby on Rails以来、PHPでもウェブアプリケーションに使えるフレームワークは雨後の筍のように登場しているが、Rubyで実現できることをPHPで同じようにやろうとしても根本的に無理があるせいで、いずれもデファクトスタンダードになるにはどこか欠けた状態であり、したがって後発であればあるほど有利な状況だ。

そんな中で、このYiiは高速性を売りにしているところが他とちょっと違っている。いわく、

How is Yii Compared with Other Frameworks?

Like most PHP frameworks, Yii is an MVC framework.

Yii excels over other PHP frameworks in that it is efficient, feature-rich and clearly-documented. Yii is carefully designed from the beginning to fit for serious Web application development. It is neither a byproduct of some project nor a conglomerate of third-party work. It is the result of the authors’ rich experience of Web application development and the investigation and reflection of the most popular Web programming frameworks and applications.

サードパーティーの複合体じゃない、何かのバイプロダクトでもない、これ自体で製品レベルのウェブアプリ開発用フレームワークでございます、というわけだ。

環境設定だが、Yiiで用意されているチェック用スクリプト(requirements/index.php)によれば、PHP側でmemcache、PDO、APC、Mcryptなどの拡張機能が有効になっていることが要求される。この時点でさっきのサードパーティーがどうこうというのが破たんしている気がするが、とりあえずいろいろリビルドしてみる。

準備が完了したらさっそくチュートリアルに従ってテストアプリケーションを作成する。YiiRootはYiiをインストールした先と読み替える。

$ cd /YiiRoot/
$ chmod +x framework/yiic
$ ./framework/yiic webapp ./testdrive

自動生成ツールを使ってみる。存在しないディレクトリを指定しているのでディレクトリを作成するかどうか尋ねられる。Yesを選択すると、ずらずらとメッセージが表示されて何やら出来たらしい。

ブラウザでhttp://example.com/testdrive/index.phpにアクセスすると、確かにそれらしい画面が表示されている。正直な感想としては、うーん、デザインはあまりいけてないな。ナビゲーション用バーにHomeと並んでLoginというメニューがあるのでそちらを叩いてみると、確かにCAPTCHAを備えた何やらログイン画面が表示される。

demo/demoまたはadmin/adminでログインできる。内容に間違いがあるとエラー画面も出力される。

とはいえ、まだデータベースを設定しているわけでもないので、たんなるデモ以上の機能はない。さっそくデータベースの設定をする。が、YAMLで設定を書いてrakeでマイグレーション、みたいなおしゃれな機能はないみたいだ。DBを使う場合はPDOがインストール済みである必要がある。

MySQL上にtestdriveというデータベースを作成したら、

CREATE TABLE User(
    id integer NOT NULL PRIMARY KEY auto_increment,
    username VARCHAR(128) NOT NULL,
    password VARCHAR(128) NOT NULL,
    email VARCHAR(128) NOT NULL
);

という内容のテーブルを作成する。で、YiiRoot/testdrive/protected/config/main.phpの26行目あたりの配列「db」を直してMySQL用のDSNやらユーザ名やらを設定する。mysql_userは実際のMySQLのユーザ名、mysql_passは、まあわかるでしょ。

'db'=>array(
                        'connectionString'=>'mysql:dbname=testdrive',
                        'username' => 'mysql_user',
                        'password' => 'mysql_pass',
                        'emulatePrepare' => true,
                ),

最後の「emulatePrepare」という呪文は、この後で発生するエラーを防ぐもの。というのも、手順通りここから

$ cd YiiRoot/testdrive
$ YiiRoot/framework/yiic shell

でインタラクティブシェルを起動して、

>> model User

と入力して対応するモデルを作成するとチュートリアルには書いてあるが、うちのCentOS 4みたいにMySQLが4系の場合は「exception ‘CDbException’ with message ‘CDbCommand failed to execute the SQL statement: SQLSTATE[HY000]: General error: 2030 This command is not supported in the prepared statement protocol yet’」などなどなどの例外が発生する。これは文字通り利用中のMySQLがまだプリペアードステートメントをサポートしていないのが原因なので、先ほどの「emulatePrepare」をtrueに設定することで回避することができる。

で、ここからがYiiの売りのひとつ、crud(create、read、update、delete)用スクリプトの自動作成。crudというのは、ようするにウェブアプリケーション開発者ならこれまで数千回くらい手がけてきた、データの新規作成、一覧、更新、削除の一連の流れのこと。

やり方は、先ほどのシェルから

>> crud User

と入力するだけ。いくつかのスクリプトが作成される。

作成したモデルにアクセスするには、http://example.com/testdrive/index.php?r=userのようにGETのrで指定した小文字のモデル名のURLを利用する。この時点ではまだSEOコンサルを喜ばせるかっちょいいURIはない。さっそくアクセスすると、「New User」と「Manage User」のリンクが表示された何もないページが出てくる。問答無用でNew Userをクリックして試しにデータを作成してみる。

ユーザ名はRoy Rogers、パスワードも同じ、メールアドレスはRoyRogers@example.comに。まだパスワード入力用フォームは平文で入力した内容がそのまま表示されてしまうようだ。

無事完了したら、先ほどのhttp://example.com/testdrive/index.php?r=userに戻って、アカウントが作成されたことを確認しよう。

ちなみにdemo/demoでログインいていたらManage Userをクリックするとエラーになってしまった。admin/adminだと動作する。アカウント種別があるようだ。

チュートリアルはここまで。

PostgreSQL小ネタ

PostgreSQLのラージオブジェクトについて、妙なことを聞かされて以下のようなことを無条件に信じていたのだが。

  • PHPでリモートサーバ上のPostgreSQLに格納されたラージオブジェクトを取得することはできない
  • なぜなら、pg_lo_exportはローカルのファイルパスを指定できるが、それだとリモートサーバ上にファイルが書きだされるだけだから

どう考えても納得できないので、試してみたらやっぱり間違っていた。もっとも、pg_lo_exportは使わなかったが。以下がサンプル。

<?php

require_once 'DB.php';

$dsn = array(
'hostspec' => '192.168.0.0',//どっかのリモートサーバ
'phptype' => 'pgsql',
'database' => 'test',
'username' => 'postgres',
'port' => '5432',
);

$db = DB::connect($dsn);

$file = './orig.png';
$to_file = './test.png';
$content = '';

//まずインポートしてPostgreSQL上にラージオブジェクトを作成
$db->query("BEGIN");
$pid = pg_lo_import($db->connection, $file);
$db->query("COMMIT");

//取り出し
$db->query("BEGIN");
if($lo = pg_lo_open($db->connection, $pid, 'r') !== false){
        do{
                $data = pg_lo_read($lo);
                if(strlen($data) == 0){
                        break;
                }
                $content .= $data;
        }while(true);
}
$db->query("COMMIT");
$db->disconnect();

//書き出し
$fp = fopen($to_file, 'w');
fputs($fp, $content);
fclose($fp);

?>

どうということもない内容だが、どうしてこれが出来ないと思いこんでいたのだろうか。

PHP-5.2.7、5.2.8のZipArchive::extractTo()

Changelogに書かれていないが、標題のZipArchiveの関数に意図しないディレクトリにまで移動できる脆弱性があったのが修正されている

Zipアーカイブのディレクトリ・トラバーサルというとWikipediaの記事にもある通りUnicodeの問題が有名だが、個人的にはあちこちで名前にRTL制御文字を入れ込んで反転させている男、「ǝunsʇo」を思い出す。