‘解決’ カテゴリーのアーカイブ

Symfonyのクライアントサイドバリデータ

SymfonyでjQueryを使ったクライアントサイドのバリデーションを簡単に実現できるjnAjaxFormValitatorというプラグインがあったので、試してみました。

このプラグインはフォームが変更された際に、Ajaxでサーバにバリデーションルールと値を送ってサーバサイドでバリデーションをかけ、その結果を表示します。

結論から言うと、簡単に導入できて便利ですが、一部セキュリティ的に難あり、

1.2用ですが、ちょっとの修正で1.4でも使えます。

まずはダウンロード。

Plugins | jnAjaxFormValidatorPlugin | 1.0.2 | symfony | Web PHP Framework

20100617223542

ダウンロードしなくてもサーバから直接インストールできるらしいのですが、今回はtgzファイルをダウンロードしてきてインストールしました。

$ ./symfony plugin:install ./jnAjaxFormValidatorPlugin-1.0.2.tgz
>> plugin    installing plugin "./jnAjaxFormValidatorPlugin-1.0.2.tgz"
>> sfSymfonyPluginManager Installation successful for plugin "./jnAjaxFormValidatorPlugin-1.0.2.tgz"

インストール成功したら、アプリケーション以下のconfig/setting.ymlを開いてモジュールを有効にする設定を書きます。

all:
  .settings:
    enabled_modules:        [default, jnAjaxFormValidator]

READMEは全部スペルミスでjnAjaxFormValitatorになっているのでコピペすると間違えます。

次はアプリケーション以下のconfig/routing.ymlに以下を追加します。

# —– jnAjaxFormValidator —–
jnAjaxFormValidator_validateJSON:
  url:   /jnAjaxFormValidator/validateJSON
  param: { module: jnAjaxFormValidator, action: validateJSON }

その後、キャッシュをクリアします。

$ ./symfony cc

バリデーションを使用するテンプレートの先頭にでも、

<?php use_helper(‘ajaxFormValidator’) ?>
<?php echo ajaxAllFieldsValidators($form) ?>

と書いておけばOKです。

インストール手順はこれで完了ですが、このままだとSymfony 1.4で動かないので、actions.class.phpを修正します。

/plugins/jnAjaxFormValidatorPlugin/modules/jnAjaxFormValidator/actions/actions.class.php 29行目
sfLoader::loadHelpers(‘I18N’);

sfContext::getInstance()->getConfiguration()->loadHelpers(array(‘I18N’));

以上で完了です。

20100617225540

あと、書き出されるエラーが
<ul class="error_list"><li>エラーメッセージ</li></ul>
の構造に固定されているので、必要であればヘルパー側を修正する必要があります。

難点は、バリデーションのルールをブラウザ側に一度保持することです。単純にバリデータクラス名とバリデーションルール、値をJSからサーバに送信してバリデータを実行しているだけなので。

20100617225648

上の図はリクエスト時のパラメータです。GETで送られます。何が問題かというと、サーバ内のバリデーションルールが表に流れるので、攻撃者が穴を見つけやすくなるということですね。

バリデーションは完璧で、特にクリティカルな要件でないなら使えると思います。

あと、返されるエラーメッセージはformで定義したものを使えないので、デフォルトのエラーメッセージになります。

デフォルトのエラーメッセージは、アプリケーションのConfigurationなどで

sfValidatorBase::setDefaultMessage(‘max_length’, ‘%max_length%文字までにしてください’);

などとしておくか、カタログファイルでも変更できます。

ちなみにソースコードはびっくりするくらいシンプル。チェコ人が作ったらしく、I18Nも意識されてます。


sfWidgetFormInputCheckboxの正負を反転させる その2

前回のエントリで、モデルを拡張して読み出し時と保存時に正負反転させる方法を紹介しましたが、カラム名と実際の動作が逆になってしまって紛らわしいので、widgetとvalidatorで反転させることにしました。Symfony 1.4+Doctrineです。

既存のsfWidgetFormInputCheckboxとsfValidatorBooleanを継承して実装します。

lib/widget/sfWidgetFormInputCheckboxInverse.class.php

<?php

class sfWidgetFormInputCheckboxInverse extends sfWidgetFormInputCheckbox
{
    public function render($name, $value = null, $attributes = array(), $errors = array())
    {
        
        if (!(null !== $value && $value !== false))
        {
            $attributes['checked'] = 'checked';
        }

        if (!isset($attributes['value']) && null !== $this->getOption('value_attribute_value'))
        {
            $attributes['value'] = $this->getOption('value_attribute_value');
        }

        return parent::render($name, null, $attributes, $errors);
    }
}

lib/validator/sfValidatorBooleanInverse.class.php

