PukiWikiプラグイン開発メモ

PukiWiki用プラグインの基本的な開発方法と、公開自作プラグインの設計方針の記録。

作り方

基礎知識

  • プラグインはデフォルトでpluginディレクトリに配置されるPHPファイルである。ファイル名は「プラグイン名.inc.php」
  • プラグインには次のうち必要な関数を実装すること
    • plugin_<プラグイン名>_convert() … ブロック型。ページ内の「#hoge()」記述で呼ばれる
    • plugin_<プラグイン名>_inline() … インライン型。ページ内の「&hoge();」記述で呼ばれる
    • plugin_<プラグイン名>_action() … コマンド。「?plugin=hoge」URLで呼ばれる
      • ページ名を伴う呼び出しは「?plugin=hoge&refer=<ページ名>」。「?cmd=hoge&page=<ページ名>」は古い書式らしい?
    • 他にもあるがあまり使わない
  • 定数は「PLUGIN_<プラグイン名>_<定数名>」という名前で定義すること

実装

  • 作りたい機能に近い標準プラグインファイルのコードをコピーし、ファイル名や関数名を変えたものを原型とする
    • 規則を覚えて一から自分でこしらえる必要などない。既存のコードを改造するのが近道
    • コピーしたコードを読み、引数の取り方や返り値の形式、使用できるシステム関数・グローバル変数などを把握する
  • システム関数・グローバル変数・定数などについて詳しくは、PukiWiki本体ファイル(libディレクトリ以下)をgrepするか、PukiWiki公式開発サイトを参照するか、ググって調べる
    • ネット上の情報は古いことが多いので注意。PukiWikiは生まれてから長いうえ、ピークがはるか昔に過ぎているため致し方なし
      • このページもどうせ古くなるので、具体的なコードなどはあえて載せていない
    • 公式サイトにも今のところ最新情報はまとまっておらず、更新情報をたどる必要がある
  • 新し目のPHP標準関数や特殊なライブラリーの使用は可能なら避けると互換性が増す
    • 悩んでまで避けなくてもよいが、必要動作環境は自分のためにも明記しておく
    • 標準プラグインのコードにおいて、あきらかに古く非効率に見える書き方は互換性のためだったりする。単に古い場合もあるが、いずれにせよ実績のあるコードなので、問題なければそのまま真似るのが無難
  • XSS防止のため、引数として渡された文字列を出力に用いる場合は必ずサニタイズすること
    • ただし、インライン呼び出しの最終引数はシステムによりサニタイズ済み
  • ページ内で複数回呼ばれる可能性に留意すること
    • static変数をインクリメントするなどして検知する。値によって初回のみ特別なコードを出力したり、複数回呼び出しを禁止したりできる
  • ページが表示されるたびに実行されるため、負荷に注意すること
    • 重い処理の結果は、いくつかの標準プラグインを参考にファイルやSQLiteにキャッシュするとよい
    • ブラウザー側で実行可能な処理はそちらに任せる(JavaScript化する)のも方法
    • あまりに時間のかかるものは、iframeAjaxスクリプトを出力し、中身はブラウザーからプラグインコマンドを叩いて非同期に取得する仕組みにしてみる
  • 別のページを参照したりインクルードしたりする種類のプラグインにおいては、自己呼び出しによる無限ループに陥らないよう注意すること
    • 最近のバージョンのPukiWikiでは、無限ループを検知してエラー終了するよう対策されてはいる

1ファイルで完結させる

個人的なポリシー、というか複雑な決まり事や面倒な作業を強いられるのが単純に嫌なため、プラグインはなるべく1ファイルで完結させたい。

リソース内蔵

PHPContent-Typeヘッダーもバイナリも出力できるため、理論的にはURIで表現されるあらゆるリソースを返すことができる(現実的には負荷や実行時間が制限されるが)。よって、コマンド呼び出しのクエリーに応じて各種リソースを返すようプラグインを実装することで、リソースファイルを別途用意する必要がなくなる。

具体的には、たとえば画像を表示するなら「<img src="?plugin=hoge&q=image" />」といった具合にURLをプラグインのコマンドとする。呼び出されたプラグインがこの引数に応じて画像のバイナリを出力すれば、画像が表示されることになる。

もしくは、それが可能な種類のリソースであれば、タグに埋め込んで出力するのが手っ取り早い。たとえば画像を表示するなら、「<img src="data:image/png;base64,..." />」といった具合にBase64形式で埋め込む。バイナリ画像のほか、CSS(styleタグ)・JavaScript(scriptタグ)・フォント(Base64)・SVG(svgタグ)なども直接出力してHTMLに埋め込むことができる。

リソースはハードコーディングせず別ファイルとしておくほうが仕様としては柔軟で美しいが、「仕様の美しさ」よりも「ユーザーにとっての簡便さ・間違えようのなさ」を優先するのがここでの方針である。つまりはフールプルーフだが、実際には、デフォルトでは内蔵リソースを用い、指定によって外部のリソースファイルも参照できる、両者のいいとこ取りで実装する。

本体無改造

スパム・セキュリティー対策などのクリティカルな事情のある場合を除き、原則としてPukiWiki本体(libディレクトリ以下)の改造は避ける。改造には何かと問題が伴い、端的にはPukiWikiのアップデートが難しくなる。改造した当人にとってすら厄介な作業を他人に求めるべきではない。未来の自分も他人と考えたほうがよい。

改造しなければ実現できない機能はPukiWikiの限界として受け入れる。受け入れられなければ、そのサイトの目的にPukiWikiは適していない。今日では選択肢が豊富にあるのだから、無理にPukiWikiを改造するより他のCMSを選ぶほうが理に適う。

