tDiary: How to make plugin
Author: TADA Tadashi

# tDiary
プラグインの作り方

※プラグインの使い方については、HOWTO-use-plugin.htmlを参照してください。

プラグインはtDiary 1.3.1以降で使えるようになった、システムに機能を追加する仕組みです。プラグインはtDiary.orgから入手することができます。また、tDiaryフルセットを利用している場合には、misc/pluginディレクトリに単独に配布されているのと同等のものが含まれています。これらの.rbファイルを、ファイルごとインストール先にあるpluginディレクトリに移動することで、利用できるようになります。

プラグインにはさまざまなものがありますが、日記中に自動的に何かの文字列を埋め込んだり、特殊な処理をさせるのが主な目的です。また、特定のプラグインを作ることで、tDiaryのメッセージをカスタマイズすることもできます。

以下では、プラグインについてその「作り方」を説明します。なお、.rbファイルのことを「プラグインファイル」、実際に日記中で呼び出して使う機能(主にRubyのメソッド)のことを「プラグイン」と呼びます。ひとつのプラグインファイルは、複数のプラグインを含みます。

## プラグインの種類

プラグインは、Rubyのメソッドになっています。これらのメソッドはプラグインを実装するPluginクラスのメソッドとして読み込まれ、日記ファイル生成時の最後の段階で呼び出されます。プラグインメソッドは文字列を返し、それがそのまま日記に埋め込まれることになります。

制作側から見たプラグインは、3種類あるように見えます。ひとつはカスタマイズ系プラグインです。これらは日記の特定の場所にすでに埋め込まれていて、日記生成時に強制的に呼び出されます。デフォルトプラグインにおいてすでに定義されていて、同名のプラグインを再定義することで動作を変更することができるものです。各種メッセージや、日付・段落アンカー、HTMLヘッダを生成するプラグインがこれにあたります。

2種類目のプラグインは、完全にオリジナルのプラグインです。ヘッダやフッタ、日記本文に日記著者が意図的に記述することで呼び出されます。これらはデフォルトでは未定義で、既存のプラグインと名前がだぶらない限り好きな名称にできます。プラグイン集に収録されているプラグインは、たいていこれです。

3種類目のプラグインはコールバック系プラグインです。これは呼び出される場所が決まっている点でカスタマイズ系プラグインに似ていますが、メソッドにはなっておらず、文字列を返すブロックを追加していく形で定義します。更新時だけに呼び出されるプラグインや、日記本文の前後に呼び出されるプラグイン、HTMLヘッダを追加するプラグインが定義できます。

以降で、これらそれぞれのプラグインの作り方について説明します。

## カスタマイズ系プラグイン

日記本文に現れるメッセージや文字列の多くは、プラグインによって差し込まれています。それぞれのメッセージに対応するプラグインを再定義することで、メッセージをカスタマイズできるのです。プラグイン作成のうちもっとも簡単なこのカスタマイズをこれから見ていきます。

カスタマイズ可能な文字列は、デフォルトプラグインとしてあらかじめ以下のように定義されています。

    def no_diary; "#{@date.strftime( @conf.date_format )}の日記はありません。"; end
    def comment_today; '本日のツッコミ'; end
    def comment_total( total ); "(全#{total}件)"; end
    def comment_new; 'ツッコミを入れる'; end
    def comment_description; 'ツッコミ・コメントがあればどうぞ! E-mailアドレスは公開されません。'; end
    def comment_description_short; 'ツッコミ!!'; end
    def comment_name_label; 'お名前'; end
    def comment_name_label_short; '名前'; end
    def comment_mail_label; 'E-mail'; end
    def comment_mail_label_short; 'Mail'; end
    def comment_body_label; 'コメント'; end
    def comment_body_label_short; '本文'; end
    def comment_submit_label; '投稿'; end
    def comment_submit_label_short; '投稿'; end
    def comment_date( time ); time.strftime( "(#{@date_format} %H:%M)" ); end
    def referer_today; '本日のリンク元'; end

    def navi_index; 'トップ'; end
    def navi_latest; '最新'; end
    def navi_update; "更新"; end
    def navi_preference; "設定"; end
    def navi_prev_diary(date); "前の日記(#{date})"; end
    def navi_next_diary(date); "次の日記(#{date})"; end

