2011年6月アーカイブ

また、フラグ用の変数はP62,63の予約変数で確認できます。スライドには、確認するためのテンプレートモジュールについて言及しています(分からない人は使わないでも問題ありません)

いや、本が手元にないんですよ、という人のために、予約変数はどこに書いてあるか、というご紹介。

mtディレクトリの以下のlib/MT/ArchiveType/以下の各ファイルにアーカイブタイプ毎に指定されています。

#  lib/MT/ArchiveType/Category.pm

sub template_params {
    return {
        archive_class              => "category-archive",
        category_archive           => 1,
        archive_template           => 1,
        archive_listing            => 1,
        'module_category_archives' => 1,
    };
}

ドキュメントにもあります(下記のページからPDFで一覧表があります)。

でも、でもでも、ソース見ろってのも違うと思うし上記のPDFだってわかりやすいとも言いがたい、というか、今変数がどうなっているかを確認したい時ってないですか? layout-wtt みたいな変数もあるし。そこでプラグイン。

カテゴリーアーカイブのプレビュー画面に <MTVarDump> と入れてプレビューするとこんな感じで表示してくれます。エスケープ済みの文字列をpreタグで囲って表示してくれます。

$VAR1 = {
          'category_entry_listing' => 1,
          'preview_template' => 1,
          'archive_class' => 'category-archive',
          'archive_template' => 1,
          'page_layout' => 'layout-wtt',
          'page_columns' => 3,
          'archive_listing' => 1,
          'category_archive' => 1,
          'module_category_archives' => 1
        };

他にもMTCookieDump、MTQueryDump、MTEnvDumpってタグが用意されています。開発・構築のお伴にどうぞ。

タイトルの『"もっと"よくわかる条件分岐。』の話じゃなくなっちゃいましたが、ま、いっか。

ちぃとばかし気になったので反応しておきます。

以前の記事では、CMSとしてはMovable Typeが優れていると書きましたが、WordPressも全く遜色の無い機能を備えていることに気づきました。

それがカスタムポスト機能です。

これは何かというと、Movable Typeで言うところの「ブログ記事」や「ウェブページ」といった投稿の種類を独自に拡張できる機能です。

たとえば不動産物件を管理するサイトであれば「不動産物件」であったり、旅館のサイトであれば「お部屋」といったように、自由に投稿の種類を追加することができます。 そのため、サイトの新着情報は新着情報として、不動産物件は不動産物件として別々に管理することができるのです。

Movable Typeも実はカスタムポスト機能と言うべき仕組みを持っているのですが(名前はついていませんけど)そのあたりのドキュメントや事例が少ないんでしょうね。少々面倒なことは否定しませんが。

Movable Typeのカスタムポストが少々面倒なのは、MTは静的生成できるからという面もあると思います。独自オブジェクトを作成してリスティングスクリーンを作成、投稿画面を作成して権限を設定、それからArchiveTypeを登録してやらないといけない。

ちなみに、ドキュメントがない、というのは4-5の頃の話で現在はGitHubに詳細なドキュメントがあります。周知不足ですねきっと。

ギークなエンジニアなら、その程度はプラグインを作ってお茶の子さいさいなのでしょうが、わざわざそのためにプラグインを作るコストを考えると非常に手間がかかります。

こいつ↓を使うとプログラムを1行も書かずにカスタムポストが実現出来ます。

有償じゃねーか、って言われそうですが、弊社案件では原価かかんないわけですからWordpressを選択する理由にならないですし。

2つ以上の言語習得が必要なMovable Type

しかしながら、逆に複雑なこと・・・例えば「多階層のカテゴリをサイドバーにメニューの一覧として出力したい」等をMTMLで実現しようとすると、プログラミング言語よりも圧倒的に制限された仕様内で試行錯誤を行わなくてはならず、非常に手間がかかります。

できる...よね? MTMLだけで。そう複雑とは思えないですが。

まぁ、この↓ページにサンプルがないのが悪い。

