CakePHP 簡単なビヘイビアを作ってみる

前回の記事、『CakePHP サニタイズでエスケープされた改行コードを元に戻す簡単なコンポーネントを作ってみる』で簡単なコンポーネントを作成したのですが、色々考えた結果「データの加工などの処理はモデルで行うべきだ」との考えに至ったので、前回作成したコンポーネントと同じ処理をするビヘイビアを作成してみたいと思います。

作成するビヘイビアのメソッドとしては、『サニタイズでエスケープされた改行コードを元に戻す』『HTMLエンティティを特殊文字に置き換える』です。

ビヘイビアは、『ModelBehavior』クラスを継承して作成します。

▼『ModelBehavior』クラスを継承

class HtmlDecodeBehavior extends ModelBehavior {
    public $name = 'HtmlDecode';
}

CakePHPの命名規則に則り、『/app/models/behaviors』に保存します。

ビヘイビアの準備ができたので、メソッドを追加していきます。

コンポーネントの作成との違いは、モデルから呼び出せるメソッドの『第1引数がモデルの参照になる』ことです。
private宣言やprotected宣言のメソッドは『第1引数がモデルの参照になる』ことはありません。

▼作成したビヘイビアにメソッドを追加

class HtmlDecodeBehavior extends ModelBehavior {
    public $name = 'HtmlDecode';

    // 置き換え対象のHTMLエンティティ
    protected $_patterns = array(
        'before' => array(
            "/¥&/",    // 『&』
            "/¥&lt;/",     // 『<』
            "/¥&gt;/",     // 『>』
            "/¥&quot;/",   // 『'』
            "/¥&#39;/",    // 『"』
        ),

        'after' => array(
            "&",        // 『&amp;』
            "<",        // 『&lt;』
            ">",        // 『&gt;』
            "'",        // 『&quot;』
            '"',        // 『&#39;』
        )
    );

    /**
     *    HTMLエンティティを特殊文字に置き換える
     *
     *    @param     string or array    $data      HTMLエンティティを特殊文字に置き換えたい配列、または文字列
     *    @param     pool               $script    置き換え後の特殊文字がScriptタグになった場合、削除するなら『true』を指定
     *    @param     pool               $space     置き換え後の文字列からスペースを削除するなら『true』を指定
     *
     *    @return    string or array    処理後、引数に指定された形式で返す。
     */
    public function htmlCheck(&$model, $data, $script = true, $image = true, $space = true) {
        if(is_array($data)) {
            $cnvData = array();

            foreach($data as $key => $item) {
                if(isset($item)) {
                    // 特殊文字へ置き換え
                    $cnvData[$key] = $this->_htmlDecode($item);

                    // スクリプトタグの削除
                    if($script) $cnvData[$key] = Sanitize::stripScripts($cnvData[$key]);

                    // 画像の削除
                    if($image) $cnvData[$key] = Sanitize::stripImages($cnvData[$key]);

                    // スペースの削除
                    if($space) $cnvData[$key] = Sanitize::stripWhitespace($cnvData[$key]);
                }
            }
        } else {
            if(isset($data)) {
                // 特殊文字へ置き換え
                $cnvData = $this->_htmlDecode($data);

                // スクリプトタグの削除
                if($script) $cnvData = Sanitize::stripScripts($cnvData);

                // 画像の削除
                if($image) $cnvData = Sanitize::stripImages($cnvData);

                // スペースの削除
                if($space) $cnvData = Sanitize::stripWhitespace($cnvData);
            }
        }

        return $cnvData;
    }

    /**
     *    配列、または文字列に含まれる『///n』を『/n』に置き換える
     *
     *    @param     string or array     $data         『///n』を『/n』に置き換えたい配列、または文字列
     *    @param     pool                $conversion    置き換え後の『/n』を『
』タグに変更する場合、trueを指定 * * @return string or array 処理後、引数に指定された形式で返す。 * */
public function nlCheck(&$model, $data, $conversion = false) { if(is_array($data)) { $cnvData = array(); foreach($data as $key => $item) { if(isset($item)) { // 『/n』へ置き換え $cnvData[$key] = $this->_nlDecode($item); // 『
』へ置き換え
if($conversion) $cnvData[$key] = nl2br($cnvData[$key]); } } } else { if(isset($data)) { // 『/n』へ置き換え $cnvData = $this->_nlDecode($data); // 『
』へ置き換え
if($conversion) $cnvData = nl2br($cnvData); } } return $cnvData; } /** * HTMLエンティティを特殊文字に置き換える * * @param string $data 置き換えたい文字列 * * @return string 置き換え後の文字列 */ protected function _htmlDecode($data) { return preg_replace($this->_patterns['before'], $this->_patterns['after'], $data); } /** * 『///n』を『/n』に置き換える * * @param string $data 置き換えたい文字列 * * @return string 置き換え後の文字列 */ protected function _nlDecode($data) { return preg_replace('/\\\n/', "\n", $data); } }

