Home

Shin x blog

UStreamはPHPで動いてる

この記事の所要時間: 10

近頃Twitter界隈でアツいのがUStreamです。これは動画配信サービスなのですが、なかなか良くできていてWebカメラさえあれば誰でも簡単にライブ動画を配信することができます。

配信するにせよライブを見るにせよ話を聞いただけでは「何が面白いの?」なサービスなのですが、これがやってみると面白いんですね。Webカメラを使っている人がほとんどなのでPC前で作業している絵面ばかりなのですが、一緒に作業している感があって良いです。SOHOやフリーランスで仕事している人にはさぼり防止に良いかもしれません;-)

さてそんなUStreamですが、サーバ側はPHPで動いています。HTTPレスポンスヘッダを見るとPHP5.2.0を使っているようです。

Server: Apache
X-Powered-By: PHP/5.2.0

動画配信やコメント一覧表示など各所でPHPが使われている箇所が見られます。

Twitterをはじめ昨今のWeb2.0的サービスではRoRが注目されることが多いですが、PHPもまだまだ頑張っていますね。

開発・運用のノウハウが公開されるのを心待ちにしたいところです。

Twitter 自分宛のメッセージを携帯で見る

この記事の所要時間: 046

今更ですが「Twitter で暇つぶし」を使って自分宛のメッセージを携帯で見る方法です。

Twitter で暇つぶし」でTwitterのユーザIDを指定すると、そのユーザが投稿したメッセージとそのユーザ宛のメッセージが表示されます。そこで自分のユーザIDを指定したURLを携帯に保存しておけば出先でも携帯で自分宛のメッセージを確認することができます。

例えばユーザIDがshin1x1なら下のように指定します。

http://twitter.1×1.jp/m/viewer/shin1x1

あとはこれを携帯でブックマークしておけばokです。

ちなみに「Twitter で暇つぶし」で見ることができるのは「Twitter検索」と同じですので、自分がaddしていない人からのメッセージも携帯で見ることができます。(逆にProtectしている人のメッセージは見れません。)

HTTPリスエストをそのまま返すPHPサーバ

  • 2007-07-21 (土)
  • PHP
この記事の所要時間: 124

PHPで簡易サーバを書いてみました。ブラウザから来たHTTPリクエストをそのまま返すだけのものです。実用性はあまり無いですが、ブラウザがどんなリクエストを出しているか確認するにはちょっとだけ便利かもしれません。;-)

socketsライブラリを有効にする

PHPでsocketsライブラリが有効になっている必要があります。Win環境ならPHPパッケージにDLLが用意されているので、php.ini等で下のように有効にすればokです。

# ; を外す
extension=php_sockets.dll

サーバを起動する

下のソースをローカルに保存して、phpコマンドで実行するとサーバが起動します。デフォルトではlocalhostの9000ポートでlistenします。

<?php
class HttpServer {
  private $socket = null;

  private $method = null;
  private $uri = null;
  private $headers = null;
  private $body = null;

  public function __construct($port = 9000) {
    $this->socket = socket_create_listen($port);
    if ($this->socket === false) {
      $this->error(); 
    }
  }

  public function execute() {
    while (true) {
      $client = socket_accept($this->socket);
      if ($client === false) {
        $this->error();
      }

      $request = $this->read($client);

      $header = "HTTP/1.x 200 OK\r\n";
      $header .= "Content-Type: text/html\r\n";
      $header .= "\r\n\r\n";
      socket_write($client, $header, strlen($header));

      $request = nl2br($request);
      socket_write($client, $request, strlen($request));
      socket_close($client);
    }
  }

  private function read($client) {
    $this->body = "";
    $req = "";
    while (true) {
      $buff = socket_read($client, 2048, PHP_BINARY_READ);
      $req .= $buff;

      if ($buff === false) {
        $this->error();
      } else if ($buff === "" || strlen($buff) < 2048) {
        if (preg_match("/\r\n\r\n$/", $req)) {
          $this->parseHeader($req);
        } else {
          $this->body .= $buff;
        }

        if ($this->isComplete()) {
          return $req;
        }
      }
    }
  }

  protected function parseHeader($req) {
		$lines = explode("\r\n", $req);
    $this->setRequestLine(array_shift($lines));

    foreach ($lines as $line) {
      $line = trim($line);
      if (empty($line)) {
        continue;
      }
      $this->setHeader($line);
    }
  }

	protected function setRequestLine($line) {
		list($this->method, $this->uri) = explode(" ", $line);
	}