問題はこの部分↓。

さらにテンプレートにはPerlは利用できませんから、プラグインで開発する独自の機能はPerlでMTMLとして記述できるように開発し、MTMLとしてテンプレートに記述するという、2段階の手順を取る必要があります。

これはもう、考え方としか言いようがないですが、プレゼンテーションとロジックが同じ言語で書かれているってのは、テンプレートの意味があるのかないのかわからないんです(分ける必要がないんじゃないかと)。PHPは言語じゃなくてタグだ(<?php ?>)なんて言う人がいたりする? けど、そうでしょうか?

そもそも、同じ人が書くことを前提にするなら学習コストが2倍みたいな話になってくるわけで、ロジックを書く人間とプレゼンテーションを書く人間をきっちり分けちゃえば片方でいいですよね。MTML自体にロジックを書くことも結構できるわけですが、それにしてもプレゼンテーション担当はPerl(PHP)やらなくでも良いわけで。

これまでにもMTCakeとかMTPlugin Wordpressってのを書いたりしたんですが、やっぱりきっちり分けることで分業効率とメンテナンス性が向上しないですかね(すると思いますよ)。

Cakeのテンプレートの例を見ていただきたいのですが(WordPressの話ではないのですが)、ループの部分は <?php foreach ($posts as $post): ?>~ <?php endforeach; ?>、タイトルを取り出すのは配列 $postのキー[Post]のさらにキー[title]をPHPのコードで取得するわけです。これはテンプレートというよりもう、何て言うかプログラムです。

MTCakeを利用した方のテンプレートは、きっちり分かれてますよね。

ビュー

<!-- File: /app/controllers/posts_controller.php -->
<?php
class PostsController extends AppController {
    var $name = 'Posts';
    function index() {
        $ctx = $this->ctx;
        $ctx->__stash[ 'vars' ][ 'page_title' ] = 'Blog posts';
        $ctx->stash( 'Post', $this->Post->find( 'all' ) );
    }
}
?>

プレゼンテーション(テンプレート)

<!-- File: /app/views/posts/index.ctp -->
<h1><mt:var name="page_title" escape="html"></h1>
<!-- Here is where we loop through our posts array, printing out post info -->
<mt:cake:loop model="Post">
<mt:ignore>
    or <mt:cake:loop model="Post" stash="posts">
</mt:ignore>
<mt:if name="__first__">
<table>
    <tr>
        <th>Id</th>
        <th>Title</th>
        <th>Created</th>
    </tr>
</mt:if>
    <tr>
        <td><mt:var name="id"></td>
        <td>
            <a href="./view/<mt:var name="id">"><mt:var name="title" escape="html"></a>
        </td>
        <td><mt:var name="created"></td>
    </tr>
<mt:if name="__last__">
</table>
</mt:if>
</mt:cake:loop>

DynamicMTML(WordPressプラグイン)やMTCakeではテンプレートでPHPも書けますが、これは敢えて書かない方向性で実装していくほうが望ましいと思います。ここら辺はもう宗教思想みたいなところにいっちゃいますけどね。

オブジェクト指向のPerlが敷居が高いってのは...この辺は好みの話になると思いますが、MTの場合はPHPでもプラグイン書けます。DynamicMTMLを使うことで静的ファイルに対する処理のプラグインもPHPで書けるようになります。テンプレートにPerlやPHPでロジックを書かない、これはもう、敢えて書かない方向でいくべきなんじゃないかな、と思うのでした。

最後に、「有償」ってのはどうなんだって話ですが、有償ってのは「責任」と言い換えることができると思います。先日来立て続けにセキュリティアップデートのリリースされたMTですが、WordPressにもこんなのがあって(→「WordPressのプラグインに悪質なコードが混入」)何かあったときに、「オラどうすんだよこの野郎」って言えるかどうかってのが有償と無償の境目なんじゃないかと。

