OXY NOTES

5段階評価プラグインを通して学ぶPukiWikiのプラグインを作成する方法

PukiWikiのプラグインの作り方をまとめました

前回jQueryと組み合わせて5段階評価を実現するjQuery Ratyを解説しました。

今回はその機能を利用してPukiWikiのプラグインを作成します。また今回作成するプラグインはPukiWikiのプラグインを理解する上で好適なため、合わせてプラグインの仕様についても解説します。


PukiWikiプラグインの仕様を確認

まずは公式の解説と関連する情報を掲載します。

公式:PukiWiki/Plug-inの仕様 
公式:プラグイン/開発者向け 
公式:PukiWiki/関数一覧表 

続いてPukiWikiプラグインのパイオニアSonots’さんの解説ページ。
プラグイン作成後に見つけましたが、必要な情報がひと通り網羅されてます。

PukiWikiプラグイン開発チュートリアル

以上のページに詳しい解説がありますが、読むだけでは全体像を把握するのが困難です。
このページではより理解を深めるために実際にプラグインを作成して動作を確認します。


プラグインの保存場所とファイル名

プラグインはPukiWikiの「plugin」ディレクトリに保存することで動作します。

以下のフォーマットでファイル名を付けます。

<プラグイン名>.inc.php

今回は「plugin」ディレクトリに「hoge.inc.php」を作成してください。


プラグインの動作を決める関数

プラグインの動作を決めるにはまず、自分が作成したいプラグインが、どのタイプかを把握する必要があります。
タイプにはインライン型ブロック型アクション型の三種類があります。

ブロック型のプラグイン

ここで言うブロックとはHTMLで言うブロックレベル要素(Block-Level Elements)と同じです。
見出しや段落、表など1つの固まりとして機能する要素ブロック要素と言います。

具体的には以下のタグはブロック要素となります。

<address>、<blockquote>、<center>、<div>、<dl>、<fieldset>、<form>、<h1>-<h6>、
<hr>、<noframes>、<noscript>、<ol>、<p>、<pre>、<table>、<ul>

PukiWikiではプラグイン内で以下のフォーマットで関数を定義するとブロック型のプラグインになります。

function plugin_<プラグイン名>_convert()
{
	<プラグインの動作を定義>
}

それでは実際に先ほど作成したhogeプラグインをブロック型に対応させます。

hoge.ini.php

<?php
function plugin_hoge_convert()
{
	return 'hoge';
}
?>

保存できたらPukiWikiで「テスト」という名前のページを作成、編集画面で「#hoge()」と入力してください。

PukiWikiテストページ

#hoge()

するとテストページに以下のように表示されるはずです。

表示例

hoge

このプラグインはブロック型なので「#hoge()」の記述は行頭に書く必要があります。たとえば「あいうえお#hoge()」と書けば、そのまま「あいうえお#hoge()」という文章を打ったのと同じように表示されてしまします。

反対に「#hoge()あいうえお」と入力した場合は「#hoge()」がプラグインと解釈され、続く「あいうえお」は無効な値として無視されます。

引数の渡し方

ここで引数の渡し方についても解説します。

hoge.ini.php

function plugin_hoge_convert()
{
	$args = func_get_args();
	return 'hoge、引数は' . $args[0];
}

PukiWikiテストページ

#hoge(123)

表示例

hoge、引数は123

このように()(カッコ)で囲んだ中に引数を指定すると、プラグイン内でfunc_get_args()を利用して受け取ることができます。
複数の引数を渡す場合は#hoge(123,456)とカンマで区切ります。

インライン型のプラグイン

続いてインライン型のプラグインについて解説します。
ブロック型のプラグイン同様に、インライン要素(Inline Elements)として動作するプラグインです。

インライン要素の具体例は以下の通り

<a>、<abbr>、<acronym>、<b>、<basefont>、<bdo>、<big>、<br>、<cite>、<code>、<dfn>、
<em>、<font>、<i>、<img>、<input>、<kbd>、<label>、<q>、<s>、<samp>、<select>、
<small>、<span>、<strike>、<strong>、<sub>、<sup>、<textarea>、<tt>、<u>、<var>

インライン型は以下のフォーマットで記述します。

function plugin_<プラグイン名>_inline()
{
	<プラグインの動作を定義>
}