これらのメソッドを再定義することで、別の文字列を埋め込むことが可能です。モバイルモード向けにも用意されています。詳しくは00default.rbを参照してください。

まず、自分のカスタマイズ用プラグインファイルを用意します。pluginディレクトリに、例えば「custom.rb」という名前でファイルを作ります。自分の日記の雰囲気には「ツッコミ」という言葉がしっくり来ないという場合を想定して、これらを書き換えることにしましょう。ツッコミという言葉を使っている4つのメソッドをcustom.rbにコピーして、「コメント」という言葉に置き換えます。

    def comment_today; '本日のコメント'; end
    def comment_new; 'コメントを入れる'; end
    def comment_description; 'コメントがあればどうぞ! E-mailアドレスは公開されません。'; end
    def comment_description_short; 'コメント'; end

これだけでOKです。日記を表示して、変化していることを確認しましょう(スーパーリロードしないと変化しないかも知れません)。

他にも、カスタマイズ用プラグインとして「theme_url」が用意されています。

    def theme_url; 'theme'; end

このプラグインは、テーマファイルが置かれているURLを指定するものです。通常はインストール先のthemeディレクトリなのでこのままで大丈夫ですが、同一サーバで複数の日記を運用している場合など、テーマファイルを一か所に集めたい場合には、これを使わないとテーマが読み込まれません。

カスタマイズするときは、「'theme'」の代わりにテーマファイルのあるディレクトリのURLを指定します。URLの最後を「/」で終わってはいけません。

このほかにも、navi_userやnavi_adminを再定義することで、ナビゲーションボタンのラベルを変更することもできます。また00default.rbにはこれ以外にも、HTMLのヘッダの情報生成や、日付・段落アンカータグを生成する以下のようなプラグインが定義されています(詳しくはコードを読んでください)。

- `author_name`: 著者名を示すmetaタグを生成
- `author_mail`: 著者のメールアドレスを示すmetaタグを生成
- `index_page`: INDEXを示すmetaタグを生成
- `css_tag`: スタイルシートの利用を指示するタグを生成
- `title_tag`: titleタグを生成
- `anchor`: アンカータグを生成する

## オリジナルプラグインの実装

以下では、Rubyのコードが書ける人を対象に、オリジナルのプラグインを実装する方法を解説します。と言っても、文字列を返すメソッドを書くだけなので難しいことはありません。ここでは、プラグイン内で利用できるインスタンス変数の解説だけ行います。

プラグインは専用のPluginクラスの内部で実行されるので、その中にある変数にしかアクセスできません。tDiaryの他の部分に影響が出ないようになっています。もっとも、evalを使って再定義してしまうことで、いくらでも自由になるのですが。

Pluginクラスのインスタンス変数には以下のものがあります。

変数名 | 説明
--- | ---
@mode | 現在動作中のモードを表現する文字列です。tdiary.rb中で使われているTDiaryクラスの策クラス名から、TDiaryを取って、downcaseしたものが含まれています。上手に利用するにはtDiaryの内部構造を知っている必要があるでしょう(いずれちゃんと説明書きます。すまぬ)
@conf | TDiary::Configクラスのインスタンスで、tdiary.confから読み込んだ設定値が入っています。「@conf.index」のようにアクセスできます。
@diaries | 日記データを保持するDiaryインスタンスのHashです。現在表示対象になっている日付を含む月全体が含まれます(最新表示で月をまたいでいる場合は、2ヶ月分含まれることがあります)。Hashのキーは「yyyymmdd」形式の日付で、Hashの値がDiaryインスタンスです。
@cgi | CGIクラスのインスタンスで、現在実行中のCGIに渡されてきたパラメタやCookieのデータが含まれています。スクリプトのパスや、パラメタの値を取得したい場合に利用できます。。
@years | 現存するすべての日記の年月データを保持するHashです。キーは年、値は月のArrayです。
@cache_path | キャッシュファイルのあるディレクトリ。プラグインでキャッシュを使いたい場合は、ここに独自のディレクトリを掘って利用します。
@date | 現在表示中の日付。特定の日か、月を表現したものかどうかは、@modeを見なければ判断できません。
@plugin_files | プラグインファイル名(フルパス)が読み込まれた順に入ったArrayインスタンスです。現在の稼働中のシステムで、どのようなプラグインが利用可能かを知る指標として使えます。
@last_modified | 現在表示しようとしているページの最終更新時刻が入ったTimeオブジェクトです(HTTPヘッダで返るものと同じ)。ただし、更新ページの場合など、この値が意味を持たない場合にはnilになります。
@debug | プラグイン開発時のデバッグ用フラグです。この値をメソッド内でtrueにすると、プラグイン実行時に存在しないメソッドを呼んでもエラーになります(通常時には無視されています)。この変数を利用するのはプラグイン開発時のみにとどめ、プラグイン配布時には必ず削除してください。