あとね、結局あの悪評高い? 再構築の件ですが、規模が一定以上だと静的配信できないとやっぱ苦しいです。自治体・官公庁に動的配信ベースのものが受け入れられるかというと、やっぱ難しいです(無いとはいいませんが)。

えー、まぁ、何というかセキュリティアップデート続きのおかげもあって今日も仕事してます。ええ。最初は行く予定だったんですけどね。

さて、ちょっと切り替えて真面目に行きます。

仮にテスト・ドリブンの開発を行っていたとしても、バグやセキュリティ脆弱性が見つかることはある程度以上のソフトウェアにとっては現状のところ避けられない問題ではないかと思います。ソフトウェアのコードを読み進むことで開発者が想定していない問題が見つかったりすることは実際にある訳で、最終的にはコードレビューこそが最終的な品質管理だと言えるのではないでしょうか(結局のところ開発者の予期しなかった問題を指摘するのはアナログな手法でコードを読めるテスターの存在頼りだったりします)。

現在、MT開発チームはセキュリティ強化月間らしいです。コードレビューをしましょうよってのは僕も思って提案してたから、良い傾向だと思います。

単にコードレビューといっても、やはり何らかの目的と視点を持ってみていかなければ脆弱性やバグは発見できないことがあります。レビューやテストに実は一番大切なのは「想像力」ではないかと思う今日この頃です。

簡単な例題でコードレビューの視点を考える

例題として「ウェブページに対する操作を行う以下のメソッドのコードレビューでは何を想像すれば良いか」というお題を考えてみましょう。

リクエストの形:
__mode=some_action&blog_id=1&id=100

問題のコード

sub some_action{
    my $app= shift;
    require MT::Page;
    my $page = MT::Page->load( $app->param( 'id' ) );
    if (! $page ) {
        return $app->translate( 'Invalid Page ID:[_1].', $app->param( 'id' ) );
    }
    my $perm = $app->user->is_superuser;
    if (! $perm ) {
        $perm = $app->user->permissions( $app->blog->id )->can_administer_website;
        $perm = $app->user->permissions( $app->blog->id )->can_administer_blog unless $perm;
        $perm = $app->user->permissions( $app->blog->id )->can_manage_pages unless $perm;
    }
    if (! $perm ) {
        return $app->translate( 'Permission denied.' );
    }
    # この後、$pageへの処理
}

例外として想像すべきケースは例えば以下のようなものです。

  1. パラメタidに数字以外のものが渡された場合
  2. パラメタidにpageオブジェクトではなくentryオブジェクトのidが渡された場合
  3. パラメタidが空の場合
  4. パラメタidに現在のblogに属さないentry/pageのidが渡された場合

1.のケースですが、数字以外のものが渡された場合、pageはロードされませんから、 return $app->translate( 'Invalid Page ID:[_1].', $app->param( 'id' ) ); が呼ばれます。return している際にパラメタ id を付けてメッセージを返していますが、idにJavaScriptが渡されたらどうなるでしょうか。このコードにはXSS脆弱性があります。

2.のケースでは、$pageに entryオブジェクトがそのまま格納されます(IDのみを指定した場合、classに関わらずロードされます)。ところが、この後の権限チェックではcan_manage_pagesで権限をチェックしています。このコードには権限のないオブジェクトに対する操作が可能な脆弱性があります。

3.のケースですが、my $page = MT::Page->load();となりますが、$pageには条件指定なしで呼び出されたオブジェクトの先頭1件が格納されてしまいます。操作の内容によりますが、これはバグである可能性があります。

4.のケースですが、entry_idが100のページがblog_idが2のブログに属していた場合どうなるでしょうか。 blog_idが1のブログに対してウェブページの管理権限を持っているかどうかのチェックを行っていますが、blog_idが2のブログの権限チェックはしていません。blog_idが2のブログにこのユーザーが権限を持っていない場合、このコードはそのまま実行されてしまいます。このコードには権限のないオブジェクトに対する操作が可能な脆弱性があります。