HTML/CSS/JavaScript(ブラウザーAPI)等の仕様の進歩により、かつては本体改造するしかなかった問題が、現在では工夫次第で無改造で解決できる場合がある。もとより必要ないはずなのに安直に本体を改造しているものは問題外。

いかにファイルを一本化し、本体無改造で機能を実現するか。PukiWikiプラグイン開発において、知恵と工夫を要するのは主にこの点である。

本体改造

どうしてもPukiWiki本体を改造する必要のある場合は、既存コードを直接書き換えるのではなく、プラグインをフィルターとして用いられるよう設計し、既存コードに明瞭なかたちで「挿入」する(システム関数do_plugin_action|convert|inlineで呼び出す)。プラグインは任意の入出力が可能であり、要するに関数としても働くため、劇的な機能変更をしない前提であれば大抵はこの仕組みで間に合う。ささいな書き換えのためにまでわざわざプラグインによるフィルターを実装するのは手間なようだが、テンプレートを一度作って使い回せば済むのだから、このようにして将来の再現の容易さを選ぶほうが長い目でみて合理的である。

設定の分離・集中

ちょっとした便利ハックの類い。

プラグインファイル内の定数を、下の例のように二重定義を避けるように記述する。

if (!defined('PLUGIN_HOGE_FUGA')) define('PLUGIN_HOGE_FUGA', 1);

すると、default.ini.phpなどの別ファイルでもこの定数を定義できるようになる(定義してもエラーが出なくなる)。

あらゆるプラグインの定数をdefault.ini.phpで設定することにしておけば、プラグインをアップデートしたり新規インストール環境に設定を再現する際などに、各プラグインファイル内の定数をいちいち書き換え直す手間を省くことができる。運用中の設定管理も楽になる。

設計思想上の是非には議論の余地があるかもしれないが、とりあえず便利。

特化事例

静的サイトのCMSに特化して相当に手を入れたPukiWikiもこのサイトとは別に管理している。上述の主張と矛盾するようだが、手がかかる一方で、自由に改造できることはPukiWikiの大きな利点でもある。特化した主な要素を以下に挙げる。

画像最適化

画像はページへの添付ではなく、特定のディレクトリにまとめて配置する。専用プラグインコマンドにより、各画像はトリミング・リサイズされて再配置される。複数の解像度、かつWebP版を生成するのがミソ。srcsetタグにより常に最適な解像度の画像が選ばれ、またブラウザーが対応していればWebP版が優先して表示される仕組み(Lighthouse監査でWebPの積極的な使用が推奨されている)。なお、画質がなるべく損なわれないよう、画像リサイズにはGDではなくImagickを利用する。

静的HTML

編集にこそ使うものの、基本的に静的であるページの閲覧にPukiWikiを介すのは無駄なため、すべてのページをHTMLファイルとして書き出すことにする。PukiWiki用の静的HTML化プログラムはいくつか公開されているが、いずれも古いうえ、自分の用途に合わせて手を入れる羽目になるのがわかりきっているため、始めから自作する。

基本的な仕組みは単純。PukiWiki各ページのURLを叩いて完全なHTMLを入手し、正規表現でHTMLに含まれるリンクや画像などのURLを静的なものに変換する。子階層を持つページは、ページ名.htmlではなくページ名ディレクトリ/index.htmlとなることに注意。あとは実際にこれらのHTMLを表示し、リンク切れなどがあれば原因を解消していく。

AMP対応

上記静的HTML化プログラムにオプションを加え、オンにすると次のようにAMP HTMLを生成するようにした。

  • 各基本要素にAMP属性を追加する
  • HTML内容を走査し、使われている要素に合わせて必要なAMPスクリプトを組み込む
  • 各メディア要素などをAMP用に書き換える
  • 自作の問い合わせフォームプラグインをAMP対応のものに差し替える
  • CSSのインライン化など、その他の要件はもともと満たしていた

せっかく作ったが、次の理由により現在はAMPをオフにしている。

  • 最適化・高速化を突き詰めると、AMPスクリプトのロード・実行がボトルネックとなる
    • ベンチマーク上の問題ではなく、非AMP版とは目に見えて画面表示時間に差がある
  • GoogleAMPキャッシュ&ビューアーの問題
    • UIが邪魔
    • 表示が崩れることがある
      • 崩れないまでも、プレーンなレンダリングとは表示領域が異なり無駄にスクロールが発生したりする
    • 解決法は一応あるものの、事実上Googleのドメインになってしまう
    • オプトアウトできない
      • Facebook陣営のInstant Articlesに対抗するAMPの出自ゆえか
  • CDNならCloudFlareなどを利用すればよい

また、本事例にはあたらないが、一般的なウィキをAMP化しようとすると次が問題となる。

  • PukiWikiのすべての機能やプラグインに対応できるわけではない
    • 使用可能な機能を規則を設けて制限する必要があり、複数人編集では運用コストがかかる
  • AMP Cacheと呼ばれるCDNにキャッシュされたページが表示される
    • ページをリロードしてもなお見ている内容が最新であるとは限らず、ウィキとの相性が悪い

ニュース記事などをAMP提供してサイトに引き込む導線に、といった目的なら利点が上回るだろうが、自分のサイトには不向きだった。しかし、AMPの理念そのものは有用であり、その方針に沿ったコーディングを心がけて損はない。

OGP・JSON-LD・PWA・reCAPTCHA v3対応

その他、一般的なページ最適化を施した。これらは動的なウィキにおいても有用なため、それぞれ独立したプラグインとして実装し直し、当サイトで公開している。