cakephperの日記(CakePHP, Laravel, PHP)


継続的WebセキュリティテストサービスVAddyを始めました!

2010-02-24

フォーム入力画面と確認画面で共通で使えるXformHelper

Cakephp1.2.6

追記(2010/12/16):

コメントでPHP5.3の動作不具合報告を頂いたので対応しました。報告ありがとうございました。現状、PHP5.2.15, PHP5.3.4で確認し、CakePHPは1.3.6, 1.2.9で確認しました。gitの最新版か、githubにあるダウンロードボタンから0.02というバージョンのコードをダウンロードしてご利用ください。


入力項目が多くなると、確認画面を作るのも時間がかかりますよね、面倒ですし。今までは、確認画面で別途 $form->value使うとか、Postの値をそのまま表示するようにしてたのですが、デザインが入力画面とほぼ同じなのに別に実装するのがなんだかなと思ってました。それに嫌気がさして、フォーム入力画面でも確認画面でも同じメソッドで、フォームタグと値の表示を切り替えるXformHelperを開発しました。

機能は色々つけてあります。大体のメソッドはカバーできてるんじゃないかと思いますが、不具合があれば教えてください。


機能一覧

  • その他の入力画面機能
    • passwordフィールドの値は常に空にする(プロパティの変更で回避可能)
    • 日付関係のセパレータを拡張(プロパティにて、日本語の年月日、時分をセパレータに指定可能)
  • その他の確認画面機能
    • 出力はエスケープ、nl2brを自動でかける(プロパティの変更で回避可能)
    • passwordフィールドは、*****の固定文字列を表示(ただしinput()ではなくpassword()メソッドの利用に限り)
    • checkboxなどの複数選択時は、カンマ区切りで表示(プロパティの変更でカンマ以外も指定可能)
    • 日付関係のセパレータを指定可能(2010年2月24日という表示が可能)

前提

入力画面と確認画面の制御方法は、下記の2パターンあり、どちらかを実行ください。

1. コントローラの確認画面のアクションで、$this->params['xformHelperConfirmFlag'] = true;をセット(バリデーション成功後が望ましい)

2. 確認画面のviewファイルの最初に、$xformjp->confirmScreenFlag = true; とする



使い方

http://github.com/ichikaway/xformHelper/downloads

ここからダウンロード、もしくはgit cloneして設置下さい。

views/helpers/xform.php

views/helpers/xformjp.php

xform.phpが基本的な実装で、xformjpはxformを継承して、プロパティのみ日本語仕様(自分仕様で不要なものは極力false)に変更してます。お好きなほうをご利用下さい。xformjpを自分用に変更しても良いですし、myxform.phpみたいな別のクラスを作るのも良いかと思います。

xformを使ってプロパティのみ変更したい場合は、コントローラからhelpersプロパティで宣言する際に、下記の様に配列引数で指定すれば変更できます。

<?php
    var $helpers = array('Html', 'Form',  
          'xform' => array('changeDatetimeSeparator' =>
                    array(
                        'datefmt' => array(
                            'year' => '',
                            'month' => '',
                            'day' => '',
                            'afterDateTag' => '&nbsp;&nbsp;&nbsp;', //dateとtimeの表示の間に入れる文字列
                            ),
                        'timefmt' => array(
                            'hour' => '',
                            'min' => '',
                            'meridian' => '',
                            )
                        )
                    )
    );

上記のように、引数が多くなると面倒なので、自分はxformjpヘルパーを作りました。

これ以降は、xformjpヘルパーを前提に書きますが、xformヘルパーを利用する場合は全てxformjpをxformに置き換えて頂ければ大丈夫です。


コントローラ設定

<?php

var $helpers = array('Html', 'Form', 'xformjp');

//入力画面アクション
function add(){

}

//確認画面アクション
function add_confirm(){
    //確認画面の表示に切り替える(バリデーション成功後が望ましい)
    $this->params['xformHelperConfirmFlag'] = true;
}

Viewの設定

基本的には、使っているViewの$formを$xformjpに置換するだけで動きます(上記のようにコントローラ側で表示切替をする場合)

自分の場合は、$xformjp::input()関係を切り出してエレメント化して、入力画面用add.ctpと確認画面用add_confonfirm.ctpから同じエレメントを呼んでます。

まずは入力画面用add.ctp

<?php echo $xformjp->create('Modelname', array('action' => 'add_confirm'));?>

<?php
//外部ファイル読み込み
//views/elements/base_input.ctp
echo $this->element("base_input");
?>