内容は、前回作成したコンポーネントと同じで、『preg_replace』関数を使用し文字を置き換えていくだけです。

後は、モデルから作成したビヘイビアを読み込み処理をしていきます。
ビヘイビアを読み込む場合は、メンバ変数『$actsAs』に使用したいビヘイビア名を指定します。

注意しなければならないのが、ビヘイビア側の第1引数はモデルの参照になっていますが、モデル側では第1引数を省略し第2引数から指定していくことです。
また、コンポーネントの『$this->コンポーネント名->コンポーネントのメソッド名』とは違い、『$this->ビヘイビアのメソッド名』となります。

class HtmlDecodeBehavior extends ModelBehavior {
    public function nlCheck(&$model, $data, $conversion = false) {
        /*
         *    処理は省略します
         */
    }
}
class Post extends AppModel {
    public $name   = 'Post';
    public $actsAs = 'HtmlDecode';

    public function find($type, $options = array()) {
        /*
         *    処理は省略します
         */

        $data = $this->nlCheck($data, true);
    }

このことに注意しながら、実装していきます。

▼モデル

class Post extends AppModel {
    public $name   = 'Post';
    public $actsAs = 'HtmlDecode';

    public function find($type, $options = array()) {
        switch($type) {
            case 'show':
                // データの取得
                $data = parent::find('first', $options);

                $data['Post'] = $this->nlCheck($data['Post'], true);

                return $data;
            break;

            default:
                return parent::find($type, $options);
            break;
        }
    }
}

▼コントローラー

class PostsController extends AppController {
    public $name  = 'Posts';
    public $users = array('Post');

    public function show($id = null) {
        if (!$id) $this->referer($this->referer());

        // データの取得
        $post = $this->Post->find('show', array('conditions' => array('Post.id = ?' => $id)));

        $this->set('post', $post);
    }
}

これでビヘイビアの作成は終了です。
いくつか注意点はありますが、基本的には簡単に作成することができました。

CakePHP サニタイズでエスケープされた改行コードを元に戻す簡単なコンポーネントを作ってみる

『Sanitize::clean』でサニタイズを行うと、改行コードもエスケープされます。

このままでは、『textarea』要素に入力された改行がない状態になり、想像していた表示にならないことがあります。
下記の赤文字の部分がサニタイズ時にエスケープされた改行コードです。

▼サニタイズされた『textarea』要素のテキスト

登録時に『Sanitize::clean』を使用しサニタイズを行いました。\n\nこのテキストは確認用です。

このままでは『nl2br』関数も使用できないので、コンポーネント作成の練習も兼ねて、『\\\n』を『\n』に戻す簡単なコンポーネントを作りたいと思います。

(表示では『\n』ですが、実際はエスケープシーケンスがあるので『\\\n』の状態になっています。)


コンポーネントは、『Object』クラスを継承し作成します。

▼『Object』クラスを継承

class HtmlDecodeComponent extends Object {
    public $name = 'HtmlDecode';
}

これをCakePHPの規約どおりに、『html_decode.php』として『/app/controllers/components』ディレクトリに配置します。
これでコンポーネントの準備ができました。

次は、メソッドを追加していきます。
改行コードを元に戻すメソッドだけでは寂しいので、HTMLエンティティを特殊文字に置き換えるメソッドも書いてみました。

▼作成したコンポーネントにメソッドを追加

class HtmlDecodeCompnent extends Object {
    public $name = 'HtmlDecode';

    // 置き換え対象のHTMLエンティティ
    protected $_patterns = array(
        'before' => array(
            "/¥&/",    // 『&』
            "/¥</",     // 『<』
            "/¥>/",     // 『>』
            "/¥"/",   // 『"』
            "/¥'/",    // 『'』
        ),

        'after' => array(
            "&",    // 『&amp』
            "<",    // 『&lt;』
            ">",    // 『&gt;』
            "'",    // 『&quot;』
            '"',    // 『&#39;』
        )
    );