<?php

class sfValidatorBooleanInverse extends sfValidatorBoolean
{

    protected function configure($options = array(), $messages = array())
    {
        parent::configure($options, $messages);
        $this->setOption('empty_value', true);
    }

    protected function doClean($value)
    {
        if (in_array($value, $this->getOption('true_values')))
        {
          return false;
        }

        if (in_array($value, $this->getOption('false_values')))
        {
          return true;
        }

        throw new sfValidatorError($this, 'invalid', array('value' => $value));
    }

}

ほぼ、逆にしたメソッドでオーバーライドしただけです。チェックボックスにチェックを入れなかった場合の挙動がデフォルトではfalseなので、trueに変更しています。

これをformクラスから

        $this->widgetSchema['show_flag'] = new sfWidgetFormInputCheckboxInverse();
        $this->validatorSchema['show_flag'] = new sfValidatorBooleanInverse();

のように定義してやればいいだけです。

例によってカラムのタイプがbooleanになっていることを確認してください。なっていないと0でも1でもtrueと判断されてしまうので。

        $this->hasColumn('show_flag', 'boolean', null, array(
             'type' => 'boolean',
             'notnull' => true,
             'default' => 0,
             ));


sfWidgetFormInputCheckboxの正負を反転させる その1

symfony 1.4にて。データベースの定義は「表示」フラグなのに、フォームでは「非表示」時にチェックを入れさせたい場合などがありまして。

データベースのカラム自体の意味を変えるのは、逆にフォームの意味が今後変わったときにどうするんだという話なので、今回はモデルでの入出力時点で値を変更する方法を試してみました。

actAsで各モデルの好きなカラムを反転できる拡張にしてみました。

lib/InvertBoolean.class.php

<?php

class InvertBoolean extends Doctrine_Template
{
    protected $_options = array(
        'columns' => array()
    );
    
    public function setTableDefinition()
    {
        $this->addListener(new InvertBooleanListener($this->_options));
    }
    
    public function setUp()
    {
    }
}

lib/model/invert_boolean/InvertBooleanListener.class.php

<?php

class InvertBooleanListener extends Doctrine_Record_Listener
{
    protected $_options;
    
    public function __construct(array $options)
    {
        $this->_options = $options;
    }

    public function postHydrate(Doctrine_Event $event)
    {
        $obj = $event->data;
        foreach ($this->_options['columns'] as $column) {
            $obj->$column = !($obj->$column);
        }
        $event->set('data', $obj);
    }

    public function preSave(Doctrine_Event $event)
    {
        foreach ($this->_options['columns'] as $column) {
            $value = !($event->getInvoker()->get($column));
            $event->getInvoker()->set($column, $value);
        }
    }
}

postHydrateでデータをデータベースから読み出してきてハイドレートした後に、preSaveで保存する前にそれぞれデータを反転しています。

カラムのタイプはあらかじめbooleanにしておく必要があります。schema.ymlからデータベース生成した場合は問題ないかと思います。そうでないなら、モデルのsetUpメソッドにでも

        $this->hasColumn('show_flag', 'boolean', null, array(
             'type' => 'boolean',
             'notnull' => true,
             'default' => 0,
             ));

など。

これを使用できるようにするために、同じくモデルのsetUpメソッドに

$this->actAs('InvertBoolean', array('columns'=>array('show_flag')));

とすればOKです。actAsのcolumnsオプションに複数カラム名設定すれば複数カラムを一括で指定できます。

これで、sfWidgetFormInputCheckboxを使ったチェックボックスにチェックを入れたときにデータベースに0が保存され、チェックを外すと1が保存されるカラムができあがりました。

問題点として、カラム名と反対の動きをすることになるので、うっかりする可能性が高まります。

なので、動作は確認しましたが、結局この案は使ってません。次のエントリーで紹介するカスタムウィジェットの方法を使っています。


VAIOのリカバリー時に、「エラー305: 64」表示

リカバリーを進めていくと、
エラー305: 64
とダイアログが表示されてリカバリーが17%くらいで止まる、というVAIOが持ち込まれてきました。
相談者はCDの焼きミスを疑い、SONYからリカバリディスクを取り寄せて試してみるも現象変わらず。

リカバリのメニューからコマンドプロンプトが起動できるので、中を見てみると、Cドライブは完全に空の状態。

試しにリカバリの推奨設定を無視して、パーティション設定でCドライブを50GBにして、残りをシステム復元用のパーティションにしてみたら、なぜかあっさり通って、CDの2枚目に入れ替えるようにというメッセージが表示されるまで進みました。
ということでメッセージに従うとあっさり復元成功。
何だろうこのバグ…