<?php echo $xformjp->submit('確認', array('name' => 'confirm_action')); ?>
<?php echo $xformjp->end();?>

次に確認画面用add_confirm.ctp



<?php
//外部ファイル読み込み
//views/elements/base_input.ctp
echo $this->element("base_input");
?>

<?php echo $xformjp->create('Modelname', array('action' => 'add_finish'));?>
ここにHiddenでPostデータを出力 or セッションでPostデータを管理
<?php echo $xformjp->submit('送信', array('name' => 'finish')); ?>
<?php echo $xformjp->submit('戻る', array('name' => 'back')); ?>
<?php echo $xformjp->end();?>

hiddenでデータを一括で書き出す場合は、「よくある確認画面でのhiddenデータの持ち回り ver2」を使うと便利です! その更に改良版のformhiddenヘルパーgithubにあげてあります


最後に、共通で呼んでるエレメント base_input.ctp

今回は、text, password, select, checkboxを利用。

名前
<?php echo $xformjp->input('name'); ?>


パスワード
<?php echo $xformjp->password('password'); ?>


メール
<?php echo $xformjp->input('email'); ?>

<?php if($xformjp->checkConfirmScreen() === false) : // 確認画面では非表示  ?>
<?php echo $xformjp->input('email_conf'); ?>(確認)
<?php endif; ?>



チェックボックス
<?php
echo $xformjp->input('checkbox1',
array(
    'type' => 'select',
    'multiple' => 'checkbox',
    'options' => array('key1' => 'checkテスト', 'key2' => 'checkテスト2'),
    )
);
?>


セレクト
<?php echo $xformjp->input( 'select1' ,
array(
    'type' => 'select',
    'options'=> array('key1' => 'selectテスト', 'key2' => 'selectテスト2'),
    'label' => false,
    'empty' => '', 
    )
); ?>

メールの入力確認などは、確認画面で出力したくないので、$xformjp->checkConfirmScreen()を使って判定しています。falseが返ってきたら入力画面、trueが返ってきたら確認画面なので、これを使えば細かく制御できます。


既存のシステムで利用する場合は、views以下のフォルダのctpファイルで、$formを$xformjpに一括置換すればOK。あとはコントローラにちょっと追記するだけ。viewファイルの文字列一括置換は、例えばLinuxなどの場合であれば、下記のようにするだけです。

cd app/views
find . -type f -name "*.ctp" -exec perl -p -i -e 's/\$xform/\$xformjp/g' {} \;

これで面倒な確認画面がちょっと楽に実装できるようになった! 改良や不具合修正があれば連絡してもらえるとうれしいです。Twitterとかgithubのpullリクエストなどで。

Enjoy!

2009-11-10

HttpSocketクラスを使ってHTTPヘッダをセット

CakePHP1.2.5を利用してます。

HttpSocketクラスを使うと、HTTPを使った通信が簡単に使えます。例えば自分のサーバから、他のサーバHTTPでデータを送るとか簡単にできます。

基本的には、HttpSocketクラスを読み込んで、newして、getメソッドを呼ぶだけ。

App::import('Core', 'HttpSocket');
$HttpSocket = new HttpSocket();

$query = array('q1' => 'queryvalue1');
$ret = $HttpSocket->get('http://hogehoge.com/hoge/',$query);

このようにすると、hogehoge.comのサーバに、getリクエストを送ってくれます。

getメソッドの第2引数クエリになっていて、ここに配列で指定すると

http://hogehoge.com/hoge/?q1=queryvalue1

というようにクエリをつけてくれます。


サーバ間通信する際に、HTTPヘッダに何かしらの情報(例えば携帯の端末IDとか)も追加してHTTP通信したい場合は、getメソッドの第3引数に、配列で渡してあげます。

App::import('Core', 'HttpSocket');
$HttpSocket = new HttpSocket();