    /**
     *	HTMLエンティティを特殊文字に置き換える
     *
     *	@param    string or array     $data      HTMLエンティティを特殊文字に置き換えたい配列、または文字列
     *	@param    pool                $script    置き換え後の特殊文字がScriptタグになった場合、削除するなら『true』を指定
     *	@param    pool                $space     置き換え後の文字列からスペースを削除するなら『true』を指定
     *
     *	@return   string or array     $data      処理後、引数に指定された形式で返す。
     */
    public function htmlCheck($data, $script = true, $image = true, $space = true) {
        if(is_array($data)) {
            $cnvData = array();

            // 配列の処理
            foreach($data as $key => $item) {
                if(isset($item)) {
                    // 特殊文字へ置き換え
                    $cnvData[$key] = $this->_htmlDecode($item);

                    // スクリプトタグの削除
                    if($script) $cnvData[$key] = Sanitize::stripScripts($cnvData[$key]);

                    // 画像の削除
                    if($image) $cnvData[$key] = Sanitize::stripImages($cnvData[$key]);

                    // スペースの削除
                    if($space) $cnvData[$key] = Sanitize::stripWhitespace($cnvData[$key]);
                }
            }
        } else {
            if(isset($data)) {
                // 特殊文字へ置き換え
                $cnvData = $this->_htmlDecode($data);

                // スクリプトタグの削除
                if($script) $cnvData = Sanitize::stripScripts($cnvData);

                // 画像の削除
                if($image) $cnvData = Sanitize::stripImages($cnvData);

                // スペースの削除
                if($space) $cnvData = Sanitize::stripWhitespace($cnvData);
            }
        }

        return $cnvData;
    }

    /**
     *	配列、または文字列に含まれる『///n』を『/n』に置き換える
     *
     *	@param    string or array    $data         『///n』を『/n』に置き換えたい配列、または文字列
     *	@param    pool               $conversion    置き換え後の『/n』を『
』タグに変更する場合、trueを指定 * * @return string or array $data 処理後、引数に指定された形式で返す。 * */
public function nlCheck($data, $conversion = false) { if(is_array($data)) { $cnvData = array(); foreach($data as $key => $item) { if(isset($item)) { // 『/n』へ置き換え $cnvData[$key] = $this->_nlDecode($item); // 『
』へ置き換え
if($conversion) $cnvData[$key] = nl2br($cnvData[$key]); } } } else { if(isset($data)) { // 『/n』へ置き換え $cnvData = $this->_nlDecode($data); // 『
』へ置き換え
if($conversion) $cnvData = nl2br($cnvData); } } return $cnvData; } /** * HTMLエンティティを特殊文字に置き換える * * @param string $data 置き換えたい文字列 * * @return string 置き換え後の文字列 */ protected function _htmlDecode($data) { return preg_replace($this->_patterns['before'], $this->_patterns['after'], $data); } /** * 『///n』を『/n』に置き換える * * @param string $data 置き換えたい文字列 * * @return string 置き換え後の文字列 */ protected function _nlDecode($data) { return preg_replace('/\\\n/', "\n", $data); } }

『preg_replace』関数を使用し、正規表現で文字を置き換えていくだけのメソッドたちです。

あとは、コントローラーからコンポーネントを呼び出せば、他のコンポーネント同様に使用することができます。

▼コントローラー

class PostsController extends AppController {
    public $name  = 'Posts';
    public $users = array('Post');
    public $components = array('HtmlDecode');

    public function index() {
        /*
        処理は省略します・・・
        */

        $text = $this->HtmlDecode->nlCheck('登録時に『Sanitize::clean』を使用しサニタイズを行いました。\n\nこのテキストは確認用です。', true);
        $this->set('text', $text);
    }
}

▼ビュー

<?php
echo $text;
?>

▼結果

登録時に『Sanitize::clean』を使用しサニタイズを行いました。
<br />
<br />
<br />
このテキストは確認用です。

コンポーネントを作成するのもとっても簡単ですね。
さすがCakePHPって感じです。

ちなみに、今回はコンポーネントとして作成しましたが、モデルの段階で処理したほうがいいのか(ビヘイビア)迷いました。

ビューで処理するのは、『コントローラーから渡されたデータをビュー側でループや条件分岐以外の処理をして表示する』のは間違っていると思うので止めましたが・・・

もっとMVCの理解を深めないといけないですね^^;


※記事を投稿後、『CakePHPを使ったMVC設計のベストプラクティス』を見て、モデルの段階で処理したほうがいい気がしてきました。
今回作成したコンポーネントと同じ処理を行うビヘイビアを作成しようと思います。

CakePHP コントローラーのメソッドを『private』や『protected』にする方法

CakePHPでコントローラーのメソッドを『private』宣言や『protected』宣言したい場合は、メソッド名の前に『_(アンダーバー)』をつけることで実現できます。
(『private』や『protected』については、「PHP publicとかprivateとかの修飾子」参照。)

『private』なメソッドを作成したい場合は『__(アンダーバー2個)』
『protected』なメソッドの場合は、『_(アンダーバー1個)』をメソッド名の前につけます。

こうする事で、自動的に『private』や『protected』メソッドとなります。

▼『private』『protected』メソッドの作成例

class TestsController extends Appcontroller {
    // これはpublic宣言になる
    function publicTest() {
    }

    // これはprivate宣言になる
    function __privateTest() {
    }