それでは実際に先ほど作成したhogeプラグインをインライン型に対応させます。
先ほどのhoge.ini.phpに追記してください。

hoge.ini.php

function plugin_hoge_convert()
{
	$args = func_get_args();
	return 'hoge、引数は' . $args[0];
}
function plugin_hoge_inline()
{
	return 'インライン型hoge';
}

PukiWikiテストページ

&hoge();

表示例

インライン型hoge

今回はインライン要素なのでブロック要素の中に記述することができます。テストページに以下のように記述してください。

これは「&hoge();」です。

表示例

これは「インライン型hoge」です。

前回と異なり、文章の中に埋め込むことができました。

またインライン型は文字列を渡すことができます。

hoge.ini.php

function plugin_hoge_inline()
{
	$args = func_get_args();
	return 'インライン型hoge、引数は' . $args[0] . '、文字列は' . $args[1];
}

PukiWikiテストページ

&hoge(123){こんにちわ};

表示例

インライン型hoge、引数は123、文字列はこんにちわ

これを利用して簡単な文字サイズを変更するインラインプラグインを作ります。同じ関数名は使えないので、plugin_hoge_inlineを書き換えます。

hoge.ini.php

function plugin_hoge_inline()
{
	$args = func_get_args();
	$html = '';
	$html .= '<span style="font-size:' . $args[0] . '%">' . $args[1] . '</span>';
	return $html;
}

PukiWikiテストページ

&hoge(200){こんにちわ};

すると以下の様なフォントサイズが200%に拡大した「こんにちわ」が表示されたはずです。

このように、実用的なプラグインも仕様さえ理解してしまえば、簡単に作成することができます。

アクション型のプラグイン

3つ目はアクション型のプラグインです。
これはGETもしくはPOSTメソッドで渡されたデータを引数としてプラグインに渡す型です。PukiWikiの編集画面で引数を編集するのではなく、入力フォームJavaScriptの動的なデータを渡す場合などに利用します。

GETメソッドとPOSTメソッドについてはHTMLの分野なので詳細は省略します。

アクション型は以下のフォーマットで記述します。

function plugin_hoge_action()
{
	<プラグインの動作を定義>
	return array('msg'=><表示するページ名>, 'body'=><表示するコンテンツ>);
}

返り値はmsgbodyで、それぞれページ名コンテンツを指定します。

すこしややこしいので、実際にやってみます。

GETメソッドの場合

GETメソッドを渡す場合は以下のように「plugin」もしくは「cmd」で繋げてプラグインにデータを送信します。
基本的にプラグイン名のあとに「&(アンド)」で繋いで変数とデータを送信するという仕組みです。

http://example.com?plugin=<プラグイン名>&page=<ページURL>&<変数名>=<内容>
http://example.com?cmd=<プラグイン名>&page=<ページURL>&<変数名>=<内容>

ちなみにpageを省略した場合は$defaultpageが表示されます。(GETメソッドをプラグインに渡した後にトップページが表示される)

<ページURL>の部分はURLエンティティ化する必要があるので注意してください。(例:テスト→%E3%83%86%E3%82%B9%E3%83%88)

hoge.ini.phpに追記

function plugin_hoge_action()
{
	global $vars;

	$html = '';
	$html .= 'cmd:' . $vars['cmd'] . "\n";
	$html .= '、page:' . $vars['page'] . "\n";
	$html .= '、hensu:' . $vars['hensu'] . "\n";

	return array('msg'=>$vars['page'], 'body'=>$html);
}

今度はfunc_get_args()ではなく、グローバル変数の$varsで引数を受け取っています。

PukiWikiテストページ

(example.comの部分はサイトに合わせて変更してください)