## コールバック系プラグイン

上で説明した、単に文字列を返すメソッドを定義してそれを日記本文に埋め込んで使うだけのプラグインと違い、特定の状況でのみ呼び出されるようなコールバック系プラグインがあります。tDiaryの機能を拡張するために利用できます。以下にその作り方を解説します。

コールバック系のプラグインは現在、以下の4種類定義されています。

- `update_proc`: 日記更新時およびツッコミ追加時に呼び出される
- `header_proc`: HTMLヘッダ情報の生成時に呼び出される
- `body_enter_proc`: 日記本文開始の直前で呼び出される。パラメタに処理中の日記の日付(Timeインスタンス)が与えられる
- `body_leave_proc`: 日記本文終了の直後に呼び出される。パラメタに処理中の日記の日付(Timeインスタンス)が与えられる
- `footer_proc`: footer生成時に呼び出される
- `edit_proc`: 更新フォームの内部で呼び出される。更新フォーム内に新たなアイテムを追加する時に使う。パラメタに処理中の日記の日付(Timeインスタンス)が与えられる。追加したアイテムのname属性は、必ず「plugin_プラグイン名_アイテム名」になっていなくてはならない
- `form_proc`: 更新フォームの次に呼び出される。更新フォームとは別のフォームなどを追加するときに使う。パラメタに処理中の日記の日付(Timeインスタンス)が与えられる。フォームを追加した場合、submitボタンには必ず「plugin」というname属性を付ける必要がある
- `conf_proc`: 設定画面で呼び出される。プラグインのオプション設定をWebから行うためのプラグイン

いずれも通常のプラグインと同様にメソッドとして実装されていますが、上書き定義してはいけません。上書きすると、この機能を使っている他のプラグインが呼び出されなくなってしまうためです。

このため、これらのプラグインには機能を追加するためのメソッド、`add_update_proc`と`add_header_proc`、`add_body_enter_proc`、`add_body_leave_proc`、`add_footer_proc`、`add_edit_proc`、`add_form_proc`が用意されています。これらはブロックを受けとります。ここで登録されたブロックが順番に呼び出されることで、複数のプラグインが実行されます。また、`conf_proc`に対応する`add_conf_proc`は後述するようにキーワードとラベル文字列を引数に取り、必要な時だけ呼び出されます。

これらのプラグインは、一般的に以下のように定義して使います。

    # 検索キーワードをheadに挿入する
    add_header_proc do
       '<meta name="keyword" content="Linux,Kondara">'
    end

ただし、body_enter_procとbody_leave_proc、edit_proc、form_procは、現在対象にしている日記の日付がブロックパラメタとして受け取れるようになっているので、以下のように指定する必要があります。

    # 最終更新日を表示する
    add_body_enter_proc do |date|
       diary = @diaries[date.strftime( '%Y%m%d' )]
       '#{diary.last_modified( "%Y-%m%-d" )}'
    end

なお、コールバック系プラグインはそれぞれのブロックが返した文字列をすべて連結してHTMLに埋め込みますが、`update_proc`は何も埋め込みません(意味がないので)。このため、`update`用に追加するブロックは何も返す必要はありません。

デフォルトプラグインファイルである00default.rbには、標準のHTMLヘッダを生成するheader_proc用ブロックが定義されています。

また、単独では意味のないコールバック系プラグインがあります。edit_procです。edit_procは更新フォーム内に文字列を埋め込むためのプラグインですが、これによって更新フォームに任意のアイテムを追加できます。このアイテムに入力された値は、別途update_proc内で受けとる必要があるでしょう。

    # name属性には「plugin_プラグイン名_アイテム名」を付けなければならない
    add_edit_proc do |date|
       'Hoge: <input name="plugin_hoge_item1">'
    end

    # 更新時に値を受け取れる
    add_update_proc do
       if /^(append|replace)$/ =~ @mode then
          hoge = @cgi.params['plugin_hoge_item1'][0]
       end
    end