    // これはprotected宣言になる
    function _protectedTest() {
    }
}

また、『private』や『protected』なメソッドには、アクションとしてアクセスできないようになります。

例えば、上記の例なら『http://ドメイン名/tests/__privateTest』としてアクセスしようとした場合、『Debug Level』が『1』以上なら『privateメソッドなのでアクセスできないよ!!』みたいなエラーページが表示。

『Debug Level』が『0』なら『そんなアクションはないよ!!』なエラーページが表示されます。

ちなみに、以下のようにしても『private』や『protected』なメソッドは作成できますが、『Debug Level』が『1』以上の場合『privateメソッドなのでアクセスできないよ!!』エラーページは表示されず、『アクションがみつからないよ!!』みたいなエラーページが表示されます。

(CakePHPは「設定よりも規約」なので、『private』メソッドは『__』。『protected』メソッドには『_』をつけるべきだと思います。)

▼『_』をつけない場合

class TestsController extends Appcontroller {
    // これはpublic宣言になる
    public function publicTest() {
    }

    // これはprivate宣言になる
    private function privateTest() {
    }

    // これはprotected宣言になる
    protected function protectedTest() {
    }
}

『private』も『__』も書いた場合は、『privateメソッドなのでアクセスできないよ!!』エラーだったので、両方書いても問題ないのかな??
情報おまちしております・・・

CakePHP ACLに苦しむ・・・

ACLコンポーネントを使ってみた結果を結論から言うと、ACL(アクセス制御リスト)ってすごい便利だと思います。

ACLとは、マニュアルにも書いてあるように「優れた保守性と管理性を保ちつつ、アプリケーションのパーミッションをきめ細かく設定する」機能を提供してくれます。
例えば、管理者しかアクセスできないコントローラーやアクションを設定することができます。

「これは是非とも使いたい」と思い、マニュアルを見たのですが、説明がすごく分かりにくかった・・・

というわけで、自分のためにもブログに残しておきます。
(文章力がないのでマニュアルより分かりづらくなりそうですが・・・)

適当なコントローラーにアクションをいくつか作成し、管理者(administrators)、運営者(managers)、利用者(users)の3つのグループとユーザーを作成。グループごとにパーミッションを設定していく流れで説明していきたいと思います。


1・データベースの準備

まずはデータベースの準備を行います。
認証とACLはセットとなるのが一般的なので、まずはuserテーブルから作成することにします。
グループごとにパーミッションを設定していく予定なので、usersテーブルには以下のフィールドを用意し作成します。
(Bakeで作成しても問題ありません。)

▼usersテープルの作成

CREATE TABLE users (
    id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(255) NOT NULL UNIQUE,
    password VARCHAR(80) NOT NULL,
    group_id INT(11) NOT NULL,
    created DATETIME,
    modified DATETIME
);

次に、usersテーブルの外部キーとなるgroupsテーブルを作成します。

▼groupsテープルの作成

CREATE TABLE groups (
    id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    created DATETIME,
    modified DATETIME
);

その他のテーブルを作成します。
今回はテストなので、適当なテーブルを1つ作成することにします。

▼その他のテープルの作成

CREATE TABLE posts (
    id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
    user_id INT(11) NOT NULL,
    title VARCHAR(255) NOT NULL,
    body TEXT,
    created DATETIME,
    modified DATETIME
);

ACL関連のテーブルを作成します。
Bakeコマンドで『cake schema create DbAcl』を実行するか、以下のSQLでテーブルを作成します。
(このSQL文は、CakePHP1.3.10では『/cake/console/templates/skel/config/schema/db_acl.php』にも書いてあります。)

▼ACL関連のテーブルを作成

CREATE TABLE acos (
    id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT,
    parent_id INTEGER(10) DEFAULT NULL,
    model VARCHAR(255) DEFAULT '',
    foreign_key INTEGER(10) UNSIGNED DEFAULT NULL,
    alias VARCHAR(255) DEFAULT '',
    lft INTEGER(10) DEFAULT NULL,
    rght INTEGER(10) DEFAULT NULL,
    PRIMARY KEY  (id)
);

CREATE TABLE aros_acos (
    id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT,
    aro_id INTEGER(10) UNSIGNED NOT NULL,
    aco_id INTEGER(10) UNSIGNED NOT NULL,
    _create CHAR(2) NOT NULL DEFAULT 0,
    _read CHAR(2) NOT NULL DEFAULT 0,
    _update CHAR(2) NOT NULL DEFAULT 0,
    _delete CHAR(2) NOT NULL DEFAULT 0,
    PRIMARY KEY(id)
);

CREATE TABLE aros (
    id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT,
    parent_id INTEGER(10) DEFAULT NULL,
    model VARCHAR(255) DEFAULT '',
    foreign_key INTEGER(10) UNSIGNED DEFAULT NULL,
    alias VARCHAR(255) DEFAULT '',
    lft INTEGER(10) DEFAULT NULL,
    rght INTEGER(10) DEFAULT NULL,
    PRIMARY KEY  (id)
);

これで、データーベースの準備は終了です。


2・Authの準備

繰り返しになりますが、ACLはあくまで「アプリケーションのパーミッションをきめ細かく設定する」ものなので、認証(ログイン処理など)とは異なります。
認証はAuthコンポーネントを使用して行います。
(Authコンポーネントについては、「CakePHP Authコンポーネントを使ってみた」を参照。)

今回は、すべてのコントローラーにAuthコンポーネントを使用したいので、『AppController』を作成したいと思います。

▼『AppController』の作成と認証処理

class AppController extends Controller {
    public $components = array('Auth', 'Acl', 'Session');

