[Symfony]SymfonyアプリケーションにKCAPTCHAを取り込む

CAPTCHA

CAPTCHAとはコンピュータが認識できない揺れた英数字を表示し、コンピュータによるスパムを防止するための機能である。

CAPTCHAとは

KCAPTCHAを単体で動作させる

まず、KCAPTCHAを以下のリンクからダウンロードする。
KCAPTCHA

ダウンロードしたパッケージをそのまま公開ディレクトリに配置し、単体で動作することを確認しよう。内包されているindex.phpにアクセスすると以下のように表示される。

SymfonyアプリケーションにKCAPTCHAを導入する

KCAPTCHAはRAW PHPで記述されており、Symfonyアプリケーション配下に取り込むとうまく動作しない。そこで、公開ディレクトリに配置し、Symfonyとは別のアプリケーションとして動作させることを考える。

単体で動作することは確認したので、Symfonyから呼び出して使う。Symfonyのテンプレートからimgタグを利用して以下のように呼び出す。

apps/frontend/module/XXXX/templates/XXXX.php

...
<img src="<?php echo $sf_request->getRelativeUrlRoot() . '/captcha/' ?>index.php ?>">
...

これで、問題なく表示されるはず。次に認証の実装を考える。KCAPTCHAをSymfonyと独立させたことで、KCAPTHCAがセッションに保存した認証文字列はSymfonyから正規ルートでアクセスすることは不可能である。クッキーからセッション情報を取り出して、無理やり認証文字列を取り出す必要がある。以下、action.class.phpの実装となる。(KCAPTCHAのセッション情報はキーを「captcha_keystring」として保存する)

apps/frontend/module/XXXX/actions/action.class.php

$sessionCaptcha = '';
$phpSessionData = file_get_contents(session_save_path() . '/sess_' . $request->getCookie('PHPSESSID'));
$arr = explode(";", $phpSessionData);
foreach ($arr as $value) {
    if (preg_match("/^captcha_keystring/", $value) === 1) {
        $arr1 = explode(":", $value);
        $sessionCaptcha = $arr1[2];
    }
}

COOKIEからPHPのセッションを取り出し、captcha_keystringをキーとし、その値を取得するというロジックである。これと、入力された文字列を比較し、認証を行う。

[PHP]PEAR::PagerをPOST遷移するときに$optionsで指定したonclickを有効にする方法

PEAR::Pagerとは

一覧で表示を行う場合にページを自動的に分割して表示する機能を提供してくれるPEARライブラリです。詳細な実装および設定方法に関しては以下のサイトを参照ください。
ページング(PEAR::Pager)

POSTで遷移する場合にonclickがきかない問題

PEAR::Pagerのオブジェクトを生成する方法はfactoryファンクションを利用するのですが、この引数として配列形式のオプションを渡してあげるとPEAR::Pagerの詳細な振る舞いについて定義することが可能となっています。このオプションでhttpMethodをPOSTにした場合にonclickがきかなくなります。関数定義とオプションの一覧を下記表に示します。

Pager::factory()
object &factory (array $options)
引数:
 $options オプションを格納した配列
戻り値:
 Pagerクラスオブジェクト。失敗した場合はPEAR_Errorオブジェクト

$optionsにて設定可能なオプションは以下の通りです。