	protected function setHeader($line) {
		$header = explode(":", $line);
		$this->headers[trim(array_shift($header))] = trim(join( ":", $header ));
	}

  protected function isComplete() {
    if ($this->method == "POST" && !empty($this->headers['Content-Length'])) {
      return strlen($this->body) == $this->headers['Content-Length'];
    }

    return true;
  }

  private function error() {
    $message = sprintf("error:%d %s", socket_last_error()
                                    , socket_strerror(socket_last_error()));
    die($message);
  }

  public function __destruct() {
    if (is_resource($this->socket)) {
      socket_close($this->socket);
    }
  }
}

$server = new HttpServer();
$server->execute();
?>

ブラウザでサーバへアクセスする

あとはブラウザで[http://localhost:9000/]にアクセスすればリクエストの内容がそのまま表示されます。適当なフォームをHTMLで作ってPOSTするとその内容もそのまま表示されます。

次はHTTPサーバ?

新大阪<->品川の新幹線で時間があったのでちゃちゃっと書いてみました。本当はもうちょっとちゃんとしたHTTPサーバもどきにしようと考えていたのですが、これはこれで面白かったのでアップしときます。。。

あとコードを書く際はZendFramework付属のhttp_serverを参考にしました。ZFはコードが読みやすいので結構好印象です。PHP5フレームワーク・ライブラリとして今後が楽しみですね。

携帯サイトに楽天ダイナミックアドを設置する

この記事の所要時間: 14

先日リリースされた楽天ダイナミックアドですが、携帯サイト用の商品表示パーツがありません。

そこで楽天ウェブサービスを使って携帯サイト用の商品表示をPHP5で作ってみました。

今回はServices_Rakutenを使わず、SimpleXMLで実装しています。

<?php
$params = array();
$params&#91;'developerId'&#93; = DEV_ID;
$params&#91;'affiliateId'&#93; = AFF_ID;
$params&#91;'url'&#93; = 'http://example.com' . $_SERVER&#91;'REQUEST_URI'&#93;;
$params&#91;'carrier'&#93; = '1'; // 携帯

$query = "";
foreach ($params as $k => $v) {
if ($query) $query .= '&';
  $query .= $k . '=' . urlencode($v);
}
$url = sprintf("http://dynamic.rakuten.co.jp/rcm/1.0/i/rest?%s", $query);
$xml = simplexml_load_file($url);
?>

あとは$xmlの値を使ってHTMLを組み立てればokです。(取得した商品情報はUTF-8なので、必要があればSJISに変換します。)

試しに昨日からTwitterで暇つぶしに設置しています。携帯サイトは画面サイズが小さいので商品を一つだけ表示しています。また表示する商品も商品名が一番短いものを選択しています。

効果の程は分かりませんが、APIを使ってコンテンツにマッチした商品情報を簡単に取得できるのは中々面白いですね。

一つ気になったのはクローラーのアクセス頻度です。クローラーが来ることについては楽天ウェブサービスにも明記されていますので特に問題ありません。

ただ短時間に一気にアクセスしに来る事があるようで、今日の昼頃は100秒間に2000件近くのアクセスが来たこともありました。今後改良されていくかもしれませんが、動的サイトに設置される方は念のためご注意を。

楽天API PEAR::Services_Rakuten-0.2.0リリース

  • 2007-07-17 (火)
  • PHP
この記事の所要時間: 34

楽天ウェブサービスをPHPで利用するPEARライブラリ「PEAR::Services_Rakuten-0.2.0」をリリースしました。今回は未対応だったAPIの対応のほか、リファクタリングを行い、今後拡張しやすい形にソースコードを変更しました。

主な変更点は以下です。

  • 2007/07/17現在の全APIに対応(楽天ダイナミックアドAPI含む)
  • ソースコードリファクタリング
  • リファクタリングに伴い利用方法を変更(後述)

インストール・アンインストール

インストール方法は以下です。

$ pear install --alldeps /blog/download/Services_Rakuten-0.2.0.tgz

インストール時に「Failed to download pear/XML_Serializer within preferred state “stable”」といったエラーが発生した場合は以下のコマンドでXML_Serializerをインストール後、Services_Rakutenをインストールして下さい。

$ pear install --alldeps XML_Serializer-beta

アンインストール方法は以下です。

$ pear uninstall __uri/Services_Rakuten

Services_Rakutenを使う[0.2.0用]

0.2.0では以下のコードのようにServices_Rakuten::factory()にて、利用するAPIコード・デベロッパーID・アフィリエイトIDを指定します。このメソッドは各APIを実行するインスタンス(apiインスタンス)を返します。

apiインスタンスのexecuteメソッド実行する楽天ウェブサービスへリクエストが送信されます。executeメソッドは連想配列を引数として受け取るので各種パラメータを設定します。なおexecuteメソッドでは文字コードの変換は行いませんので、keywordなど日本語を渡す場合はUTF-8で指定して下さい。

取得した結果は、apiインスタンスのgetResultDataメソッドにて取得できます。

<楽天市場系API>

<?php
require_once('Services/Rakuten.php');
define('DEV_ID', 'xxxx');
define('AFF_ID', 'xxxx');

// 楽天商品検索
$api = Services_Rakuten::factory('ItemSearch', DEV_ID, AFF_ID);
$api->execute(array('keyword' => '大福'));
var_dump($api->getLastUrl());
var_dump($api->getResultData());

// 楽天商品ジャンル検索
$api = Services_Rakuten::factory('GenreSearch', DEV_ID, AFF_ID);
$api->execute();
var_dump($api->getLastUrl());
var_dump($api->getResultData());

// 楽天商品コード検索
$api = Services_Rakuten::factory('ItemCodeSearch', DEV_ID, AFF_ID);
$api->execute(array('itemCode' => 'book:11907840'));
var_dump($api->getLastUrl());
var_dump($api->getResultData());

// 楽天カタログ検索
$api = Services_Rakuten::factory('CatalogSearch', DEV_ID, AFF_ID);
$api->execute(array('keyword' => 'ワンセグ'));
var_dump($api->getLastUrl());
var_dump($api->getResultData());
?>

<楽天ブックス系API>

<?php
require_once('Services/Rakuten.php');
define('DEV_ID', 'xxxx');
define('AFF_ID', 'xxxx');

// 楽天書籍検索
$api = Services_Rakuten::factory('BookSearch', DEV_ID, AFF_ID);
$api->execute(array('keyword' => 'ブログ'));
var_dump($api->getLastUrl());
var_dump($api->getResultData());

// 楽天CD検索
$api = Services_Rakuten::factory('CDSearch', DEV_ID, AFF_ID);
$api->execute(array('keyword' => '氷室'));
var_dump($api->getLastUrl());
var_dump($api->getResultData());

// 楽天DVD検索
$api = Services_Rakuten::factory('DVDSearch', DEV_ID, AFF_ID);
$api->execute(array('keyword' => '氷室'));
var_dump($api->getLastUrl());
var_dump($api->getResultData());
?>

<楽天トラベル系API>

<?php
require_once('Services/Rakuten.php');
define('DEV_ID', 'xxxx');
define('AFF_ID', 'xxxx');

// 楽天トラベル施設検索
$api = Services_Rakuten::factory('SimpleHotelSearch', DEV_ID, AFF_ID);
$api->execute(array('largeClassCode' => 'japan', 'middleClassCode' => 'kanagawa', 'smallClassCode' => 'hakone'));
var_dump($api->getLastUrl());
var_dump($api->getResultData());

// 楽天トラベル施設情報
$api = Services_Rakuten::factory('HotelDetailSearch', DEV_ID, AFF_ID);
$api->execute(array('hotelNo' => '65638'));
var_dump($api->getLastUrl());
var_dump($api->getResultData());

// 楽天トラベル空室検索
$api = Services_Rakuten::factory('VacantHotelSearch', DEV_ID, AFF_ID);
$api->execute(array('largeClassCode' => 'japan', 'middleClassCode' => 'kanagawa', 'smallClassCode' => 'hakone'
                  , 'checkinDate' => '2007-07-18', 'checkoutDate' => '2007-07-20'));
var_dump($api->getLastUrl());
var_dump($api->getResultData());

// 楽天トラベル地区コード
$api = Services_Rakuten::factory('GetAreaClass', DEV_ID, AFF_ID);
$api->execute();
var_dump($api->getLastUrl());
var_dump($api->getResultData());

// 楽天トラベルキーワード検索
$api = Services_Rakuten::factory('KeywordHotelSearch', DEV_ID, AFF_ID);
$api->execute(array('keyword' => '伊豆'));
var_dump($api->getLastUrl());
var_dump($api->getResultData());
?>

<その他API>

<?php
require_once('Services/Rakuten.php');
define('DEV_ID', 'xxxx');
define('AFF_ID', 'xxxx');

// 楽天ダイナミックアド
$api = Services_Rakuten::factory('DynamicAd', DEV_ID, AFF_ID);
$api->execute(array('url' => '/blog/'));
var_dump($api->getResultStatus());
var_dump($api->getResultStatusMessage());
var_dump($api->getLastUrl());
var_dump($api->getResultData());
?>

Services_Rakutenを使う[0.1.0用]

従来からあったdoItemSearch/doGenreSearch/doItemCodeSearch/doBookSearchメソッドはdeprecated(非推奨)となっています。0.2.0では動作しますが、今後のリリースでは廃止される可能性があります。

今後開発される際はServices_Rakuten::factoryによる利用方法で実装して下さい。

リファクタリングの効果

ちょうどリリース作業を行っている最中に楽天ダイナミックアドAPIが発表されました。「げげっ」と思いつつも、どうせなら今回のリリースに含めたいと思い作業を開始したところ、API拡張用にリファクタリングを行っていたため短時間で機能追加を行うことができました。

早速リファクタリングの効果が発揮され、我ながら嬉しかったです;-)。