    public function beforeFilter() {
    // Authコンポーネントの設定

    /*
     * モードの指定
     * [actions] でアクション単位のパーミッションチェック(アクセス可能か確認)を行う
     * [crud]    でコントローラー単位のパーミッションチェック(CRUDによる確認)を行う
     */
    $this->Auth->authorize = 'actions';

    $this->Auth->loginRedirect  = array('controller' => 'posts', 'action' => 'index');
    $this->Auth->logoutRedirect = array('controller' => 'users', 'action' => 'login');

    // ↓グループとユーザーとパーミッションを登録したら削除する
    $this->Auth->allow('*');
    }
}

ACLでもっとも苦しんだのが『$this->Auth->authorize』でした・・・
実は、『$this->Auth->authorize』に指定する値でパーミッションをチェックする方法が大きく変わります。

『actions』を指定すると、アクションに対するアクセス可能か確認し、『crud』を指定するとコントローラーに対してCRUDによるパーミッションの確認となります。

つまり、『actions』ではCRUDによる確認はできず、『crud』ではコントローラーだけにCRUDによる確認をすることができるということになります。

今回のケースでは、アクションごとにパーミッションを設定したいので『actions』を指定します。

ちなみに、マニュアルの5.1 アクセス制御リストは『crud』を使った場合の説明
11.2 ACL を制御するシンプルなアプリケーションは『actions』を使った場合の説明となっています。
マニュアルを読むときは、このことを意識しながら見ないと私みたいに苦労します・・・

zistaさんの『CakePHPのACLを理解する(Authコンポーネントから探る) その2』がとても参考になりました。詳しく知りたい方は、是非一度ご覧ください。


3・AROの作成

次は、AROを作成していきます。
グループとユーザーをACLに関連付け、自動的にarosテーブルにも登録する処理を作成します。
AclBehaviorを使用した以下のコードをGroupモデルとUserモデルに書くことによって、グループやユーザーを登録すると自動的にarosテーブルにも登録してくれるようになります。

▼Userモデル

class User extends AppModel {
    public $name   = 'User';
    public $actsAs = array('Acl' => 'requester');

    public $belongsTo = array(
        'Group' => array(
            'className' => 'Group',
                'foreignKey' => 'group_id',
                'conditions' => '',
                'fields'     => '',
                'order'      => ''
            )
        );

    public $hasMany = array(
        'Post' => array(
            'className'    => 'Post',
            'foreignKey'   => 'user_id',
            'dependent'    => false,
            'conditions'   => '',
            'fields'       => '',
            'order'        => '',
            'limit'        => '',
            'offset'       => '',
            'exclusive'    => '',
            'finderQuery'  => '',
            'counterQuery' => ''
        )
    );

    /* 以下を追加
    --------------------------------- */
    // モデルとACLテーブルを自動的に結びつける処理
    public function parentNode() {
        if (!$this->id && empty($this->data)) return null;

        $data = $this->data;

        if (empty($this->data)) $data = $this->read();

        if (!$data['User']['group_id']) {
            return null;
        } else {
            return array('Group' => array('id' => $data['User']['group_id']));
        }
    }

    // ユーザーの所属するグループが変更された時の処理
    public function afterSave($created) {
        if (!$created) {
            $parent = $this->parentNode();
            $parent = $this->node($parent);

            $node = $this->node();

            $aro = $node[0];
            $aro['Aro']['parent_id'] = $parent[0]['Aro']['id'];
            $this->Aro->save($aro);
        }
    }
}

▼Groupモデル

class Group extends AppModel {
    public $name   = 'Group';
    public $actsAs = array('Acl' => array('requester'));

    public $hasMany = array(
        'User' => array(
            'className'    => 'User',
            'foreignKey'   => 'group_id',
            'dependent'    => false,
            'conditions'   => '',
            'fields'       => '',
            'order'        => '',
            'limit'        => '',
            'offset'       => '',
            'exclusive'    => '',
            'finderQuery'  => '',
            'counterQuery' => ''
        )
    );