オプション名 内容
itemData array 対象となるアイテムを格納した配列
totalItems integer 対象となるアイテム数(itemDataが未指定の場合に使用)
perPage integer 1ページごとに表示されるアイテム数
delta integer 現在のページの前後に表示するページ番号の数
mode string 動作モードを指定。ジャンプ型(Jumping)またはスライド型(Sliding)
httpMethod string 使用するHTTPメソッド。「GET」または「POST」
formID string httpMethodでPOSTを指定した場合のHTMLフォームを指定
importQuery boolean 変数と値がクエリ文字列からインポートするならTRUE
currentPage integer 初期ページ番号
expanded boolean TRUEの場合、ウィンドウサイズは2×delta+1
linkClass string リンクのスタイルのためのCSSクラス
urlVar string ページ番号を示すためのクエリ変数名(デフォルトは「pageID」)
path string ページへの絶対パス(ページ名は除く)
fileName string ページ名。appendがTRUEなら”%d”
fixFileName boolean FALSEなら、オプションfileNameは上書きされません
append boolean TRUEならpageIDはクエリ文字列としてURLに追加。FALSEならfileNameに従ってURLに埋め込まれる(デフォルトは「TRUE」)
altFirst string 最初のページのリンクに表示されるalt文。デフォルトは「first page」
altPrev string 前のページのリンクに表示されるalt文。デフォルトは「previous page」
altNext string 次のページのリンクに表示されるalt文。デフォルトは「next page」
altLast string 最後のページのリンクに表示されるalt文。デフォルトは「last page」
altPage string ページ番号の前に表示されるalt文。デフォルトは「page」
prevImg string 前のページのリンクである「<<」の代替文字。タグも可能
nextImg string 次のページのリンクである「&lgt;>」の代替文字。タグも可能
separator string ページ番号を分けるための区切り文字。カンマやハイフンの他にタグも可能
spacesBeforeSeparator integer 区切り文字の前のスペース数
spacesAfterSeparator integer 区切り文字の後のスペース数
curPageLinkClassName string 現在のページのリンクのスタイルのためのCSSクラス
curPageSpanPre string 現在のページのリンクの前のテキスト
curPageSpanPost string 現在のページのリンクの後のテキスト
firstPagePre string 最初のページ番号の前の文字。任意の文字列の他にタグも可能
firstPageText string 最初のページ番号の文字
firstPagePost string 最初のページ番号の後の文字。任意の文字列の他にタグも可能
lastPagePre string 最後のページ番号の前の文字。任意の文字列の他にタグも可能
lastPageText string 最後のページ番号の文字
lastPagePost string 最後のページ番号の後の文字。任意の文字列の他にタグも可能
clearIfVoid boolean 1ページしか無い場合にPageリンクを表示しないかどうか
useSessions boolean TRUEの場合、ページごとに表示するアイテム数は$_SESSION[$_sessionVar]変数に保存
closeSession boolean TRUEの場合、セッションはR/Wされた後にクローズする
sessionVar string perPageの値を格納するセッション変数名(1ページに複数のPagerを使用する場合に利用)
showAllText string generated by getPerPageSelectBox()で生成されたセレクトボックスでの’show all’オプションのために使用されるテキスト
pearErrorMode constant raiseError()でPEAR_ERRORモードを使用するかどうか(デフォルトは「PEAR_ERROR_RETURN」)

POSTで遷移する場合にonclickがきかない問題はなぜ発生するのか

