[Mashup]jQueryプラグインで無限スクロールページをつくる(NAVERまとめのマッシュアップ)

今回使うjQueryプラグイン

Wookmark

使用方法はこちらです。

このプラグインを利用することでグリッドレイアウトをベースとした、無限スクロールのページが作成できます。

スクリーンショット 2013-05-30 23.57.07

サンプル

サンプルはこちら

NAVERまとめのページから、画像とタイトルとURLをスクレイピングで取得。

スクロールに応じて、非同期に画像ファイル等を取得して、どんどん表示してきます。

ページングがだるい、一つのページでより多くのコンテンツをみたいという方は是非。

実行環境/言語

実装には以下のものを利用しました。

言語:PHP
フレームワーク:Symfony , jQuery, Wookmark, htmlSQL, Snoopy

htmlSQL

htmlSQLはSQLベースでHTMLの要素を取得できるライブラリです。スクレイピングをするさいに利用しました。
慣れ親しんだSQLベースで要素の取得ができる便利なライブラリです。

GitHub htmlSQL

以下のような記法で配列形式のデータが取得可能となります。

SELECT (取得したい属性) FROM (タグ) WHERE $(属性) == (属性の値);

[Symfony]Cookieにsecure属性を付与する

Symfonyで設定する方法

個別に実装を書く必要はありません。factories.ymlに以下を追加するだけです。

all:
(省略)
  storage:
    class: sfSessionStorage
    param:
      session_cookie_secure: true
(省略)

以前の記事でも紹介しましたが、Javaの場合は以下のサイトをご参照ください。
[Java]SSL環境下でCookieにsecure属性を付与する
※この記事はTomcatのバージョンが低く – ver5 – 、SSLアクセレレータ配下でデフォルトのオプション(server.xml)で設定できない場合の実装方法です。
Tomcatのバージョンが高い – ver6以降 – 場合はSSLアクセレレータ配下で通信がHTTP(HTTPSでない)の場合でもデフォルトのオプションでsecure属性をつけてくれます。

すごく簡単。

[Symfony]jsonレスポンスを返す設定方法

外部API

最近では、WebサービスとしてAPIが多く提供されています(Google Map API…)。そのコンテンツを充実させるのに注力するために、ここでは外部APIとしてよくある「json」レスポンスを返す方法について紹介します。

例)
Google Map API
ATND API

jsonレスポンスを返すための設定

この記事はPHPとSymfonyをベースとした実装方法を紹介していきます。従ってPHPおよびSymfonyの基礎知識はある前提で紹介していきます。

まず、レスポンスとしてjsonを返すので、アプリケーションのcontent-Typeをapplication/jsonにします。また、HTMLとしてのレスポンスを返す必要がないのでhas_layoutもfalseとします。それらの設定はview.ymlにて設定可能です。

default:
  http_metas:
    content-type: application/json; charset=UTF-8
#   通常の場合
#   content-type: text/html

...

  has_layout: false
# 通常の場合
# has_layout: true
  layout: layout

[株]HumbleFinanceを使ってみた

作ったもの

Go To Chart Navi

Humble Finance

チャートをHTML5で描画してくれるJavascriptフレームワークです。これを使ってWEBページを作ってみます。

Humble Software Development

他に使ったライブラリとか

CHAPI! – 株価API
株価を検索するためのフリーAPIです。IKACHI Projectなる団体が管理しているらいです。
ここが提供する四本値データと銘柄データを利用してチャート描画に必要なデータを収集しています。

IKACHI Project

Prototype
Humble Financeの動作条件として必須なJavascriptフレームワーク。

Prototype

JQuery
Javascriptフレームワーク。Prototypeと文法上競合するので記述方法に気をつけます。

JQuery

KickStart
cssフレームワーク。

KickStart

さあ、どうぞ

PHP & Symfonyで実装しました。

Go To Chart Navi