    /* 以下を追加
    --------------------------------- */
    // モデルとACLテーブルを自動的に結びつける処理
    public function parentNode() {
        return null;
    }
}

これで、自動的にarosテーブルにも登録されるようになりましたので、グループとユーザーを登録するアクションを作成し登録していきます。
管理者(administrators)、運営者(managers)、利用者(users)の3つのグループとそれぞれのグループに所属するユーザーを登録したら次に進みます。


4・ACOの作成

ACOの作成ですが、マニュアルに便利なコードがあったので、それをそのまま使用したいと思います。
以下がコードです。

▼ACOの作成を自動化するコード

// ACOの作成
public function build_acl() {
    if (!Configure::read('debug')) return $this->_stop();

    $log  = array();
    $aco  =& $this->Acl->Aco;

    $root = $aco->node('controllers');

    if (!$root) {
        $aco->create(array('parent_id' => null, 'model' => null, 'alias' => 'controllers'));
        $root = $aco->save();
        $root['Aco']['id'] = $aco->id;
        $log[] = 'Created Aco node for controllers';
    } else {
        $root = $root[0];
    }

    App::import('Core', 'File');
        $Controllers = Configure::listObjects('controller');
        $appIndex = array_search('App', $Controllers);

        if ($appIndex !== false ) unset($Controllers[$appIndex]);

        $baseMethods = get_class_methods('Controller');
        $baseMethods[] = 'buildAcl';
        $Plugins = $this->_getPluginControllerNames();
        $Controllers = array_merge($Controllers, $Plugins);

        // look at each controller in app/controllers
        foreach ($Controllers as $ctrlName) {
            $methods = $this->_getClassMethods($this->_getPluginControllerPath($ctrlName));

            // Do all Plugins First
            if ($this->_isPlugin($ctrlName)) {
                $pluginNode = $aco->node('controllers/'.$this->_getPluginName($ctrlName));

                if (!$pluginNode) {
                    $aco->create(array('parent_id' => $root['Aco']['id'], 'model' => null, 'alias' => $this->_getPluginName($ctrlName)));
                    $pluginNode = $aco->save();
                    $pluginNode['Aco']['id'] = $aco->id;
                    $log[] = 'Acoを作成しました ' . $this->_getPluginName($ctrlName) . ' Plugin';
                }
            }

            // find / make controller node
            $controllerNode = $aco->node('controllers/'.$ctrlName);

            if (!$controllerNode) {
                if ($this->_isPlugin($ctrlName)) {
                    $pluginNode = $aco->node('controllers/' . $this->_getPluginName($ctrlName));
                    $aco->create(array('parent_id' => $pluginNode['0']['Aco']['id'], 'model' => null, 'alias' => $this->_getPluginControllerName($ctrlName)));
                    $controllerNode = $aco->save();
                    $controllerNode['Aco']['id'] = $aco->id;
                    $log[] = 'Created Aco node for ' . $this->_getPluginControllerName($ctrlName) . ' ' . $this->_getPluginName($ctrlName) . ' Plugin Controller';
                } else {
                    $aco->create(array('parent_id' => $root['Aco']['id'], 'model' => null, 'alias' => $ctrlName));
                    $controllerNode = $aco->save();
                    $controllerNode['Aco']['id'] = $aco->id;
                    $log[] = 'Created Aco node for ' . $ctrlName;
                }
            } else {
                $controllerNode = $controllerNode[0];
            }

            //clean the methods. to remove those in Controller and private actions.
            foreach ($methods as $k => $method) {
                if (strpos($method, '_', 0) === 0) {
                    unset($methods[$k]);
                    continue;
                }

                if (in_array($method, $baseMethods)) {
                    unset($methods[$k]);
                    continue;
                }

                $methodNode = $aco->node('controllers/'.$ctrlName.'/'.$method);

                if (!$methodNode) {
                    $aco->create(array('parent_id' => $controllerNode['Aco']['id'], 'model' => null, 'alias' => $method));
                    $methodNode = $aco->save();
                    $log[] = 'Created Aco node for '. $method;
                }
            }
        }

        if(count($log)>0) $this->set('logs', $log);
    }

    private function _getClassMethods($ctrlName = null) {
        App::import('Controller', $ctrlName);

        if (strlen(strstr($ctrlName, '.')) > 0) {
            // plugin's controller
            $num = strpos($ctrlName, '.');
            $ctrlName = substr($ctrlName, $num+1);
        }

        $ctrlclass = $ctrlName . 'Controller';

        return get_class_methods($ctrlclass);
    }

    private function _isPlugin($ctrlName = null) {
        $arr = String::tokenize($ctrlName, '/');

        if (count($arr) > 1) {
            return true;
        } else {
            return false;
        }
    }

    private function _getPluginControllerPath($ctrlName = null) {
        $arr = String::tokenize($ctrlName, '/');

        if (count($arr) == 2) {
            return $arr[0] . '.' . $arr[1];
        } else {
            return $arr[0];
        }
    }