PHPベンチマーク CakePHPでモデルを使用しない

この記事の所要時間: 135

via: PHPベンチマーク: Zend Framework vs Symfony vs CakePHP vs CodeIgniter vs PHP on TRAX – 徒然なるままにBlog

エントリでPHPフレームワークのベンチが比較されており、とても興味深いものです。

ベンチ対象のソースが公開されているので、ざっと見てみると気になるところが一点ありました。

各フレームワークで行った処理はコントローラを呼び出しビューに遷移させて
“Hello World!”を表示させるだけのかなりシンプルな内容です。
DBへの接続やモデルの作成は行わず、自動レイアウト機能があるものはオフにするか全て削除しています。

PHPベンチマーク: Zend Framework vs Symfony vs CakePHP vs CodeIgniter vs PHP on TRAX – 徒然なるままにBlog

モデル作成を行わないはずなのにCakePHPだけモデルが生成されるようになっていました。

そこでモデル生成あり・なしでどの程度差が出るかを計ってみます。計測方法は[ab -c 100 -n 100]を10セットしてその平均を出しています。

モデル生成ありのソースは元エントリのまま。モデル生成なしは元エントリのhello_controller.phpを下のように変更しています。

<?php
class HelloNoModelController extends AppController{
  var $layout=null;
  var $autoLayout=false;
  var $uses = array(); // モデルを使用しない