その他にも、オブジェクトに対する操作を伴うメソッドには magic_token によるチェック、及びcms_save_permission_filterやcms_delete_permission_filter等操作に応じたコールバックをコールして権限チェックを行わなければなりません。

例題のコードの修正すべき点は以下の通りです。

  1. idがなければその時点でエラーを返す(エラーメッセージに受け取ったパラメタを含める場合はencode_html)を忘れない(もしくはテンプレート側でエスケープする)。
  2. idが数字かどうかをチェックする(数字でなければエラー)、またはエラーメッセージにユーザーが指定可能なパラメタを含める場合、MT::Util::encode_htmlを通して返す(ettor.tmplの方でエラー出力のタグにescape="html"が含まれている場合は$app->errorメソッドを使うことでも良いですが、現状のMTではコード側でエスケープした方が安全)。
  3. 読み込んだ$pageの$page->classをチェックして、page以外であればエラーを返す。もしくはロードメソッドでclass=>'page'を指定して、オブジェクトがpageであることを保証する。
  4. $app->user->permissions()に渡すBlogIDを $app->blog->id ではなく、 $page->blog_id とする(もしくは$app->blog->id と $page->blog_id が異なる場合にエラーを返す*システムスコープで実行される可能性があるケースでは前者)。
  5. この後オブジェクトに何らかの操作を行うメソッドの場合は、magic_tokenのチェックを事前に入れる
  6. 保存、削除を伴うメソッドの場合、cms_save_permission_filter.pageコールバックをコールして、戻り値がない場合にエラーを返す(必要に応じてcms_save_filter.page/cms_pre_save.page/cms_post_save.pageをコールする)

修正適用後のコード(例)

sub some_action {
    my $app = shift;
    $app->validate_magic or
        return $app->trans_error( 'Permission denied.' );
    require MT::Page;
    my $page;
    if (! $app->param( 'id' ) ||
        (! $page = MT::Page->load( $app->param( 'id' ) ) ) {
        return $app->trans_error( 'Invalid Page ID:\'[_1]\'', MT::Util::encode_html( $app->param( 'id' ) ) );
    }
    if ( $page->class ne 'page' ) {
        return $app->trans_error( 'Invalid Class:\'[_1]\'', $page->class );
    }

    # permission check case 1

    if ( $app->blog->id != $page->blog_id ) {
        return $app->trans_error( 'Invalid BlogID:\'[_1]\'', MT::Util::encode_html( $app->blog->id ) );
    }    
    if (! $app->can_do( 'manage_pages' ) ) {
        return $app->trans_error( 'Permission denied.' );
    }
    
    # or case 2
    
    my $perm = $app->user->is_superuser; 
    if (! $perm ) {
        $perm = $app->user->permissions( $page->blog_id )->can_administer_website;
        $perm = $app->user->permissions( $page->blog_id )->can_administer_blog unless $perm;
        $perm = $app->user->permissions( $page->blog_id )->can_manage_pages unless $perm;
    }
    if (! $perm ) {
        return $app->trans_error( 'Permission denied.' );
    }

    if (! $app->run_callbacks( 'cms_save_permission_filter.page' $app, $page ) {
        return $app->trans_error( 'Permission denied.' );
    }
    # Do Something.
}

こんなことが本当にあるの? ってか、でも結構あるんですよね。だいぶ潰れてるとは思いますが、セキュリティ強化月間で徹底的にやっていただきたいものです。もちろん、弊社もセキュリティ強化月間です。

さて、実はもう一つ紹介したい例題があるのですが、さすがにちょっとここには書けないです。世の中には危ないコードがいっぱいありますからね。またいずれ気が変わったら書くかもしれません。

Facebook

Twitter

このアーカイブについて

このページには、2011年6月に書かれたブログ記事が新しい順に公開されています。

前のアーカイブは2011年3月です。

次のアーカイブは2011年7月です。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。

Powered by Movable Type 6.2.6