一方、同じフォーム追加を目的としたform_procではこのようにする必要はなく、同じform_proc内で入力を受けとることができます。

他のコールバック系プラグインとは少しおもむきが異なるのが、`add_conf_proc`です。以下のように呼び出します。

    add_conf_proc( 'hoge', 'hogeの設定' ) do
       # 設定の保存
       if @mode == 'saveconf' then
          @conf['hoge.fuga'] = @cgi.params['hoge.fuga'][0]
       end

       # 設定画面の出力
       <<-HTML
       <h3>fugaの指定</h3>
       <p><input name="hoge.fuga"></p>
       HTML
    end

1つ目の引数は、複数の`conf_proc`の中からどれを表示するか選ぶために、全プラグイン内で一意になるようにつけるキーワードです。一般的にはプラグインの名称をつけます。2番目の引数は設定画面の一覧を表示するときに使うラベル文字列です。言語によって変更させます。

`@mode`が`'saveconf'`の時のみ、@cgi経由で渡された設定を保存します。保存先は@conf[]にオプション用のキーワードを指定して代入します。これは従来tdiary.confにて、@optionsとして指定したものと同じです。なお、conf_procで保存できるオプションは、文字列(String)、整数(Fixnum)、真偽値(TrueClass/FalseClass)、nil(NilClass)で表現されるオブジェクトだけです。

conf_procを使って設定を保存すると、その後はtdiary.confによる@optionsは効かなくなるので注意が必要です。また、@conf.secureに応じて設定できる項目が変化するようにしておけば、レンタルの場合に不必要な項目をユーザが変更できなくすることも可能です。

## プラグインが生成するHTML

プラグインは一般的に文字列を返すことでその文字列が日記に埋め込まれるわけですが、多くの場合その文字列はHTMLの断片になります。このHTML断片は以下のルールに従うようにしてください。

- HTML 4.01 Strictに準拠する。tDiaryはHTML 4.01 Strictに準拠したHTMLを生成します。このため、プラグインもそれに倣う必要があります
- 独自のクラス名(HTMLタグのclass属性)を生成する場合には、テーマ上での指定時に汎用性を持たせるために「プラグイン名-クラス名」のような形式にすることを強く推奨します

古いプラグインでは2番目の条件に従っていない場合が少なくありませんが、新規に作成する場合には遵守するようにしてください。

## Pluginクラスのメソッド

プラグインは、TDiary::Pluginクラスのインスタンス内に定義された特異メソッドとして動作します(コールバックは除く)。そのため、すでにPluginインスタンス中で定義されているメソッドは、プラグインから自由に呼び出すことが可能です。例えばあるプラグインの機能を別のプラグインから通常のメソッド呼び出しの形式で利用できるわけです。

この他に、Pluginクラスには以下のようなメソッドが用意されています。通常はPluginクラス外へ影響を及ぼせないプラグインですが、これらのメソッドを使うことでそれが一部可能になっています。

メソッド名 | 説明
--- | ---
add_cookie( cookie ) | Webブラウザに返すCookieを追加します。プラグインから何らかの情報をWebブラウザ側に保持しておいて欲しい時に使います。引数cookieはCGI::Cookieインスタンスです。なお、逆にCookieを受けとる時は、@cgiインスタンス変数から取得してください。
apply_plugin( str, remove_tag ) | プラグイン内で生成した文字列の中に、さらにプラグイン呼出しの指定がある場合、もう一度プラグインの呼出しを行います。第二引数をtrueにすると(省略可能でデフォルトはfalse)、さらにHTMLタグを削除します。
shorten( str, limit ) | 文字列strをlimitで指定されたバイト数に切り詰めます。切り詰められた場合には末尾に「...」が付加されます。プラグイン内で生成した文字列の表示文字数を制限したい場合に使います。
add_XXX_proc( proc ) | コールバック系プラグインを追加するメソッド。「XXX」にはheader、update、body_enter、body_leave、footerが入る。引数procにはProcインスタンス、もしくはブロックを与える。コールバック系プラグインを参照。

## プラグインへのオプションの渡し方

プラグインはRubyのメソッドですから、呼び出し時に引数を指定することで、利用者がプラグインの挙動を変更することが可能です。しかし、tdiary.conf等で日記管理者が強制的にオプションを指定したい場合があります。そのようなプラグインを作成する時には、tdiary.confで指定できる@options変数を使うことができます。