  function hello()
  {
  }
}
?>

結果は以下のとおり。

モデル生成あり 5.145 Request/sec
モデル生成なし 6.381 Request/sec

実行環境が元エントリとは異なるので絶対的な数値にはあまり意味が無いですが、両者を比べるとモデル生成なしの方が20%ほど速いようです。

できれば元エントリの環境でモデル生成なし版によるベンチが見てみたいところです;-)。

Web API実践リファレンスブック

この記事の所要時間: 026

本屋で何気に「Web API実践リファレンスブック」をパラパラと見ていると楽天ウェブサービスのところで拙作Services_Rakutenが紹介されていました!

全然知りませんでした。こういうのって連絡とか来ないんですね。。。

# まあServices_Rakutenは放置していたのでアレですが;-)。

使っている人がいるかどうか分かりませんが、楽天ウェブサービスのAPIもかなり増えているので近日中に更新したいと思います。

CakePHP 1.1.15.5144以降はHtmlHelper#tagValueに注意

この記事の所要時間: 13

確認画面等で入力値を表示するのにHtmlHelper#tagValueを使っています。

これは内部でHTMLタグをエスケープしてくれるので重宝していたのですが、1.1.15.5144から、このメソッドにエスケープするか否かを表すフラグが追加されました。

[cake/libs/view/helpers/html.php]