ViewをレンダリングするPEAR::Pagerライブラリを見ればそれは一発です。Pager/Common.phpの問題のコードは以下となります。

    /**
     * Renders a link using the appropriate method
     *
     * @param string $altText  Alternative text for this link (title property)
     * @param string $linkText Text contained by this link
     *
     * @return string The link in string form
     * @access private
     */
    function _renderLink($altText, $linkText)
    {
        if ($this->_httpMethod == 'GET') {
            if ($this->_append) {
                $href = '?' . $this->_http_build_query_wrapper($this->_linkData);
            } else {
                $href = str_replace('%d', $this->_linkData[$this->_urlVar], $this->_fileName);
            }
            $onclick = '';
            if (array_key_exists($this->_urlVar, $this->_linkData)) {
                $onclick = str_replace('%d', $this->_linkData[$this->_urlVar], $this->_onclick);
            }
            return sprintf('<a href="%s"%s%s%s%s title="%s">%s</a>',
                           htmlentities($this->_url . $href, ENT_COMPAT, 'UTF-8'),
                           empty($this->_classString) ? '' : ' '.$this->_classString,
                           empty($this->_attributes)  ? '' : ' '.$this->_attributes,
                           empty($this->_accesskey)   ? '' : ' accesskey="'.$this->_linkData[$this->_urlVar].'"',
                           empty($onclick)            ? '' : ' onclick="'.$onclick.'"',
                           $altText,
                           $linkText
            );
        } elseif ($this->_httpMethod == 'POST') {
            $href = $this->_url;
            if (!empty($_GET)) {
                $href .= '?' . $this->_http_build_query_wrapper($_GET);
            }
            return sprintf("<a href='javascript:void(0)' onclick='%s'%s%s%s title='%s'>%s</a>",
                           $this->_generateFormOnClick($href, $this->_linkData),
                           empty($this->_classString) ? '' : ' '.$this->_classString,
                           empty($this->_attributes)  ? '' : ' '.$this->_attributes,
                           empty($this->_accesskey)   ? '' : ' accesskey=\''.$this->_linkData[$this->_urlVar].'\'',
                           $altText,
                           $linkText
            );
        }
        return '';
    }

    // }}}
    // {{{ _generateFormOnClick()

    /**
     * Mimics http_build_query() behavior in the way the data
     * in $data will appear when it makes it back to the server.
     *  For example:
     * $arr =  array('array' => array(array('hello', 'world'),
     *                                'things' => array('stuff', 'junk'));
     * http_build_query($arr)
     * and _generateFormOnClick('foo.php', $arr)
     * will yield
     * $_REQUEST['array'][0][0] === 'hello'
     * $_REQUEST['array'][0][1] === 'world'
     * $_REQUEST['array']['things'][0] === 'stuff'
     * $_REQUEST['array']['things'][1] === 'junk'
     *
     * However, instead of  generating a query string, it generates
     * Javascript to create and submit a form.
     *
     * @param string $formAction where the form should be submitted
     * @param array  $data       the associative array of names and values
     *
     * @return string A string of javascript that generates a form and submits it
     * @access private
     */
    function _generateFormOnClick($formAction, $data)
    {
        // Check we have an array to work with
        if (!is_array($data)) {
            trigger_error(
                '_generateForm() Parameter 1 expected to be Array or Object. Incorrect value given.',
                E_USER_WARNING
            );
            return false;
        }

        if (!empty($this->_formID)) {
            $str = 'var form = document.getElementById("'.$this->_formID.'"); var input = ""; ';
        } else {
            $str = 'var form = document.createElement("form"); var input = ""; ';
        }

        // We /shouldn't/ need to escape the URL ...
        $str .= sprintf('form.action = "%s"; ', htmlentities($formAction, ENT_COMPAT, 'UTF-8'));
        $str .= sprintf('form.method = "%s"; ', $this->_httpMethod);
        foreach ($data as $key => $val) {
            $str .= $this->_generateFormOnClickHelper($val, $key);
        }

        if (empty($this->_formID)) {
            $str .= 'document.getElementsByTagName("body")[0].appendChild(form);';
        }

        $str .= 'form.submit(); return false;';
        return $str;
    }

    // }}}
    // {{{ _generateFormOnClickHelper

    /**
     * This is used by _generateFormOnClick().
     * Recursively processes the arrays, objects, and literal values.
     *
     * @param mixed  $data Data that should be rendered
     * @param string $prev The name so far
     *
     * @return string A string of Javascript that creates form inputs
     *                representing the data
     * @access private
     */
    function _generateFormOnClickHelper($data, $prev = '')
    {
        $str = '';
        if (is_array($data) || is_object($data)) {
            // foreach key/visible member
            foreach ((array)$data as $key => $val) {
                // append [$key] to prev
                $tempKey = sprintf('%s[%s]', $prev, $key);
                $str .= $this->_generateFormOnClickHelper($val, $tempKey);
            }
        } else {  // must be a literal value
            // escape newlines and carriage returns
            $search  = array("\n", "\r");
            $replace = array('\n', '\n');
            $escapedData = str_replace($search, $replace, $data);
            // am I forgetting any dangerous whitespace?
            // would a regex be faster?
            // if it's already encoded, don't encode it again
            if (!$this->_isEncoded($escapedData)) {
                $escapedData = urlencode($escapedData);
            }
            $escapedData = htmlentities($escapedData, ENT_QUOTES, 'UTF-8');

            $str .= 'input = document.createElement("input"); ';
            $str .= 'input.type = "hidden"; ';
            $str .= sprintf('input.name = "%s"; ', $prev);
            $str .= sprintf('input.value = "%s"; ', $escapedData);
            $str .= 'form.appendChild(input); ';
        }
        return $str;
    }

ここで問題となるのはリンクをレンダリングする関数の「function _renderLink($altText, $linkText)」です。POSTとGETで実装が分岐しています。POST箇所のonclickのレンダリングを見てみると、L.37「$this->_generateFormOnClick($href, $this->_linkData)」でやっているようです。しかし、onclickをレンダリングする実装に$optionsで指定した「$this->_onclick」が呼ばれている実装はありません。これが原因。GETメソッドの場合はL.20で呼ばれているのに。

解決方法