[[hogeプラグインに送信>http://example.com/index.php?cmd=hoge&page=%E3%83%86%E3%82%B9%E3%83%88&hensu=123]]

編集が終わったらリンクになっている「hogeプラグインに送信」をクリックしてみてください。
ページ名が「テスト」でコンテンツに以下の内容が表示されるはずです。

表示例

cmd:hoge 、page:テスト 、hensu:123

これで機能はバラバラながら、1つのプラグインで「ブロック型、インライン型、アクション型」の動作を確認しました。このように1つのプラグインで複数の型に対応することができます。

POSTメソッドの場合

続いては応用編としてPOSTの場合もテストします。

hoge.ini.phpを書き換え

function plugin_hoge_inline()
{
	global $vars;

	$args = func_get_args();
	$html = '';
	$html .= '<form method="post" action="' . get_script_uri() . '?cmd=hoge&page=' . $vars['page'] . '" method="post">'. "\n";
	$html .= '<p>'. "\n";
	$html .= '<input type="radio" name="this_page_is" value="good" checked="checked" /> 良い'. "\n";
	$html .= '<input type="radio" name="this_page_is" value="bad" /> 悪い'. "\n";
	$html .= '</p>'. "\n";
	$html .= '<p><input type="submit" value="送信" /></p>'. "\n";
	$html .= '</form>'. "\n";
	return $html;
}

function plugin_hoge_action()
{
	global $vars;

	$lines = get_source($vars['page']);
	array_push($lines, $vars['this_page_is']);
	$newlines = implode('', $lines);

	page_write($vars['page'], $newlines);

}

インライン型でフォームを書き出し、フォームから渡されるPOSTメソッドを自ら(plugin_hoge_action)に送信しています。

7行目get_script_uri()はPukiWikiの関数で、現在のURLを取得できます。
続けてpage$vars[‘page’]を渡しているのでURLは「http://example.com/index.php?cmd=hoge&page=%E3%83%86%E3%82%B9%E3%83%88」となります。

続いてアクション型の部分では、受け取った変数をコンテンツの最後に追記してページを書き換えています。

21行目get_source()もPukiWikiの関数で、引数にページ名を指定するとページのコンテンツが配列で取得できます
22行目、PHPの関数array_push()で配列の最後に$vars[‘this_page_is’]の値を追加。
23行目、PHPの関数implode()で配列をテキスト形式に成形。

最後、25行目でPukiWikiの関数page_write()を利用してページを書き換えています。サンプルを見てもらえば分かるように第1引数にページ名第2引数にコンテンツを指定します。

ではテストページでインライン型でプラグインを読み込んでテストします。

PukiWikiテストページ

&hoge();

すると以下のようにラジオボタンと送信ボタンが表示されるはずです。

試しに「良い」か「悪い」どちらかを選択して「送信」ボタンをクリックしてみてください。

するとクリックする度にgoodbadというコンテンツが追加される思います。

以上、PukiWikiのプラグインの仕様について、例を交えながら解説しました。
応用すればどんなプラグインも作成できそうですね。

プラグインの初期化関数

特殊な関数として初期化のための関数が用意されています。
これはプラグインの読み込み時に1度しか実行されないという特徴を持っています。各種関数で使いまわす設定や、定数の定義などを行います。

function plugin_<プラグイン名>_init()
{
		<初期化に関する記述>
}

よくある使い方

function plugin_<プラグイン名>_init()
{
//定数の定義
define ('HOGE_WIDTH', '100');
define ('HOGE_HEIGHT', '200');

//エラーメッセージの定義
$messages = array(
	'hoge_messages' => array(
		'title_error' => 'Title Error',
		'no_page_error' => '$1 のページは存在しません',
	)
);
}

理解の早い方は気がついたかもしれませんが、特にinitで関数化することありません。
「PukiWikiではこのように記述する」ということを覚えておくと、他のプラグインを解析するのが楽になります。


5段階評価を実現するjQuery Ratyと組み合わせたPukiWikiのプラグインを作成

追記:改良版を「PukiWikiで5段階評価を付けるプラグインを作成しました」というページに作りました。

前回解説したjQuery Ratyと今回解説したPukiWikiのプラグインを組み合わせて5段階評価を追加するプラグインを作成しました。
もう十分すぎるほど解説したと思うので、機能だけ紹介しますw

  1. 1.クリックするだけで5段階評価ができる
  2. 1.平均値と総投票数を表示
  3. 1.ページ内に複数設置可能
  4. 1.Cookieが無効な場合は投稿禁止
  5. 1.Cookieを利用した連投の禁止(デフォルトで3日間)

raty.ini.php


// ブロック型の対応
//----------------------------------------------------------------------------------
function plugin_raty_convert()
{
	global $vars, $defaultpage;

	// プラグインから引数を取得し平均値を出す
	$args = func_get_args();
	$args = h($args);
	$vowels = array("[", "]");
	$num = str_replace($vowels, "", $args); // drop []

	// numのバリデーション
	foreach($num as $i){
		if ( !is_numeric($i) && $i = "" ){
			$msg = "ratyプラグインの引数が正しくありません";
			return $msg;
		}
	}

	// 投稿時にCookieが有効かチェックするためのCookie
	$cookie_name = 'raty_cookie_check';
	if ( !isset($_COOKIE[$cookie_name]) ) {
		$matches = array();
		preg_match('!(.*/)!', $_SERVER['REQUEST_URI'], $matches);
		setcookie($cookie_name, 1, time()+(60*60), $matches[0]);
	}

	$count = count($num);
	$ave_num = array_sum($num) / $count;
	$ave_num = round($ave_num, 1);
	$page = isset($vars['page']) ? $vars['page'] : $defaultpage;

	$html = html_convert($ave_num, $count);

	return $html;
}

// jQuery Ratyの記述
//----------------------------------------------------------------------------------
function html_convert($ave_num, $count)
{
	global $vars, $digest;
	static $raty_id = 0;

	// プラグインをクリックされた時に作成されるURLを作成する
	$page = isset($vars['page']) ? $vars['page'] : $defaultpage;
	$url =
	get_script_uri() . '?plugin=raty' .
	'&refer=' . rawurlencode($page) .
	'&digest=' . rawurlencode($digest) .
	'&raty_id=' . $raty_id . 
	'&score=';
	
	$form = '';
	// jQueryがない場合
	//if ($raty_id == 0) {
	//	$form .= '<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>' . "\n";
	//}	
	$form .= '<script type="text/javascript" src="./skin/jquery.raty.js"></script>' . "\n";
	$form .= '<div class= "raty star' . $raty_id . '" style="width: 155px; float: left;"></div><p style="margin: 8px 0px 5px 0px;">評価数:' . $count . '、平均評価:' . $ave_num . '</p>' . "\n";

	$form .= '<script type="text/javascript">' . "\n";
	$form .= '$(function raty' . $raty_id . '() {' . "\n";
	$form .= '$.fn.raty.defaults.path = "./image";' . "\n"; // スター画像のへのパスを記述
	$form .= '$(".star' . $raty_id . '").raty({' . "\n";
	$form .= 'number: 5,' . "\n";
	$form .= 'score : ' . $ave_num . ',' . "\n";
	$form .= "hints: ['1', '2', '3', '4', '5']," . "\n";
	$form .= 'click: function(score, evt) {' . "\n";

	$form .= 'var url = "' . $url . '" + score;' . "\n"; // 作成したURLにクリックされたスコアを追加
	$form .= 'location.href=url;' . "\n"; // GETでURL更新

	$form .= '}' . "\n";
	$form .= '});' . "\n";
	$form .= '});' . "\n";
	$form .= '</script>' . "\n";

	$raty_id++; // 複数回プラグインが読み込まれたら加算する

	return $form;
}

// アクション型の対応
//----------------------------------------------------------------------------------
function plugin_raty_action()
{
	global $vars;

	// 衝突時のメッセージなど
	global $_title_collided;

	// pukiwikiが閲覧モードの場合は編集不可
	if (PKWK_READONLY) die_message('PKWK_READONLY prohibits editing');

	// ページに複数ratyが設置されている場合を想定して$raty_id
	// 新しく追加するスコアを$scoreに代入する
	$raty_id = $vars['raty_id'];

	$page = isset($vars['refer']) ? $vars['refer'] : $defaultpage;

	// Cookieが有効かどうかを調べる(有効であればplugin_raty_convertで埋め込み済み)
	$cookie_name = 'raty_cookie_check';
	if ( !isset($_COOKIE[$cookie_name]) ) {
		return array(
		'msg'  => _('投稿エラー'),
		'body' => _('評価をするにはCookieを有効にしてください。'),
		);
	}

	// 書き込む際に付けられるCookieを既に持っている場合はメッセージを表示(多重投稿用)
	if (is_continuous_raty($page, $raty_id)) {
		return array(
		'msg'  => _('投稿エラー'),
		'body' => _('評価の連投は禁止しています。'),
		);
	}

	$lines = get_source($page);

	// raty_idのバリデーション
	if ( is_numeric($vars['raty_id']) ){
		$score = $vars['raty_id'];
	} else {
		$msg = "raty_idの値が正しくありません";
		return array('msg'=>$msg, 'body'=>"");
	}

	// scoreのバリデーション
	if ( is_numeric($vars['score']) ){
		$score = $vars['score'];
	} else {
		$msg = "scoreの値が正しくありません";
		return array('msg'=>$msg, 'body'=>"");
	}

	// digestのバリデーション(衝突チェック)
	$contents = implode('', $lines);
	if (md5($contents) !== $vars['digest']) {
		$msg  = $_title_collided;
		$body = show_preview_form($_msg_collided, $contents);
		return array('msg'=>$msg, 'body'=>$body);
	}

	$i = 0; // 行カウント用
	$raty_count = 0; // $raty_idカウント用

	foreach($lines as $line)
	{
		$i++;

		// プラグインの行を調べ、複数ある場合に備えて$vote_idでチェック、当該行を更新
		if (preg_match('/^#raty\(.*\)$/i', $line, $matches) && $raty_id == $raty_count++ ) {
			preg_match('/\[(.*)\]/', $line, $scoredata);
			if ( $scoredata[1] == "" ){
				$line = '#raty([' . $score . '])' . "\n";
			} else {
				$line = '#raty([' . $scoredata[1] . ',' . $score . '])' . "\n";
			}
			$i--;
			array_splice($lines, $i, 1, $line);
			$newlines = implode('', $lines);
		}
	}

	page_write($page, $newlines, TRUE); // TRUEでタイムスタンプ更新しない

}

// 衝突時に表示されるエラー画面
//----------------------------------------------------------------------------------
function show_preview_form($msg = '', $body = '')
{
	global $vars, $rows, $cols;
	$s_refer  = h($vars['refer']);
	$s_digest = h($vars['digest']);
	$s_body   = h($body);
	$form  = '';
	$form .= $msg . "\n";
	$form .= '<form action="' . get_script_uri() . '?cmd=preview" method="post">' . "\n";
	$form .= '<div>' . "\n";
	$form .= ' <input type="hidden" name="refer"  value="' . $s_refer . '" />' . "\n";
	$form .= ' <input type="hidden" name="digest" value="' . $s_digest . '" />' . "\n";
	$form .= ' <textarea name="msg" rows="' . $rows . '" cols="' . $cols . '" id="textarea">' . $s_body . '</textarea><br />' . "\n";
	$form .= '</div>' . "\n";
	$form .= '</form>' . "\n";
	return $form;
}

// html特殊文字をエスケープ(XSS対策)
//----------------------------------------------------------------------------------
function h($str){
	if(is_array($str)){
		return array_map("h",$str);
	}else{
		return htmlspecialchars($str,ENT_QUOTES,"UTF-8");
	}
}

// Cookieのを利用して3日間は同じ項目の評価を禁止(連投規制)
//----------------------------------------------------------------------------------
function is_continuous_raty($page, $raty_id)
{
	$cmd = 'raty';
	$ratykey = $cmd . '_' . $page . '_' . $raty_id;

	// 有効なCookieを持っている場合(前回の投稿から3日以内)
	if (isset($_COOKIE[$ratykey])) {
		return true;
	}

	// 有効なCookieを持っていない場合
	$matches = array();
	preg_match('!(.*/)!', $_SERVER['REQUEST_URI'], $matches);
	setcookie($ratykey, 1, time()+(60*60*24*3), $matches[0]);
	return false;
}

コメントでざっくり解説しているので、このページを読んでくれた方には理解できると思います。

このプラグインはpukiwiki.skin.phpjQueryを読み込んでいる前提です。
もしサイトでjQueryを利用しておらず、このプラグインでだけjQueryを利用するという場合は57~59行目のコメントアウトを削除して有効にしてください。

さらにプラグインで利用する画像をPukiWikiの「image」ディレクトリに保存。今回の表示で使うのは「star-on.png、star-off.png、star-half.png」の3つです。
デフォルトのままでも良かったのですが、そこはデザイナーのこだわりとしてサイトに合わせてフラットデザインに変更しました。

サンプルはこちら:アクアリウムWikiのCO2ビートルカウンターページ

個人用として作ったので問題ありませんが、スコアをオプションでなく、文字列として渡したほうが拡張性があったなと今更ながら反省していますw

私自身きちんとしたプラグインを作ったのは初めてなので、なにか問題点があったらコメント等で教えて下さい。