<?php

   function tagValue($fieldName, $escape = false) {

?>

困った事にデフォルトではfalseになっているので、従来の[$html->tagValue(‘Foo/bar’);]のような使い方をしているとHTMLタグがそのまま出力されてしまいます。1.1.15.5144以降(昨日リリースされた1.1.16.5421も含む)でHTMLタグをエスケープするには以下のように第2引数にtrueを指定しなければなりません。

// tagValueでHTMLタグをエスケープ
<?php echo $html->tagValue('Foo/bar', true);
// もしくはh()で囲む
<?php echo h($html->tagValue('Foo/bar')) ?>;

1.1.14.4797以前から1.1.15.5144以降へバージョンアップする際はくれぐれもご用心を。

ちなみに1.2.0.5147ではtagValueがdeprecatedになっており、代わりにvalueメソッドを使うようになるようです。

CentOS yumでGPGキーエラー

  • 2007-07-03 (火)
  • unix
この記事の所要時間: 036

先日某所で借りたサーバがCentOS3だったのですが、yumでRPMを更新しようとすると以下のエラーが発生しました。

Error: You may also check that you have the correct GPG keys installed centos

GPGキーを入れろとの事なので以下のコマンドでインストールして解決。

# rpm --import http://mirror.centos.org/centos/RPM-GPG-KEY-CentOS-3

サーバ屋構築のサーバなんだから、あらかじめGPGキー入れといてくれても良いかと。

CakePHP 比較演算子インジェクションに注意

この記事の所要時間: 239

@deprecated

この情報はCakePHP1.2betaまでのものです。1.2RC1についてはこの方法は有効ではありません。詳しくはCakePHP 1.2RC1からは比較演算子をキーに書くをどうぞ。

CakePHPのモデルで検索条件を指定する場合は比較演算子に注意が必要です。

検索条件では↓な感じで条件値の他にSQLの比較演算子を入れることができます。

<?php
class UserController extends AppController {
  funtion index($id) {
     $id = is_numeric($id) ? $id : 0;
     // $id より大きなidを持つレコードを取得
     $list = $this->findAll(array('id' => '> ' . $id)); 
     $this->set('list', $list);
  }
}
?>

これを見ただけで分かる人はピンと来ますね。そう外部から送られてきた値に比較演算子が含まれていてもそれがそのままSQL文として動作してしまうわけです。

これがマズそうな場面は色々あるでしょうけど、決定的なのがログイン画面です。例としてメールアドレスとパスワードで認証するアクションを見てみます。

<?php
    function login() {
      if (!empty($this->data['User'])) {
        $conds = array();
        $conds['email'] = $this->data['User']['email'];
        $conds['password'] = $this->data['User']['password'];
        $user = $this->User->find($conds);
        if (!empty($user)) {
          // ログイン成功
        }
      }
    }
?>

まあありがちな処理だと思いますが、このアクションに[email=!=hoge@example.com&password=!=a]をPOSTすると↓のようなSQL文が発行されてしまいます。(WHERE句以降)

WHERE "email" != 'hoge@example.com' AND "password" != 'a' LIMIT 1

これだとusers.emailにhoge@expample.com、users.passwordにaのどちらもが存在しなければログインする事が可能となります。つまりユーザ情報に登録されていなさそうな値を使えば誰でもログインが可能となります。

他にも登録されているメールアドレスを入力して、パスワードだけ[!=]を使うという手もあります。これならユーザのメールアドレスさえ分かればどのユーザとしてもログインできてしまいます。

この問題の対策は2つ+1つあります。

1. findBy/findAllByを使う

findBy/findAllByを使うとこの現象は発生しません。先程のloginアクションを書き換えてみます。

<?php
//        $user = $this->User->find($conds);
      $user = $this->User->findByEmailAndPassword($conds['email']
                                                          , $conds['password']);

これなら入力された比較演算子も文字列として処理されます。

WHERE "User"."email" = '!=hoge@example.com' AND "User"."password" = '!=a' LIMIT 1

2. 入力値の前に’= ‘を付ける

findやfindAllで条件を連想配列で渡したい場合は各値の前に’= ‘を付けます。実はfindBy/findAllByでは内部的にこの処理を行ってます。つまりそれを自分でやってしまうというわけです。

ここでのポイントは’=’の後ろにスペースを入れるという事です。これが無いと一部の演算子が有効となってしまいます。

<?php
        $conds = array();
        $conds&#91;'email'&#93; = '= ' . $this->data['User']['email'];
        $conds['password'] = '= ' . $this->data['User']['password'];
        $user = $this->User->find($conds);
?>

3. 入力値を厳密にチェックする

これももちろん大事です。検索に使用する値が数値のみや日付形式といった制限がある場合はそちらのチェックでこの問題を防ぐことができます。

ただ、実は比較演算子の他に[like/ilike/in/or/not/in/between/regexp/similar to]といったキーワードも同様の問題をはらんでいます。これらは文字列の中では通常の単語として出てくる可能性もある(「Like a child」とか)ので判別が難しいです。

ちなみにこの問題が発生するのは比較演算子・キーワードが文字列の先頭になっている場合のみです。それ以外の箇所にある場合は問題ありません。

find/findAllで検索条件に連想配列を使う場合は注意を

これは忘れるとインパクトがあるのでfind/findByfindBy/findAllByで検索する場合はくれぐれもご注意を。

@see: CakePHPの何か「CakePHPのModelを使う」

Home

検索
フィード
メタ情報

Return to page top