@option変数は、プラグインから@conf[]を経由して見ることができるで、tdiary.confで任意の文字列をキーとして定義すれば、それをプラグイン内で利用することが可能です。@optionsのキーに指定する文字列はなんでもかまいませんが、名前の重複を避けるためにプラグイン名とオプション名を「.」で区切ったものを推奨します。

    # sampleプラグインにhogeオプションを指定する(tdiary.conf内)
    @options['sample.hoge'] = 'foobar'

    # sampleプラグイン内でオプションを取得する(sample.rb内)
    hoge = @conf['sample.hoge']

またconf_procプラグインを使うことで、この変数の値をWeb上から対話的に設定できるようにもできます。詳しくはコールバック系プラグインの説明を参照してください。

なお、この仕組みはプラグイン制作者側には便利ですが、ファイルを設置するだけで使えるようになるプラグインの簡便さを削いでしまう可能性もあります。できるだけ@optionsが設定されていなくても動作するように、適切なデフォルトを用意するようにして下さい。

## CSRF 対策とプラグインの関係

tDiary 2.0.2、2.1.2 以降、クロスサイトリクエストフォージェリ (CSRF) 攻撃に対する防御機構が実装されました。CSRF 攻撃の被害を防ぐためには、 プラグインも含む Web アプリケーション全体が防御策を実装していなければ なりません。tDiary では、個々のプラグイン作者が対策手法の詳細を意識しな くても、全体として一体的な防御策がとられるよう配慮されていますが、 正しく防御機構を動作させるためには、プラグインの作者も若干の 注意が必要となります。具体的には、次の3点に注意して実装を行ってください。

1. 副作用を含む設定動作には conf モードを使わない

  tDiary の設定、動作などに何らかの影響を加えるページ（例えば、キャッシュの消去）などは、 conf モードではなく、必ず saveconf モード内で実装して下さい。conf モードの ページに関しては、CSRF 対策が働きませんので、外部から攻撃される可能性が あります。

  なお、当然のことですが、日記の読者が行う動作（例えば、ツッコミ）などには 対策は必要ありません。

2. formplugin モードで動作させるためのフォームには CSRF 対策鍵を埋め込む

  form_proc 内などで、plugin モードで動作させる機能のためのフォームを 生成する場合（例えば、絵日記プラグインの「画像をアップロード」フォーム）、 POST メソッドのフォーム内で csrf_protection メソッドの呼び出し結果を埋め込んで下さい。 CSRF 鍵が設定され使われている場合に限り、 CSRF 対策キーが hidden 項目で埋め込まれます。 標準の絵日記プラグイン (image.rb) などが対策済みですので そちらを参考にして下さい。

  この対策を忘れた場合、CSRF 対策鍵を利用するモードでのみ プラグインが動作しなくなります。デフォルトの設定では 問題なく動作してしまいますので、プラグイン作者は CSRF 鍵もチェックに利用する設定 (@options['csrf_protection_method'] = 3) にして動作確認することをお勧めします。

  当然のことですが、自分の日記の update.rb に送付するフォーム以外には、 絶対に CSRF 対策鍵を埋め込んではいけません。

  なお、この鍵を埋め込まれたフォームは、必ず POST メソッドで送信される必要 があります (Referer ヘッダ経由で外部に漏出することを防ぐため)。 プラグインの誤実装による CSRF 鍵の漏洩を防ぐため、 tDiary は、update.rb に GET メソッドで CSRF 対策鍵が送られてきた場合、 すべての要求を拒否するようになっています。

  edit_proc と conf_proc で動作する機能に関しては、 フォームに既に対策鍵が埋め込まれていますので、 本項の対策は必要ありません。目安としては、 `<form method="post" action="#{@conf.update}">` のような文字列を自分で出力している場合が該当すると思ってください。

3. 実動作を伴うページへの送信は必ず POST で

  plugin, saveconf, preview系, 更新系 の各ページは、 CSRF 鍵を必要とする場合があるため、POST 以外でのリクエストを 拒絶するようになっています。もし plugin ページを GET で呼び出している場合は POST メソッドを利用するよう (そして 2. の対策をするよう) 変更して下さい。 場合によっては、リンクをフォームに変更するなどの書き換えが必要かもしれません。