どうも復元用のパーティションが規定値では確保できないのが問題っぽいです。が、一度上記で成功すると2回目からは規定値でも復元可能になるという不思議な現象。

一応エラーコードの内容に関しては以下に掲載されているので参考までに。
http://www.kb.sony.com/selfservice/microsites/search.do?cmd=displayKC&externalId=C1000995&fes=true


admin generatorの編集後のリダイレクト先変更

admin generatorのeditアクションで、編集後に再度編集画面ではなく、リストに飛ばしたい要件があったのでメモ。例によってSymfony 1.4+Doctrine+admin generator。

class hogeActions extends autoHogeActions
{
    public function executeUpdate(sfWebRequest $request)
    {
        $this->forward404Unless($request->isMethod('put'));
        
        $this->hoge = $this->getRoute()->getObject();
        $this->form = $this->configuration->getForm($this->hoge);
        
        if ($this->form->bindAndSave($request->getParameter($this->form->getName()), $request->getFiles($this->form->getName()))) {
            $this->redirect('hoge');
        }
        
        $this->setTemplate('edit');
    }
}

executeUpdateをオーバーライドすればOKのようです。ここではisMethodがputなことに注意。


Symfonyでのsave時に独自の処理を挟む

Symfony 1.4+Doctrineで、admin generatorな環境です。editなど、自動生成なので、どこをオーバーライドすれば目的の動作になるのかわかりにくいことがありますね。

例えば、あるモデルのステータスを変更すると同時に、他のモデルに対しても操作したい場合があるとします。

まぁ、mergeFormとかembedFormとか使ってもいいのですが、今回は他テーブルのプライマリキーをフォームから選択させたかったので、汎用性のあるdoSaveでの処理を追加してみました。

symfony Forms in Action | 第11章 – Doctrine との統合 | symfony | Web PHP Framework

class BackendHogeForm extends HogeForm
{
    public function configure()
    {
        parent::configure();

        // 使用するフィールドを指定
        $this->useFields(array('status'));
        
        // 別モデルのIDリストを選択するフォームを作成する
        $choices = array(100 => '100:value1', 101 => '101:value2');
        $this->widgetSchema['other_model_id'] = new sfWidgetFormSelect(array('choices' => $choices));
        $this->validatorSchema['other_model_id'] = new sfValidatorChoice(array('choices' => array_keys($choices)));
    }

    protected function doSave($con = null)
    {
        if ($this->getValue('status') == 'DONE') {
            Doctrine::getTable('OtherModel')->find($this->getValue('other_model_id'))
                ->setIsSelected(TRUE)
                ->save();
        }
        return parent::doSave($con);
    }
}

基本的にはdoSaveをオーバーライドするだけです。ちなみにuseFieldsメソッドを使うと必要なフィールド以外を全部unsetしてくれるので便利です。


PHPのDateTimeの結果が-0001-11-30 00:00:00になる現象について

mySQLなどで、日時を0000-00-00 00:00:00としてデータベースに格納しておくことがありますが、これを読み込んでそのままPHPのDateTimeオブジェクトに渡すと、出力が-0001-11-30 00:00:00になってしまいます。

$a = new DateTime('0000-00-00 00:00:00');
echo $a->format('Y-m-d H:i:s');
// Output: -0001-11-30 00:00:00

PHP :: Bug #42971 :: DataTime::format(): not well formated data ‘0000-00-00 00:00:00’

で、これはバグではないと言われているので、どういうことかと考えてみると、0000-00-00は存在しない0月0日を指定しているので、0月は繰り下がって-1年12月0日、さらに0日も繰り下がって-1年11月30日、となるわけですね。

データベースとPHPの文化の違い、というところでしょうか。

ちなみにDateTime型、コンストラクタにNULLを渡すと現在時刻のインスタンスが生成されるので、データベースの値をNULLにしておくと、現在時刻になってしまいます。うーん…symfonyのDoctrineでNULL判定したい場合はどうすればいいんだ…

$row->getDateTimeObject('deleted_at')

みたいなことがやりたいのですが。

sfDoctrineRecordも

    $type = $this->getTable()->getTypeOf($dateFieldName);
    if ($type == 'date' || $type == 'timestamp')
    {
      return new DateTime($this->get($dateFieldName));
    }

こうなってるからオーバーライドするしかないのかな。


SymfonyのAdmin generatorでリストにフィルタを表示させない方法

Admin generatorを使っていて、リスト表示をするときにフィルタが不要な場合があります。例えば行が少ないとか、すでに決まったフィルタが別にある場合など。フィルタのフォーム自体を消したい場合は以下の方法で。

apps/backend/modules/MODULE_NAME/templates/