POSTのonclickレンダリングで、$this->_onclickを設定してあげればよいのです。以下、実装です。

    // }}}
    // {{{ _generateFormOnClick()

    /**
     * Mimics http_build_query() behavior in the way the data
     * in $data will appear when it makes it back to the server.
     *  For example:
     * $arr =  array('array' => array(array('hello', 'world'),
     *                                'things' => array('stuff', 'junk'));
     * http_build_query($arr)
     * and _generateFormOnClick('foo.php', $arr)
     * will yield
     * $_REQUEST['array'][0][0] === 'hello'
     * $_REQUEST['array'][0][1] === 'world'
     * $_REQUEST['array']['things'][0] === 'stuff'
     * $_REQUEST['array']['things'][1] === 'junk'
     *
     * However, instead of  generating a query string, it generates
     * Javascript to create and submit a form.
     *
     * @param string $formAction where the form should be submitted
     * @param array  $data       the associative array of names and values
     *
     * @return string A string of javascript that generates a form and submits it
     * @access private
     */
    function _generateFormOnClick($formAction, $data)
    {
        // Check we have an array to work with
        if (!is_array($data)) {
            trigger_error(
                '_generateForm() Parameter 1 expected to be Array or Object. Incorrect value given.',
                E_USER_WARNING
            );
            return false;
        }

        $onclick = str_replace('%d', $this->linkData[$this->_urlVar], $this->onclick);
        $str = $onclick;

        if (!empty($this->_formID)) {
            $str = 'var form = document.getElementById("'.$this->_formID.'"); var input = ""; ';
        } else {
            $str = 'var form = document.createElement("form"); var input = ""; ';
        }

        // We /shouldn't/ need to escape the URL ...
        $str .= sprintf('form.action = "%s"; ', htmlentities($formAction, ENT_COMPAT, 'UTF-8'));
        $str .= sprintf('form.method = "%s"; ', $this->_httpMethod);
        foreach ($data as $key => $val) {
            $str .= $this->_generateFormOnClickHelper($val, $key);
        }

        if (empty($this->_formID)) {
            $str .= 'document.getElementsByTagName("body")[0].appendChild(form);';
        }

        $str .= 'form.submit(); return false;';
        return $str;
    }

追加したのはL.38, L.39です。これで、$optionsで設定したonclickがきくはず。

[PHP]Windows(XAMPP環境)でmemcachedを使う

memcachedとは

YouTube, Wikipedia, SouceForage, Facebookといった大規模有名サイトで利用されている分散型メモリキャッシュシステムである。利用方法としては、データベースの検索結果をmemcached内に保持し、データベースのアクセスの前にmemcached内のメモリを読み出して、なるべく低速な記憶媒体へのアクセスを減らすことに利用される。memcache内のメモリ領域を超えた場合は、以降の新規データ挿入は一番最後に利用されたキャッシュの順に削除される。
これは、Javaのサーブレットアプリケーションのようにサーバ内にプロセスが常駐してメモリを共有する機構のない言語、即ちPHPなどのスクリプト系の言語で利用される。

wikipediaより

インストール手順

大きく以下の流れで設定をしていきます。前提としてXAMPPがインストールされている必要があります。

1. memcachedをインストールする
2. PHP extensionとしてのmemcachedをインストールする

1. memcachedをインストールする

インターネットからダウンロードしてきて適切なディレクトリに配置します。実態はexeファイルひとつのみです。

以下にダウンロードリンクをはりつけておきます。
http://code.jellycan.com/files/memcached-1.2.5-win32-bin.zip

memcachedをC直下に配置した場合のサービスの登録方法(C:¥memcached¥memcached.exe)
ファイル名を指定して実行から以下のコマンドを発行します。

C:¥memcached¥memcached.exe -d install

memcachedの起動方法
ファイル名を指定して実行から以下のコマンドを発行します。

C:¥memcached¥memcached.exe -d start

PHP extensionとしてのmemcachedをインストールする

インターネットからダウンロードして適切なディレクトリに配置します。例えば、XAMPPのホームが「C:¥xampp」の場合はダウンロードしたファイル(php_memcache.dll)をC:¥xampp¥php¥extに配置してください。

次に、PHPの設定ファイル(php.ini)を開き有効化を行います。
php.iniを開き、extensionを追加します。またはコメントアウトされている場合、そのコメントアウトをはずします。

extension=php_memcache.dll

memcacheの設定を追加します。

[Memcache]
memcache.allow_failover = 1
memcache.max_failover_attempts = 20
memcache.chunk_size = 8192
memcache.default_port = 11211

以下にダウンロードリンクをはりつけておきます。
http://downloads.php.net/pierre/php_memcache-2.2.6-5.3-nts-vc9-x86.zip
http://downloads.php.net/pierre/php_memcache-2.2.6-5.3-vc9-x86.zip

確認

まず、phpinfo()にmemcacheが有効化されていることを確認ください。その後、サンプルコードを作ってみて。