    private function _getPluginName($ctrlName = null) {
        $arr = String::tokenize($ctrlName, '/');

        if (count($arr) == 2) {
            return $arr[0];
        } else {
            return false;
        }
    }

    private function _getPluginControllerName($ctrlName = null) {
        $arr = String::tokenize($ctrlName, '/');

        if (count($arr) == 2) {
            return $arr[1];
        } else {
            return false;
        }
    }

    /**
     * Get the names of the plugin controllers ...
     *
     * This function will get an array of the plugin controller names, and
     * also makes sure the controllers are available for us to get the
     * method names by doing an App::import for each plugin controller.
     *
     * @return array of plugin names.
     *
     */
    private function _getPluginControllerNames() {
        App::import('Core', 'File', 'Folder');

        $paths = Configure::getInstance();
        $folder =& new Folder();
        $folder->cd(APP . 'plugins');

        // Get the list of plugins
        $Plugins = $folder->read();
        $Plugins = $Plugins[0];
        $arr = array();

        // Loop through the plugins
        foreach($Plugins as $pluginName) {
            // Change directory to the plugin
            $didCD = $folder->cd(APP . 'plugins'. DS . $pluginName . DS . 'controllers');

            // Get a list of the files that have a file name that ends
            // with controller.php
            $files = $folder->findRecursive('.*_controller\.php');

            // Loop through the controllers we found in the plugins directory
            foreach($files as $fileName) {
                // Get the base file name
                $file = basename($fileName);

                // Get the controller name
                $file = Inflector::camelize(substr($file, 0, strlen($file)-strlen('_controller.php')));

                if (!preg_match('/^'. Inflector::humanize($pluginName). 'App/', $file)) {
                    if (!App::import('Controller', $pluginName.'.'.$file)) {
                        debug('Error importing '.$file.' for plugin '.$pluginName);
                    } else {
                        // Now prepend the Plugin name ...
                        // This is required to allow us to fetch the method names.
                        $arr[] = Inflector::humanize($pluginName) . "/" . $file;
                    }
                }
            }
        }

        return $arr;
    }

これをアクションとして実行するとACOが自動的に作成されます。

実行するアクションは『build_acl』なので、GroupsControllerのアクションにした場合は『http://ドメイン/groups/build_acl』といった感じで実行します。

ACOの作成が終了したら、パーミッションの設定を行っていきます。


5・パーミッションを設定する

ここまででACLを使用する準備が完了したので、実際にパーミッションを設定していきます。
今回のケースでは、『$this->Auth->authorize』に『actions』を指定したので、アクションごとにパーミッションを設定することになります。

設定するパーミッションは、

管理者(administrators)グループには、GroupsController・UsersController・PostsControllerの全てのアクションに対してアクセスを許可。

運営者(managers)グループには、UsersControllerのlogin・logoutアクションとPostsControllerの全てのアクションに対してアクセスを許可。

利用者(users)グループには、UsersControllerのlogin・logoutアクションとPostsControllerのindex・show・addアクションに対してのアクセスを許可

と、します。

また、各コントローラーのアクションは、

GroupsControllerにはindex・show・add・delete・build_aclアクション。

UsersControllerにはlogin・logout・index・show・add・deleteアクション。

PostsControllerにはindex・show・add・deleteアクション。

があるものとします。

パーミッションの設定は、『$this->Acl->allow($aroAlias, $acoAlias)』『$this->Acl->deny($aroAlias, $acoAlias)』で行っていきます。

▼パーミッションを設定する

// パーミッションの設定
public function build_parmition() {
    // 管理者のパーミッションを設定
    $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 1), 'controllers');
    $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 1), 'controllers/groups/index');
    $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 1), 'controllers/groups/show');
    $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 1), 'controllers/groups/add');
    $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 1), 'controllers/groups/delete');
    $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 1), 'controllers/groups/build_acl');
    $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 1), 'controllers/users/login');
    $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 1), 'controllers/users/logout');
    $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 1), 'controllers/users/index');
    $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 1), 'controllers/users/show');
    $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 1), 'controllers/users/add');
    $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 1), 'controllers/users/delete');
    $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 1), 'controllers/posts/index');
    $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 1), 'controllers/posts/show');
    $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 1), 'controllers/posts/add');
    $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 1), 'controllers/posts/delete');

    // 運営者のパーミッションを設定
    $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 2), 'controllers');
    $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 2), 'controllers/groups/index');
    $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 2), 'controllers/groups/show');
    $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 2), 'controllers/groups/add');
    $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 2), 'controllers/groups/delete');
    $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 2), 'controllers/groups/build_acl');
    $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 2), 'controllers/users/login');
    $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 2), 'controllers/users/logout');
    $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 2), 'controllers/users/index');
    $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 2), 'controllers/users/show');
    $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 2), 'controllers/users/add');
    $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 2), 'controllers/users/delete');
    $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 2), 'controllers/posts/index');
    $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 2), 'controllers/posts/show');
    $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 2), 'controllers/posts/add');
    $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 2), 'controllers/posts/delete');

    // 利用者のパーミッションを設定
    $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 3), 'controllers');
    $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 3), 'controllers/groups/index');
    $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 3), 'controllers/groups/show');
    $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 3), 'controllers/groups/add');
    $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 3), 'controllers/groups/delete');
    $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 3), 'controllers/groups/build_acl');
    $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 3), 'controllers/users/login');
    $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 3), 'controllers/users/logout');
    $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 3), 'controllers/users/index');
    $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 3), 'controllers/users/show');
    $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 3), 'controllers/users/add');
    $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 3), 'controllers/users/delete');
    $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 3), 'controllers/posts/index');
    $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 3), 'controllers/posts/show');
    $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 3), 'controllers/posts/add');
    $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 3), 'controllers/posts/delete');
}