_filters.php
を空で作成すればいいだけです。

admin generatorでの開発とはキャッシュを読む作業と見つけたり…

追記

コメントで教えていただいた方法で、もっと簡単に実現できました。filterのclassをfalseにするだけでOKです。

generator:
  class: sfDoctrineGenerator
  param:
    config:
      filter:
        class: false


ImageMagickのPDF変換用gsコマンドを差し替える

例によってRedHat EL4にて、yumでconvertを入れたものの、バージョンが6.4.6と古く、Powerpointで作成したPDFがエラーになる。

エラー内容はこんな感じ。

$ convert -channel RGB ./test.pdf ./test.jpg

   **** This file has a corrupted %%EOF marker, or garbage after the %%EOF.
   **** The file was produced by Microsoft? PowerPoint? 2010:
   **** please notify the author of this software
   **** that the file does not conform to Adobe's published PDF
   **** specification.  Processing of the file will continue normally.

Error: /rangecheck in --.buildshading2--
Operand stack:
   --dict:8/8(L)--   --dict:3/5(L)--   --nostringval--   false   --nostringval--   --nostringval--   --nostringval--   --nostringval--   --dict:3/4(L)--   --nostringval--   --dict:5/5(L)--
Execution stack:
   %interp_exit   .runexec2   --nostringval--   --nostringval--   --nostringval--   2   %stopped_push   --nostringval--   --nostringval--   --nostringval--   false   1   %stopped_push   1   3   %oparray_pop   1   3   %oparray_pop   1   3   %oparray_pop   --nostringval--   2   1   2   --nostringval--   %for_pos_int_continue   --nostringval--   --nostringval--   --nostringval--   --nostringval--   1   %stopped_push   --nostringval--   --nostringval--   --nostringval--   1   %stopped_push   --nostringval--   --nostringval--   %array_continue   --nostringval--   false   1   %stopped_push   --nostringval--   %loop_continue   --nostringval--   --nostringval--   --nostringval--   3   10   %oparray_pop   --nostringval--   --nostringval--   false   1   %stopped_push   --nostringval--   --nostringval--
Dictionary stack:
   --dict:1069/1123(ro)(G)--   --dict:0/20(G)--   --dict:93/200(L)--   --dict:93/200(L)--   --dict:97/127(ro)(G)--   --dict:229/230(ro)(G)--   --dict:19/24(L)--   --dict:4/6(L)--   --dict:19/20(L)--   --dict:3/5(L)--
Current allocation mode is local
GNU Ghostscript 7.07: Unrecoverable error, exit code 1
convert: Postscript delegate failed `./test.pdf': No such file or directory @ pdf.c/ReadPDFImage/612.
convert: missing an image filename `./test.jpg' @ convert.c/ConvertImageCommand/2710.

で、見る限り、GhostScriptのエラーのようなので、gsのバージョンを調べてみると、

$ gs -v

GNU Ghostscript 7.07 (2003-05-17)

Copyright (C) 2003 artofcode LLC, Benicia, CA.  All rights reserved.

というわけで、GhostScriptのコマンドの最新版を別にインストールして対応。その際、元のgsコマンドを入れ替えるわけにいかなかったので、ImageMagickから読み込むgsコマンドのファイルだけを指定できないかと思ったら、ImageMagickはXMLでコマンドを定義していたんですね。

ImageMagick • View topic – manually set delegate path to gs (ghostscript)

各種コマンドのオプションがImageMagick-6.4.6/config/delegates.xmlに記述されています。

gsコマンドの箇所を絶対パスに書き換えてやるだけで完了。便利。


Symfonyでメール送信のデバッグをする

Symfony1.4で、メールを送信するコードで、開発中にテンプレートの動作確認をしたい場合があります。Swift Mailerを使っているなら、すべての送信メールのあて先を上書きして特定のメールアドレスにすることができるので、やり方メモ。

The More with symfony book | メール | symfony | Web PHP Framework

設定の仕方は簡単で、アプリケーションのconfig\factories.ymlにdelivery_strategy: single_addressを指定すればOK。同時にdelivery_addressに、あて先メールアドレスを記述します。

dev:
  mailer:
    param:
      delivery_strategy: single_address
      delivery_address: dev@localhost

これで開発環境のローカルユーザdevにメールが送られます。

元々のヘッダ指定 toccbcc のどれに送られたメールなのかを確認できるよう、それぞれ X-Swift-ToX-Swift-CcX-Swift-Bcc というヘッダが追加されます。

あとはローカルのメールをMewとかで読めばOKです。

もちろんdelivery_addressを外部のメールアドレスに指定してもいいですよ。

PayPalの開発環境みたいにメールのSandBoxとか統合されないかなぁ。