[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をキーとし、その値を取得するというロジックである。これと、入力された文字列を比較し、認証を行う。

[Symfony]テンプレートにアクションで定義した変数を埋め込む方法

テンプレートにアクションで定義した変数を渡すには

テンプレート(モジュールではなくアプリケーション)でアクションで定義した変数を呼び出したい場面が多々あるが、それにはslotを使う。

方法

大まか以下の流れで実現可能。
①アクションに変数を定義
②モジュールのテンプレートでslotを定義
③アプリケーションのテンプレートでslot呼び出し

順々に、
①アクションクラスにhogeという変数名のパラメータを定義します。

class XXXXaction extends sfActions {
	public function executeIndex() {
		$this->hoge = 'technology.';
	}
}

②モジュールのテンプレートでは(XXXSuccess.php等)では普通に呼べるのでここにslotをまず埋める。

<?php slot('sample', $hoge);

③アプリケーションのテンプレートでslotを呼び出します。

<?php if (has_slot('sample')): ?>
<?php include_slot('sample') ?>  // <- technology.が表示されます。
<?php else: ?>
スロットが定義されていません。
<?php endif; ?>

これで、actionで定義した変数をlayout.php等で利用可能です。もちろん、layout.phpが読み込んでるパーシャル等でも。

[Symfony]対顧機能と社内機能のアクセス制限機能について

概要

フロントエンド向けのアプリケーションとバックエンド向けのアプリケーションを作成した場合、フロントエンドはWANからのLB経由でのアクセスのみ、バックエンドのアプリケーションは社内からのイントラネットでのアクセスのみと制限したいケースがある。その方法について簡単に。

解決方法

結論から言うと、Symfonyのディレクトリ構成を変えてしまえばよい。Symfonyの通常のアプリケーションはすべて、web下にある{アプリケーション名}.phpでのアクセスとなってしまう。

例えば、バックエンド(backend.php)に対してアクセス制限したい場合は、webと同列のbackendディレクトリを作成して、その下にbackend.phpを移動させてしまおう。そうすることで、LBはweb下にあるindex.phpへのアクセスのみを許可し、backend/backend.phpにはアクセスできない。逆に社内イントラネットからはbackend/backend.phpのアクセスだけを許可してやればよい。

[Symfony]ログ出力をカテゴリ別に分ける

概要

機能毎にログ出力を別ファイルに行うこと(※)を実現したい場合に、Symfony標準機能ではアプリケーションレベルで機能を分ける必要があります。
同一アプリケーション内でログ出力を分けたい場合には、独自の実装が必要となります。その方法について解決策を紹介します。

※一例として、同一アプリケーションでHTTPリクエストのログと外接システムとのログを分けたい場合

実現方法

同一アプリケーションで複数ログファイルを作成したい場合はsfAggregateLoggerを利用しますが、このクラスでは複数ログは出力できるものの、出力内容はほとんど同じになってしまい、(ログレベルの違いしか変更できない)少しカスタマイズが必要となります。

SymfonyでのログクラスはすべてsfLoggerという抽象クラスを継承しており、ログ出力に関してはdoLog関数に集約されています。
下記は、sfAggregateLoggerのdoLog関数です。

  /**
   * Logs a message.
   *
   * @param string $message   Message
   * @param string $priority  Message priority
   */
  protected function doLog($message, $priority)
  {
    foreach ($this->loggers as $logger)
    {
      $logger->log($message, $priority);
    }
  }

ここで登録されているログクラスについてすべて出力してしまっているので、ここに条件を入れれば問題として解決しそうだということが分かります。
したがって、方針としてはsfAggregateLoggerを拡張したmyAggregateLoggerを作成し、またmyAggregateLoggerに登録するログクラスをsfFileLoggerを拡張したmyFileLoggerを登録することとします。そしてmyFileLoggerに同一インターフェースをもたせるためにmyInterfaceLoggerも作成します。

myInterfaceLoggerを作成する

<?php
/**
 * 本アプリケーションで利用するログが継承すべきインターフェース。
 *
 * @author Yusuke Iwasaki
 */
interface myInterfaceLogger {
    
    /**
     * 実装クラスのログのタイプを返す。
     * このログタイプにより、出力ログファイルを振り分けます。
     */
    public function getType();
}

myFileLoggerを作成する

<?php
/**
 * カスタムファイルロガー。
 *
 * @author Yusuke Iwasaki
 */
class myFileLogger extends sfFileLogger implements myInterfaceLogger {

    protected
        $log_type;

    public function initialize(sfEventDispatcher $dispatcher, $options = array()) {
        if (isset($options['log_type'])) {
            $this->log_type = $options['log_type'];
        }
        return parent::initialize($dispatcher, $options);
    }

    public function getType() {
        return $this->log_type;
    }
}

myAggregateLoggerを作成する

<?php
/**
 * カスタムアグレゲートロガー。
 *
 * @author Yusuke Iwasaki
 */
class myAggregateLogger extends sfAggregateLogger {
    /**
     * ログファイルの分散出力を行います。
     * @param type $message ロギング文言
     * @param type $type ログファイルタイプ(request, circumscription)
     * @param type $priority ログレベル
     */
     public function logging($message, $type, $priority = self::INFO) {
        foreach ($this->loggers as $logger) {
            if (method_exists($logger, 'getType')) {
                if ($logger->getType() === $type) {
                    $logger->log($message, $priority);
                }
            }
        }
    }
}

ここで注意すべきはinterfaceで定義したgetType関数を持つか持たないかを判別する条件式を入れることです。開発環境では、デバックをONにしている場合にsfWebDebugLoggerが入ってくるためです。

factories.ymlに作成したログを登録する

all:
  logger:
    class:   myAggregateLogger
    param:
      level:   info
      loggers: 
        api_logger:
          class: myFileLogger
          param:
            level: info
            file: %SF_LOG_DIR%/%SF_APP%_%SF_ENVIRONMENT%_request.log
            log_type: request
        request_logger:
          class: myFileLogger
          param:
            level: info
            file: %SF_LOG_DIR%/%SF_APP%_%SF_ENVIRONMENT%_circumscription.log
            log_type: circumscription

利用方法

通常のログクラスと同様に熱かってください。sfContextクラスからログ関数を取得して、通常ならば「info, …, log」関数を呼び出しましたが、独自で作成した「logging」を呼ぶように注意します。

[Symfony]フレームワークで提供されているCSRF機能について

WEBサイト作成にかかわるセキュリティ対策について

WEBサイト作成ではセキュリティを意識した実装を行う必要がある。セキュリティの種類によっては対応するレイヤー(フレームワーク・設定ファイル・実装・・・)が異なるので、整理・サイトに与える影響度・工数を見積もりながら対応を取捨選択する必要がある。いわゆる一般的なホームページ的サイト(GETメソッドで完結)では、あまりセキュリティを意識する必要はないが、エンドユーザの情報をサーバに送信するような機能を持つサイトはセキュリティに対して細心の注意が必要です。

CSRF(クロスサイトリクエストフォージェリ)

今回はCSRFにフォーカスをあて、一般的な「ワンタイムトークンの発行」について言及する。SymfonyというPHPフレームワークではCSRF対策がデフォルトで提供されている。ただ、特殊な画面構成をもつ画面を実装しようとしたときにCSRFトークンの発行の仕方に問題があったので自分で拡張した。

CSRFとは

sfFormのデフォルトの実装

  /**
   * Returns a CSRF token, given a secret.
   *
   * If you want to change the algorithm used to compute the token, you
   * can override this method.
   *
   * @param  string $secret The secret string to use (null to use the current secret)
   *
   * @return string A token string
   */
  public function getCSRFToken($secret = null)
  {
    if (null === $secret)
    {
      $secret = $this->localCSRFSecret ? $this->localCSRFSecret : self::$CSRFSecret;
    }

    return md5($secret.session_id().get_class($this));
  }

コード18行目でトークン発行が[秘密文字列]と[セッションID]と[クラス名]を連結してmd5エンコーディングしたものになっている。いわゆる一般的なフォーム画面では問題ないのだが、エンドユーザからの入力が複数画面にまたがる場合かつフォームクラスが複数画面で共通の場合に、どの画面もフレームワークによって発行されるトークンが同一となり、それを狙われた攻撃のリスクが高まる。また、ランダム性もないので自分で実装することにした。

sfFormのgetCSRFTokenをオーバーライドしたコードの一部

    /**
     * CSRFトークンを返す。
     * 
     * クラスで一意のトークンになるのを回避するために、時刻の要素を加味したメソッド。
     *
     * @param string $secret
     * @return striken A token string 
     */
    public function getCSRFToken($secret = null)
    {
        if (null === $secret)
        {
            $secret = $this->localCSRFSecret ? $this->localCSRFSecret : self::$CSRFSecret;
        }
        return md5($secret.session_id().get_class($this).time());
    }

コード18行目のtime関数を追加した。これで時刻によるランダム性により同一クラスでも発行されるトークンが毎回異なることとなる。
そこで問題になるのが、トークンを発行&確認するタイミングが異なるので一回発行したときのトークンを確認するときのために保持しておく必要がある。これはセッションに保存することとしよう。(セッションに保存したトークンは確認で利用したら削除する。)