グループごとにパーミッションを設定するので、aroAliasにはGroupモデルを使用しています。
上記のアクションを適当なコントローラーに配置し、実行します。
これでパーミッションの設定は完了です。

『build_parmition』アクションは、パーミッション設定後は削除してしまってオッケーです。


6・確認

AppControllerの『$this->Auth->allow(‘*’);』をコメントアウトか削除し、動作確認を行います。

ちゃんと動作していれば、リンクからの推移なら推移元のページにリダイレクト、直接URLを入力ならトップページにリダイレクトされると思います。


7・エラーメッセージを出力する場合

エラーメッセージを出力したい場合は、『$this->Auth->authError』にエラーメッセージを指定します。

▼エラーメッセージの例

$this->Auth->authError  = 'アクセス権限がありません。';

あとは、『$session->flash』でエラーメッセージをビューに表示します。

▼エラーメッセージの例

echo $session->flash('auth');

基本的なACLの使い方は以上です。
あとは、パーミッションを簡単に変更するアクションを作成したりして、使いやすいようにしていくだけです。

ACLコンポーネントを使用する上で、私が思う注意点は『$this->Auth->authorize』に指定した値でパーミッションチェックの方法が変わるという事だと思います。
私のように、マニュアルや他の方のブログなど読んで混乱する人は、この事を強く意識した方が理解への近道になるかもしれませんね。

さすがに長文になりました・・・
最後まで読んでくださった方。ありがとうございました。

CakePHP ブラウザを閉じたらSESSIONを破棄

CakePHPで『ブラウザを閉じるとSESSIONを破棄』をしたかったので、メモも兼ねて紹介します。

手っ取り早い方法としては、『/app/config/core.php』の200行目付近の『Security.level』を『high』に変更します。

『Security.level』に『high』を指定していると、『/cake/libs/cake_session.php』の480行目付近で

if ($this->security == 'high') {
    $this->cookieLifeTime = 0;
} else {
    $this->cookieLifeTime = Configure::read('Session.timeout') * (Security::inactiveMins() * 60);
}

となり、『cookie_lifetime』に0が指定されます。
さらに、『Security.level』が『high』だと、リクエストごとにSESSION ID が作られるようになります。


しかし、『Security.level』が『high』だと色々と不都合があったりしますよね・・・
『Security.level』が『medium』のまま『ブラウザを閉じるとSESSIONを破棄』する方法は以下のようにして行います。

まずは、『/app/config/core.php』の127行目付近の『Session.save』を好きな名前に変更します。

▼『Session.save』を変更

// ▼変更前
// Configure::write('Session.save', 'php');

// ▼変更後
Configure::write('Session.save', 'new_session');

次に、『Session.save』で指定した名前のPHPファイルを『/app/config/』に作成します。
上記の例だと、『new_session.php』となります。
『new_session.php』の内容は、『/cake/libs/cake_session.php』の487行目付近のswitch文で色々書かれている『ini_set』関数たちをコピーしてきます。

▼『new_session.php』の内容

<?php
    ini_set('session.use_trans_sid', 0);
    ini_set('session.name', Configure::read('Session.cookie'));

    // ↓cookie_lifetimeに0を指定する
    ini_set('session.cookie_lifetime', 0);

    ini_set('session.cookie_path', $this->path);
?>

これで、『Security.level』が『medium』のまま『ブラウザを閉じるとSESSIONを破棄』することができます。

注意点は『Session.save』に[cake][database][php][cache]などを指定しないことです。
[cake][database][php][cache]のいずれかを指定してしまうと、
『/cake/libs/cake_session.php』の487行目付近のswitch文のcaseに該当してしまうので、自分で作成したファイルを読み込みません。