$query = array('q1' => 'queryvalue1');
$request = array('header' => array('X_UP_SUBNO' => 'AU000001111') );
$ret = $HttpSocket->get('http://hogehoge.com/hoge/’, $query, $request);

これをすれば、例えばSSLページのモバイルアクセス集計でビーコンのサーバ間通信する際に、ヘッダに携帯端末IDも入れてくださいといわれても大丈夫!


ヘッダ情報以外にも第3引数配列で色々と指定できるので、詳細は下記のページを参考に。

http://book.cakephp.org/ja/view/804/request

これを見ると、BASIC認証を通すとか、Cookieをセットするなどが可能です。


getメソッド以外にもPOSTメソッドなどもあります。詳細はAPIを参考に。

http://api.cakephp.org/file/cake/libs/http_socket.php

2008-12-28

Securityコンポーネントを使うときの注意点と特定アクションにBASIC認証をかける

CakePHP 1.2 RC3を利用しています。


Securityコンポーネントを使うと、Admin用コントローラやアクションのみにBASIC認証をかけるとかできて、色々と便利です。

その他にも、コンポーネントとして読み込むだけでフォームにトークンを埋め込んで、埋め込んだトークンがPOSTデータに含まれていないとエラーとして扱ってくれるなど、自然にCSRF対策が出来て便利です。

ただし、Viewファイルの中で、Cakeのformヘルパーを使ってフォームタグとフォームを閉じるタグを出力しないと、トークンを自動で埋め込んでくれないのでPOSTデータはすべてエラー扱いにされてしまいます。

最初、フォームを閉じる場合に、htmlのフォームタグを使って</form>としていてエラーになりはまりました。

フォームは下記のように

<?php echo $form->create('Admin', array( 'url' => '/admin/index' ) );?>

    <?php echo $form->input( 'loginid' , array('label' => false, 'size' => 15, 'maxlength' => 20 ) ); ?>

    <?php echo $form->password( 'password' , array('label' => false, 'size' => 15, 'maxlength' => 20 ) ); ?>

    <input name="login" type="image" src="/img/login.gif" alt="ログイン" onClick="#" />

<?php echo $form->end(); ?>

として、フォームの開始と終了を、create, endを呼び出しておく必要があります。


これで、例えばこのコントローラのアクションにBASIC認証をかけたい場合は、

class AdminController extends AppController {
	var $name = 'Admin';

	var $uses = array('Admin');

	var $helpers = array('Html', 'Form');


	var $components = array('Security');


	function beforeFilter(){
		parent :: beforeFilter();

                //下記ですべてのアクションにBASIC認証をかける
		$this->Security->loginOptions = array('type'=>'basic');
		$this->Security->loginUsers = array( BASIC_ADMIN_ID => BASIC_ADMIN_PW );
		$this->Security->requireLogin('*');

	}


	function index(){

		if( !empty( $this->data ) ){

			pr($this->data);exit;

		}

	}

	function hoge(){


	}

}

このようにすればOKです。

これで/admin/indexや/admin/hogeにアクセスすればBASIC認証画面が出ます。BASIC認証のID/PWはloginUsersにID/PWの連想配列を渡せばOKです。


参考

CakePHPBasic認証対応ページを作る

 http://blog.katsuma.tv/2008/07/cakephp_basic_auth.html

2008-12-18

ユニークなIDを生成する方法

CakePHP 1.2RC3を利用しています。

ユーザ登録などでアカウントアクティベーションする際に、ユニークなIDを含むURLを発行してメールで送信し、そのURLにアクセスさせる方法は良く見られます。

ユニークIDをphpで出力する一例として

md5(uniqid(rand(),1));

という方法などでmd5関数の出力結果を用いる場合があります(下記URL参照)

http://d.hatena.ne.jp/kakku22/20081016/1224154493



CakePHPを使って、ユニークなIDを簡単に発行する方法は、コントローラ


 $uuid = String::uuid();

とすると、$uuidに

494955da-0a5c-408f-ab20-0778ff2fc4a6

というようなユニークIDが含まれます。

これはCakePHPcake/libs/string.phpのuuidメソッドを利用しています。

uuidメソッドのソースコードを見ると、サーバのIPアドレス、プロセスIDの取得の後に、下記のコードでuuidを生成しています。

list($timeMid, $timeLow) = explode(' ', microtime());
$uuid = sprintf("%08x-%04x-%04x-%02x%02x-%04x%08x", (int)$timeLow, (int)substr($timeMid, 2) & 0xffff,
			mt_rand(0, 0xfff) | 0x4000, mt_rand(0, 0x3f) | 0x80, mt_rand(0, 0xff), $pid, $node);

return $uuid;

この実装は、下記のRFCに準拠しています。

http://www.ietf.org/rfc/rfc4122.txt

2008-12-03

cakeの便利なライブラリを使ってみよう(Socket編)

環境はCakePHP 1.2RC3です。

結構前にcakeの便利なライブラリを使ってみよう(xml編)というエントリーを書いたのですが、今回はソケット編です。

Cakephpには便利なライブラリcake/libs以下にあります。


例えば、http_socketであれば、簡単にhttp通信が行えます。http_socketに関する詳しい記事がありました。

http://www.syuhari.jp/blog/archives/825


http通信ではなく、他のプロトコルの通信が行いたい場合(レアケースだと思うけど)、socketライブラリを使うと簡単にできます。

今回はcakephpをコマンドラインからシェルで動かして、IRCサーバと通信する例です。

Cakephpのシェルに関する記事は下記に書いたので参照下さい。

cakePHPのコマンドラインプログラムでバッチ処理


まずは単純にサーバにConnect, Disconnectする例です。

app/venders/shells/cakeircbot.php

<?php
class CakeircbotShell extends Shell {


    var $uses = null;

    function main() {

		$ircserver = 'irc.freenode.net';
		$port = 6667;
		$channel = '#cakephp-jp';
		$nick = 'ichibot';
		$username = 'ichibot';

		$this->config = array(
			'persistent'	=> false,
			'host'			=> $ircserver,
			'protocol'		=> 'tcp',
			'port'			=> $port,
			'timeout'		=> 3000
		);

		uses('Socket');

		$sock = new CakeSocket($this->config);

		$sock->connect();

		$sock->disconnect();

    }

}

?>

接続するサーバのアドレスやポート番号をconfigにセットします。

uses('Socket')でcake/libs/socket.phpを呼び出しています。socket.phpを見ると、クラス名はCakeSocketになっているため、new CakeSocketでインスタンスを生成します。

メソッドconnectで接続、disconnectで切断です。これ以外にサーバへデータを送信するwrite, データを受信するreadメソッドがあります。


実行は、下記をコマンドラインから実行します。-app /home/hoge/www/appは付けなくても動くかもしれません。

/home/hoge/www/cake/console/cake -app /home/hoge/www/app cakeircbot


さて、socket通信でIRCサーバに接続するところまでできました。次は、IRCサーバのチャンネルにJoinして、IRCでのすべての発言を取得し、その内容に応じて応答を返すプログラムです。

<?php
class CakeircbotShell extends Shell {


    var $uses = null;

    function main() {


		$ircserver = 'irc.freenode.net';
		$port = 6667;
		$channel = '#cakephp-jp';
		$nick = 'ichibot';
		$username = 'ichibot';

		$this->config = array(
			'persistent'	=> false,
			'host'			=> $ircserver,
			'protocol'		=> 'tcp',
			'port'			=> $port,
			'timeout'		=> 3000
		);

		uses('Socket');

		$sock = new CakeSocket($this->config);


		$sock->connect();

		/* サーバへのメッセージは必ず\r\nで終わらせる */
		$sock->write("NICK " . $nick . "\r\n");
		$sock->write("USER $username $username $username $username" . "\r\n");
		$sock->write("JOIN " . $channel . "\r\n");



		while( 1 ){
			$value = $sock->read();
			if( $value == false ){

				if( $sock->connected != 1){
					exit;
					break;

				}else{
					continue;
				}

			}


			if( preg_match("/:.*hoge/i", $value) ){
				$sock->write('PRIVMSG ' . $channel . ' : '  . 'hogeって言ったよね?' . "\r\n");

			}elseif( preg_match("/:.*> $nick/i", $value) ){
				$sock->write('PRIVMSG ' . $channel . ' : '  . '呼んだ?' . "\r\n");


			
			}


			echo $value;
		}


		$sock->disconnect();

    }

}

?>

まず、connectした後に、writeでIRCサーバにChannelやNickの情報を送ります。送信後は、readでサーバからの応答を受け取ります。ここは永久ループでreadし続けます(終了は Ctl + c)

readした内容がfalseの場合はコネクションが切れている場合があるので、

$sock->connected

を使ってコネクションが切断されているかチェックします。

誰かがIRCで発言すると、$valueにその内容がセットされます。

下記の箇所では、誰かの発言の中にhogeという文字が含まれていたら、このボットプログラムが「hogeって言ったよね?」という応答を該当チャンネルで発言します。

それ以外に、ボットの名前を誰かの発言の中で見つけると、「呼んだ?」と返します。

if( preg_match("/:.*hoge/i", $value) ){
	$sock->write('PRIVMSG ' . $channel . ' : '  . 'hogeって言ったよね?' . "\r\n");

}elseif( preg_match("/:.*> $nick/i", $value) ){
	$sock->write('PRIVMSG ' . $channel . ' : '  . '呼んだ?' . "\r\n");



こんな感じで、お手軽にsocket通信できます。

今回はIRCでしたが、そのプロトコルに従えば何でも通信できますし、サーバ側のプログラムを自分で書けばオリジナルプロトコルも実現可能です。