--- /dev/null
+<?php
+namespace sfjp\Wiki\Exception;
+class Base extends \RuntimeException {}
\ No newline at end of file
--- /dev/null
+<?php
+namespace sfjp\Wiki\Exception;
+class Internal_Error extends Base {}
+
--- /dev/null
+<?php
+namespace sfjp\Wiki\Exception;
+class Invalid_Argument extends Base {}
+
--- /dev/null
+<?php
+namespace sfjp\Wiki\Exception;
+class Plugin_Error extends Base {};
\ No newline at end of file
--- /dev/null
+<?php
+namespace sfjp\Wiki\Formatter;
+abstract class Base {
+ protected $processor;
+ function __construct() {
+ }
+
+ function __destruct() {
+ unset($this->processor);
+ }
+
+ public function cleanup() {}
+
+ public function reset() {}
+
+ public function setProcessor($proc) {
+ $this->processor = $proc;
+ }
+
+ public function getProcessor() {
+ return $this->processor;
+ }
+
+ public function setContext($c) {
+ return $this->getProcessor()->setContext($c);
+ }
+
+ public function getContext($key = null) {
+ return $this->getProcessor()->getContext($key);
+ }
+
+ public function __($text, $args=array()) {
+ return $this->getProcessor()->__($text, $args);
+ }
+
+ abstract public function raw_node($string);
+ abstract public function text_node($text);
+ abstract public function open_element($neme, $opt = null);
+ abstract public function close_element($name, $opt = null);
+
+ static public function escape_id_value($str) {
+ $str = preg_replace_callback('/[^A-Za-z0-9_:.-]+/',
+ create_function('$m', '$p = unpack("H*", $m[0]); return $p[1];'),
+ $str);
+ if (!ctype_alpha(substr($str, 0, 1))) $str = "badid-".$str;
+ return $str;
+ }
+}
\ No newline at end of file
--- /dev/null
+<?php
+namespace sfjp\Wiki\Formatter;
+class Buffered_DocNode {
+ public $name;
+ public $data;
+ public $parent;
+ public $children = array();
+
+ function __construct($name, $data=null) {
+ $this->name = $name;
+ $this->data = $data;
+ }
+
+ function __destruct() {
+ $this->destroy();
+ }
+
+ public function destroy() {
+ if (isset($this->children)) {
+ foreach ($this->children as $c) {
+ $c->destroy();
+ }
+ }
+ unset($this->parent);
+ unset($this->name);
+ unset($this->data);
+ unset($this->children);
+ }
+
+ public function __toString() {
+ return '';
+ }
+
+ public function getParent() {
+ return $this->parent;
+ }
+
+ public function getPrev() {
+ $prev = null;
+ $found = false;
+ foreach ($this->parent->children as $c) {
+ if ($c === $this) {
+ $found = true;
+ break;
+ }
+ $prev = $c;
+ }
+ return $found ? $prev : null;
+ }
+
+ public function getNext() {
+ $idx = null;
+ $list = $this->parent->children;
+ foreach ($list as $i => $c) {
+ if ($c === $this) {
+ $idx = $i;
+ break;
+ }
+ }
+ return (isset($i) && array_key_exists($idx+1, $list)) ? $list[$idx+1] : null;
+ }
+
+ public function hasChildren() {
+ return !empty($this->children);
+ }
+
+ public function addChild($name, $data=null) {
+ $myclass = get_called_class();
+ $child = new $myclass($name, $data);
+ $child->parent = $this;
+ $this->children []= $child;
+ return $child;
+ }
+}
+
+class Buffered_Delegator extends Base {
+ public $root_node;
+ public $cur_node;
+ public $formatter;
+
+ function __construct() {
+ parent::__construct();
+ $this->reset();
+ }
+
+ function __destruct() {
+ if ($this->root_node) $this->root_node->destroy();
+ unset($this->root_node);
+ unset($this->cur_node);
+ unset($this->formatter);
+ }
+
+ public function reset() {
+ if ($this->root_node) $this->root_node->destroy();
+ $this->root_node = new Buffered_DocNode('*ROOT');
+ $this->cur_node = $this->root_node;
+ if (!$this->formatter) {
+ $this->formatter = new HTML();
+ }
+ $this->formatter->reset();
+ }
+
+ public function cleanup() {
+ return $this->render();
+ }
+
+ public function render() {
+ return $this->render_node($this->root_node);
+ }
+
+ protected function render_node($node) {
+ $fmt = $this->formatter;
+ $ret = '';
+ if ($node->name === "*ROOT") {
+ # nothing
+ } elseif ($node->name === "*RAW") {
+ $ret .= $fmt->raw_node($node->data);
+ } elseif ($node->name === "*TEXT") {
+ $ret .= $fmt->text_node($node->data);
+ } else {
+ $ret .= $fmt->open_element($node->name, $node->data);
+ }
+ foreach ($node->children as $child) {
+ $ret .= $this->render_node($child);
+ }
+ if (substr($node->name, 0, 1) != "*") {
+ $ret .= $fmt->close_element($node->name, $node->data);
+ }
+ return $ret;
+ }
+
+ public function open_element($name, $opt=null) {
+ $this->cur_node = $this->cur_node->addChild($name, $opt);
+ return $this->cur_node;
+ }
+
+ public function close_element($name, $opt=null) {
+ // TODO: now all options are ignored.
+ $this->cur_node = $this->cur_node->parent;
+ return $this->cur_node;
+ }
+
+ public function text_node($text) {
+ return $this->cur_node->addChild('*TEXT', $text);
+ }
+
+ public function raw_node($string) {
+ return $this->cur_node->addChild('*RAW', $string);
+ }
+
+ public function _dump_tree() {
+ return $this->_inspect_node($this->root_node, 0);
+ }
+
+ public function _inspect_node($node, $level) {
+ $ret = '';
+ for ($i = 0; $level*2 > $i; $i++) {
+ $ret .= ' ';
+ }
+ $ret .= "+ [" . $node->name . "] (" . htmlspecialchars(substr(print_r($node->data, 1), 0, 80)) . ")<br />";
+ foreach ($node->children as $c) {
+ $ret .= $this->_inspect_node($c, $level+1);
+ }
+ return $ret;
+ }
+}
\ No newline at end of file
--- /dev/null
+<?php
+namespace sfjp\Wiki\Formatter;
+class HTML extends Base {
+ static public $singleton_tags = array('line', 'hr', 'image', 'img', 'newline', 'br');
+
+ public $tagmap;
+
+ function __construct() {
+ $this->tagmap = array(
+ "bold" => "strong",
+ "italic" => "em",
+ "strike" => "del",
+ "underline" => array('span', array('style' => 'text-decoration: underline;')),
+ "monospace" => "tt",
+ "superscript" => "sup",
+ "subscript" => "sub",
+ "table_row" => "tr",
+ "table_col" => "td",
+ "list_mark" => "ul",
+ "list_num" => "ol",
+ "list_roma" => array("ol", array('style' => 'list-style-type: lower-roman;')),
+ "list_ROMA" => array("ol", array('style' => 'list-style-type: upper-roman;')),
+ "list_alpha" => array("ol", array('style' => 'list-style-type: lower-alpha;')),
+ "list_ALPHA" => array("ol", array('style' => 'list-style-type: upper-alpha;')),
+ "list_item" => "li",
+ "quote" => array("blockquote", array("class" => "citation")),
+ "paragraph" => "p",
+ "heading1" => "h1",
+ "heading2" => "h2",
+ "heading3" => "h3",
+ "heading4" => "h4",
+ "heading5" => "h5",
+ "heading6" => "h6",
+ "line" => "hr",
+ "indent" => array('div', array('class' => 'indent')),
+ "image" => "img",
+ "newline" => "br",
+ "error" => array("span", array('class' => 'wiki-system-error')),
+ );
+ }
+
+ public function reset() {
+ ; # nothing
+ }
+
+ public function cleanup() {
+
+ }
+
+ public function raw_node($string) {
+ return $string;
+ }
+
+ public function text_node($text) {
+ return htmlspecialchars($text);
+ }
+
+ public function open_element($name, $opt=null) {
+ if (!isset($opt)) $opt = array();
+ switch ($name) {
+ case "bolditalic":
+ return self::tag_builder("strong", $opt)."<em>";
+ case "table":
+ return
+ self::tag_builder("table",
+ array_merge(array("class" => "wikitable"),
+ $opt))
+ . '<tbody>';
+ case "link":
+ return self::tag_builder("a", $opt);
+ default:
+ if (array_key_exists($name, $this->tagmap)) {
+ $taginfo = self::merge_tagopt($this->tagmap[$name], $opt);
+ return self::tag_builder($taginfo[0], $taginfo[1]);
+ } else {
+ return self::merge_tagopt($name, $opt);
+ }
+ }
+ }
+
+ public function close_element($name, $opt=array()) {
+ if (in_array($name, static::$singleton_tags))
+ return ''; # no close tag.
+ switch ($name) {
+ case "bolditalic":
+ return "</em></strong>";
+ case "table":
+ return "</tbody></table>";
+ case "link":
+ return "</a>";
+ default:
+ if (array_key_exists($name, $this->tagmap)) {
+ $taginfo = self::merge_tagopt($this->tagmap[$name], $opt);
+ return self::tag_builder($taginfo[0], $taginfo[1], false);
+ } else {
+ return "</$name>";
+ }
+ }
+ }
+
+ public function element($name, $text = '', $opts = array()) {
+ return $this->open_element($name, $opts) .
+ $this->text_node($text) .
+ $this->close_element($name, $opts);
+ }
+
+ static protected function merge_tagopt($taginfo, $newopt) {
+ if (!is_array($taginfo))
+ $taginfo = array($taginfo, array());
+ if (!isset($newopt)) return $taginfo;
+ $taginfo[1] = $taginfo[1] + $newopt;
+ return $taginfo;
+ }
+
+ static function tag_builder($name, $opt=null, $open=true) {
+ $ret = "<".($open ? '' : '/').htmlspecialchars($name);
+
+ if ($name == "a" && array_key_exists("href", $opt) &&
+ !(array_key_exists("pass_unsecure", $opt) && $opt["pass_unsecure"])) {
+ $opt["href"] = preg_replace('/^(javascript|telnet|tel):/i', '', $opt["href"]);
+ }
+
+ if ($open && isset($opt) && is_array($opt) && count($opt) > 0 ) {
+ foreach($opt as $pname => $pvalue) {
+ if ($pname === "id") {
+ $pvalue = self::escape_id_value($pvalue);
+ }
+ if (isset($pvalue)) {
+ $ret .= " " . htmlspecialchars($pname) . '="'. htmlspecialchars($pvalue) .'"';
+ } else {
+ $ret .= " " . htmlspecialchars($pname);
+ }
+ }
+ } else if ($open && isset($opt) && !empty($opt)) {
+ $ret .= " $opt";
+ }
+
+ $close = in_array($name, static::$singleton_tags) ? ' />' : '>';
+ return $ret . $close;
+ }
+}
\ No newline at end of file
--- /dev/null
+<?php
+namespace sfjp\Wiki;
+
+class Parser {
+ public $text;
+ public $processor;
+
+ function __construct($context = null, $processor = null, $formatter = null) {
+ if ($processor) {
+ $this->processor = $processor;
+ } else {
+ $this->processor = new Processor\Trac();
+ }
+ if ($context)
+ $this->processor->setContext($context);
+ if ($formatter)
+ $this->processor->setFormatter($formatter);
+ $storage_class = $this->processor->getContext('storage.class');
+ if (!$storage_class) $storage_class = 'Dummy';
+ $c = new \ReflectionClass("sfjp\\Wiki\\Storage\\{$storage_class}");
+ $storage = $c->newInstance();
+ $this->processor->setContext(array('storage' => $storage));
+ }
+
+ public function parse($text) {
+ $this->processor->reset();
+ return $this->processor->process($text)->getFormattedText();
+ }
+
+ public function getFormattedText() {
+ return $this->processor->getFormattedText();
+ }
+
+ public function isVary() {
+ return $this->processor->isVary();
+ }
+
+ public function setContext($c) {
+ $this->processor->setContext($c);
+ }
+
+ public function getContext($key = null) {
+ return $this->processor->getContext($key);
+ }
+
+ public function removeContext($key) {
+ $this->processor->removeContext($key);
+ }
+
+ public function clearContext() {
+ $this->processor->clearContext();
+ }
+
+ public function reset() {
+ $this->processor->reset();
+ }
+}
--- /dev/null
+<?php
+namespace sfjp\Wiki\Plugin;
+use sfjp\Wiki\Exception\Plugin_Error;
+abstract class Base {
+ public $is_vary = true;
+ public $is_block = false;
+ public $processor;
+
+ function __construct($processor) {
+ $this->processor = $processor;
+ }
+
+ public function setContext($ctx) {
+ return $this->processor->setContext($ctx);
+ }
+
+ public function getContext($key=null) {
+ return $this->processor->getContext($key);
+ }
+
+ public function error($msg) {
+ $this->setContext(array('whole_page_cachable' => false));
+ throw new Plugin_Error($msg);
+ }
+
+ public function __($text, $args=array()) {
+ if (!$this->processor->hasContext('i18n')) {
+ return $text;
+ } else {
+ return $this->getContext('i18n')->__($text, $args);
+ }
+ }
+
+ public function getProcessor() {
+ return $this->processor;
+ }
+
+ public function getFormatter() {
+ return $this->getProcessor()->getFormatter();
+ }
+
+ public function parseArgs($args, $list = array(), $default = null) {
+ $opt = array();
+ foreach ($args as $arg) {
+ if (strpos($arg, '=')) {
+ list($key, $value) = explode('=', $arg, 2);
+ if (!$key)
+ continue;
+ } elseif (isset($default) && !empty($default)) {
+ $key = $default;
+ $value = $arg;
+ }
+ if (!in_array($key, $list) && !array_key_exists($key, $list)) {
+ $this->error("unknown argument $arg");
+ }
+ $opt[$key] = $value;
+ }
+ return $opt;
+ }
+
+ public function render_table($data) {
+ $fmt = $this->getFormatter();
+ $ret = '';
+
+ $ret .= $fmt->open_element('table');
+ foreach ($data as $cols) {
+ $ret .= $fmt->open_element('table_row');
+ foreach ($cols as $col) {
+ $ret .= $fmt->open_element('table_col');
+ $ret .= $col;
+ $ret .= $fmt->close_element('table_col');
+ }
+ $ret .= $fmt->close_element('table_row');
+ }
+ $ret .= $fmt->close_element('table');
+ return $fmt->raw_node($ret);
+ }
+
+ public function render_date_separated_list($data) {
+ $wikistr = '';
+ $last_date = null;
+
+ foreach ($data as $item) {
+ if ($last_date != date('Y-m-d', $item[0]))
+ $wikistr .= "==== " . date('Y-m-d', $item[0]) . " ====\n";
+ $last_date = date('Y-m-d', $item[0]);
+ $wikistr .= " * " . $item[1];
+ }
+ $p = new \sfjp\Wiki\Parser($this->getContext());
+ return $this->getFormatter()->raw_node($p->parse($wikistr));
+ }
+
+}
\ No newline at end of file
--- /dev/null
+<?php
+namespace sfjp\Wiki\Plugin;
+use sfjp\Wiki\Exception\Plugin_Error;
+class Br extends Base {
+ public $is_vary = false;
+ public function process($args) {
+ $opt = '';
+ if (isset($args[0]) && !empty($args[0])) {
+ list ($key, $val) = explode('=', strtolower(trim($args[0])));
+ if ($key == "clear" && ($val == 'left' || $val == 'all' || $val == 'right')) {
+ $opt = " clear=\"${val}\"";
+ }
+ }
+ return $this->getFormatter()->open_element('newline');
+ }
+}
\ No newline at end of file
--- /dev/null
+<?php
+namespace sfjp\Wiki\Plugin;
+use sfjp\Wiki\Exception\Plugin_Error;
+class FootNoteProc {
+ public $count = 0;
+ public $message = '';
+ public $formatter;
+
+ public function process() {
+ $fmt = $this->formatter;
+ $ret = '';
+ if ($this->count == 1) {
+ $ret .= $fmt->raw_node('<hr><ol id="footnote" class="footnote">');
+ }
+ $ret .= $fmt->raw_node("<li id=\"_fn_note-{$this->count}\">"
+ . "<a class=\"footnote-revref footnote-counter\" href=\"#_fn_ref-{$this->count}\">*{$this->count}</a>"
+ . "{$this->message}</li>");
+ if (FootNote::$counter - 1 == $this->count) {
+ $ret .= $fmt->raw_node('</ol>');
+ }
+ return $ret;
+ }
+}
+
+class FootNote extends Base {
+ static public $counter = 1;
+ public $is_vary = false;
+ public function process($args) {
+ $oneline_trac = new \sfjp\Wiki\Processor\Trac_oneline();
+ $oneline_trac->enable_plugin = false;
+
+ $proc = new FootNoteProc();
+ $proc->count = self::$counter++;
+ $proc->formatter = $this->getFormatter();
+ $proc->message = $oneline_trac->process($args[0])->getFormattedText();
+
+ $this->processor->addPostProc($proc);
+
+ return $this->getFormatter()->raw_node(
+ "<span id=\"_fn_ref-{$proc->count}\" class=\"footnote-ref\">"
+ . "<a href=\"#_fn_note-{$proc->count}\">*{$proc->count}</a></span>");
+ }
+}
\ No newline at end of file
--- /dev/null
+<?php
+namespace sfjp\Wiki\Plugin;
+use sfjp\Wiki\Exception\Plugin_Error;
+class PageBreak extends Base {
+ public $is_vary = false;
+ public $is_block = true;
+ public function process($args) {
+ return $this->getFormatter()->raw_node('<div style="page-break-after: always;"></div>');
+ }
+}
\ No newline at end of file
--- /dev/null
+<?php
+namespace sfjp\Wiki\Plugin;
+use sfjp\Wiki\Exception\Plugin_Error;
+class PageOutline extends Base {
+ public $is_vary = false;
+ public $is_block = true;
+ public function process($args) {
+ $proc = $this->processor;
+ $opt = array('start' => 1, 'depth' => 3, 'type' => 'ordered');
+ $opt['title'] = $this->__('Outline');
+ $counter = array();
+
+ foreach ($args as $arg) {
+ if (!strpos($arg, '='))
+ return $this->error("unknown argument $arg");
+ list($key, $value) = explode('=', $arg, 2);
+ if (!$key || !array_key_exists($key, $opt))
+ return $this->error("unknown argument $arg");
+ $opt[$key] = $value;
+ }
+
+ $text = "";
+ $match = array();
+ $orig = $proc->getText();
+ $orig = preg_replace('/^{{{.*?^}}}/sm', '', $orig);
+ foreach($proc->getBlockstyleRules() as $r) {
+ if ($r[0] != "heading")
+ continue;
+ $headre = $r[1];
+ break;
+ }
+ $parser = new \sfjp\Wiki\Parser(array());
+ preg_match_all("/$headre/m", $orig, $match);
+ for($h = 0; array_key_exists($h, $match[1]); $h++) {
+ $level = strlen($match[1][$h]);
+ $label = preg_replace('/<.*?>/', '', $parser->parse(preg_replace('/\[\[.*?\]\]/', '', $match[0][$h])));
+ if (isset($match[3][$h]) && strlen($match[3][$h])) {
+ $linkid = trim($match[3][$h]);
+ } else {
+ $linkid = "h" . ($level + intval($proc->getContext('head_excess'))) . "-";
+ $linkid .= preg_replace_callback($parser->processor->getInlinestyleRegex(),
+ create_function('$m', 'return empty($m[1]) ? "" : substr($m[0], 1);'),
+ trim($match[2][$h]));
+ }
+ if (!isset($counter)) $counter[$linkid] = 0;
+ $c =& $counter[$linkid];
+ if (++$c > 1) $linkid .= "-{$c}";
+ $linkid = str_replace('%', '.', rawurlencode($linkid));
+
+ $outlevel = $level - ($opt['start']-1);
+ if ($outlevel > 0 && $outlevel <= $opt['depth'])
+ $text .= sprintf("%".($outlevel*2)."s%s [#%s %s]\n", '',
+ ($opt['type'] == 'unordered' ? '*' : '1.'),
+ $linkid, htmlspecialchars_decode($label));
+ }
+ return $this->getFormatter()->raw_node(
+ '<div class="pageoutline"><div class="pageoutline-title">'
+ . '<div class="action">'
+ . '<button type="button" onClick="javascript:togglePageOutline(this)">'
+ . '<img src="//static.sourceforge.jp/wiki/images/icons/roll-up.gif" border="0"></button>'
+ . '</div>'
+ . $this->__($opt['title'])
+ . '</div>' . $parser->parse($text) . '</div>');
+ }
+
+
+}
\ No newline at end of file
--- /dev/null
+<?php
+namespace sfjp\Wiki\Processor;
+class Auto extends Base {
+ public function process($text) {
+ $match = null;
+ $typemap = array();
+ if (preg_match('/\A#!\/\S+\/(?:env\s+)?(ada|asm|asw|bash|csh|perl|python|ruby|sh|tcsh|zsh|php)/',
+ $text, $match)) {
+ $type = array_key_exists($match[1], $typemap) ? $typemap[$match[1]] : $match[1];
+ $p = new Code(array($type));
+ } else {
+ $p = new Pre();
+ }
+ $p->setContext($this->getContext());
+ $p->setFormatter($this->getFormatter());
+ $this->formatted_text = $p->process($text)->getFormattedText();
+ if (!$this->hasContext("parent_processor"))
+ $this->formatted_text .= $this->getFormatter()->cleanup();
+ return $this;
+ }
+}
--- /dev/null
+<?php
+namespace sfjp\Wiki\Processor;
+use sfjp\Wiki\Exception;
+abstract class Base {
+ private $context;
+ public $is_vary;
+ public $text;
+ public $formatted_text;
+ public $preproc;
+ public $postproc;
+ public $formatter;
+ public $args = array();
+
+ protected static $reserved = array('__halt_compiler', 'abstract', 'and', 'array', 'as', 'break', 'callable', 'case', 'catch', 'class', 'clone', 'const', 'continue', 'declare', 'default', 'die', 'do', 'echo', 'else', 'elseif', 'empty', 'enddeclare', 'endfor', 'endforeach', 'endif', 'endswitch', 'endwhile', 'eval', 'exit', 'extends', 'final', 'for', 'foreach', 'function', 'global', 'goto', 'if', 'implements', 'include', 'include_once', 'instanceof', 'insteadof', 'interface', 'isset', 'list', 'namespace', 'new', 'or', 'print', 'private', 'protected', 'public', 'require', 'require_once', 'return', 'static', 'switch', 'throw', 'trait', 'try', 'unset', 'use', 'var', 'while', 'xor');
+
+ protected static $context_key_rename_rules = array(
+ 'plugin_include_path' => 'extension.additional_include_path',
+ 'plugin.order' => 'extension.acl_order',
+ 'plugin.allow' => 'extension.allow',
+ 'plugin.deny' => 'extension.deny',
+ 'supress_plugin_error' => 'extension.hide_error',
+ );
+
+ function __construct($args = null) {
+ $default_ctx = array("wiki_baseurl" => ".",
+ "site_root_url" => (empty($_SERVER['HTTPS']) ? 'http' : 'https') . "://" . (empty($_SERVER['HTTP_HOST']) ? 'localhost.localdomain' : $_SERVER['HTTP_HOST']),
+ "svn_base_url" => "http://svn.sourceforge.jp/view",
+ "cvs_base_url" => "http://cvs.sourceforge.jp/view",
+ "sfjp.group_name" => null,
+ "sfjp.group_id" => null,
+ "extension.additional_include_path" => null,
+ "extension.acl_order" => "deny,allow",
+ "extension.allow" => array(),
+ "extension.deny" => array(),
+ "extension.hide_error" => false,
+ "self_url" => array_key_exists('REQUEST_URI', $_SERVER) ? $_SERVER['REQUEST_URI'] : '',
+ "cache_manager" => null,
+ "cache_depends" => array(),
+ "whole_page_cachable" => true,
+ "head_excess" => 0,
+ "gen_head_id" => true,
+ "internal_url_regex" => null,
+ "nofollow_on_external_links" => true,
+ );
+ $this->context = $default_ctx;
+ $this->clearPreProc();
+ $this->clearPostProc();
+ }
+
+ function __destruct() {
+ unset($this->formatter);
+ }
+
+ public function setArgs($args) {
+ if (!is_array($args))
+ $args = array($args);
+ $this->args = $args;
+ }
+
+ public function getArgs($pos = null) {
+ if (is_null($pos))
+ return $this->args;
+ return $this->args[$pos];
+ }
+
+ public function getContext($key = null) {
+ if (isset($key)) {
+ $key = static::renameContextKey($key);
+ return array_key_exists($key, $this->context) ? $this->context[$key] : null;
+ } else {
+ return $this->context;
+ }
+ }
+
+ static public function renameContextKey($key) {
+ if (!array_key_exists($key, static::$context_key_rename_rules))
+ return $key;
+ return static::$context_key_rename_rules[$key];
+ }
+
+
+ public function hasContext($key) {
+ return array_key_exists(static::renameContextKey($key), $this->context);
+ }
+
+ public function setContext($c) {
+ foreach ($c as $key => $val) {
+ $key = static::renameContextKey($key);
+ $this->context[$key] = $val;
+ }
+ $this->context = array_merge($this->context, $c);
+ }
+
+ public function removeContext($key) {
+ unset($this->context[static::renameContextKey($key)]);
+ }
+
+ public function clearContext() {
+ $this->context = array();
+ }
+
+ public function incrementCounter($name) {
+ $c = &$this->getCounter($name);
+ return ++$c;
+ }
+
+ public function &getCounter($name) {
+ $c = &$this->context["counters"];
+ if (!isset($c[$name])) {
+ $c[$name] = 0;
+ }
+ return $c[$name];
+ }
+
+ public function setCounter($name, $val) {
+ $c = &$this->context["counters"];
+ $c[$name] = $val;
+ }
+
+ public function clearCounter($name = null) {
+ if (isset($name)) {
+ $this->setCounter($name, 0);
+ } else {
+ $this->setContext(array("counters" => array()));
+ }
+ }
+
+ public function &getCacheDepends() {
+ return $this->context["cache_depends"];
+ }
+
+ public function addCacheDepends($deps) {
+ if (empty($deps)) return;
+ if (!is_array($deps)) $deps = array($deps);
+ $c = &$this->context["cache_depends"];
+ foreach ($deps as $dep) {
+ $c []= $dep;
+ }
+ }
+
+ public function clearCacheDepends() {
+ $this->context["cache_depends"] = array();
+ }
+
+ public function reset() {
+ $this->is_vary = false;
+ $this->text = "";
+ $this->formatted_text = "";
+ $this->clearCounter();
+ $this->clearPreProc();
+ $this->clearPostProc();
+ $this->clearCacheDepends();
+ if ($this->getFormatter())
+ $this->getFormatter()->reset();
+ }
+
+ public function isVary() {
+ return $this->is_vary;
+ }
+
+ public function setText($text) {
+ $old = $this->text;
+ $this->text = $text;
+ return $old;
+ }
+
+ public function getText() {
+ return $this->text;
+ }
+
+ public function getFormattedText() {
+ $ret = '';
+ foreach ($this->preproc as $p) {
+ $ret .= $p->process();
+ }
+ $ret .= $this->formatted_text;
+ foreach ($this->postproc as $p) {
+ $ret .= $p->process();
+ }
+ return $ret;
+ }
+
+ public function get_plugin_instance($class_name) {
+ $check_name = str_replace('\\', '/', $class_name);
+ $name = basename($check_name);
+
+ if (!$this->check_plugin_allowed($check_name))
+ throw new Exception\Plugin_Error("Load Denied: $name");
+ if (!preg_match('/^[A-Za-z0-9._-]+$/', $name))
+ throw new Exception\Plugin_error("Wrong Plugin Name: $name");
+
+ if (in_array(strtolower($name), self::$reserved)) {
+ $class_name = substr($class_name, 0, strlen($class_name) - strlen($name)) . "_{$name}";
+ }
+
+ try {
+ $orig_include_path = null;
+ if ($this->getContext('extension.additional_include_path')) {
+ $orig_include_path = ini_get('include_path');
+ ini_set('include_path', $this->getContext('extension.additional_include_path').":$orig_include_path");
+ }
+ $class = new \ReflectionClass("\\sfjp\\Wiki\\{$class_name}");
+ if ($orig_include_path)
+ ini_set('include_path', $orig_include_path);
+ $instance = $class->newInstance($this);
+ if (!is_callable(array($instance, "process")))
+ throw new \ReflectionException();
+ } catch (\ReflectionException $e) {
+ ini_set('include_path', $orig_include_path);
+ error_log("Plugin '$name' load failed.");
+ throw new Exception\Plugin_Error("Not Found: {$name}");
+ }
+ return $instance;
+ }
+
+
+
+ protected function check_plugin_allowed($name) {
+ $order = $this->getContext("plugin.order");
+ $allow = $this->getContext("plugin.allow");
+ $deny = $this->getContext("plugin.deny");
+ $ret = false;
+ if (!$order) $order = "deny,allow";
+ $order = strtolower($order);
+ if (!is_array($allow)) $allow = array($allow);
+ if (!is_array($deny)) $deny = array($deny);
+ $deny = array_map('strtolower', $deny);
+ $allow = array_map('strtolower', $allow);
+ $name = strtolower($name);
+
+ if ($order === "deny,allow") {
+ $ret = true;
+ if (in_array('all', $deny)) $ret = false;
+ if (in_array($name, $deny)) $ret = false;
+ if (in_array('all', $allow)) return true;
+ if (in_array($name, $allow)) return true;
+ } else { # "allow,deny"
+ $ret = false;
+ if (in_array('all', $allow)) $ret = true;
+ if (in_array($name, $allow)) $ret = true;
+ if (in_array('all', $deny)) return false;
+ if (in_array($name, $deny)) return false;
+ }
+ return $ret;
+ }
+
+ public function addPreProc($obj) {
+ $this->preproc[] = $obj;
+ }
+
+ public function clearPreProc() {
+ $this->preproc = array();
+ }
+
+ public function addPostProc($obj) {
+ $this->postproc[] = $obj;
+ }
+
+ public function clearPostProc() {
+ $this->postproc = array();
+ }
+
+ public function getFormatter() {
+ return $this->formatter;
+ }
+
+ public function setFormatter($f) {
+ $this->formatter = $f;
+ if (!$this->formatter->getProcessor())
+ $this->formatter->setProcessor($this);
+ }
+
+ public function __($text, $args=array()) {
+ if (!$this->hasContext('i18n')) {
+ return $text;
+ } else {
+ return $this->getContext('i18n')->__($text, $args);
+ }
+ }
+}
--- /dev/null
+<?php
+namespace sfjp\Wiki\Processor;
+require_once 'include/general/syntax-highlight.php';
+class Code extends Base {
+ public function process($text) {
+ $this->setText($text);
+ $lang = $this->getArgs(0);
+ $fmt = $this->getFormatter();
+ $ret = $fmt->raw_node(highlight_syntax(trim($text, "\r\n"), $lang));
+ if (!$this->hasContext("parent_processor"))
+ $ret .= $this->getFormatter()->cleanup();
+ $this->formatted_text = $ret;
+ return $this;
+ }
+}
--- /dev/null
+<?php
+namespace sfjp\Wiki\Processor;
+class Comment extends Base {
+ public function process($text) {
+ $this->formatted_text = '';
+ return $this;
+ }
+}
\ No newline at end of file
--- /dev/null
+<?php
+namespace sfjp\Wiki\Processor;
+class Denied extends Base {
+ function __construct($args = null) {
+ parent::__construct();
+ $this->name = $args[0];
+ }
+
+ public function process($text) {
+ $this->setContext(array('whole_page_cachable' => false));
+ if ($this->getContext("suppress_plugin_error")) {
+ $this->formatted_text = '';
+ } else {
+ $this->formatted_text = '<span class="wiki-system-error">[Plugin Load Denied: ' . htmlspecialchars($this->name) . ']</span>';
+ }
+ return $this;
+ }
+}
\ No newline at end of file
--- /dev/null
+<?php
+namespace sfjp\Wiki\Processor;
+if (!function_exists("kses"))
+ require_once("kses.php");
+
+class HTML extends Base {
+ static public $allowed = array(
+ 'h1' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'align' => 1),
+ 'h2' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'align' => 1),
+ 'h3' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'align' => 1),
+ 'h4' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'align' => 1),
+ 'h5' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'align' => 1),
+ 'h6' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'align' => 1),
+ 'div' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'align' => 1),
+ 'span' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'align' => 1),
+ 'address' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+ 'em' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+ 'strong' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+ 'dfn' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+ 'code' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+ 'samp' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+ 'kbd' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+ 'var' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+ 'abbr' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+ 'acronym' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+ 'blockquote' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'cite' => 1),
+ 'q' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'cite' => 1),
+ 'sub' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+ 'sup' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+ 'p' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'align' => 1),
+ 'br' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'clear' => 1),
+ 'pre' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'width' => 1),
+ 'ins' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'cite' => 1, 'datetime' => 1),
+ 'del' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'cite' => 1, 'datetime' => 1),
+ 'ul' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'type' => 1),
+ 'ol' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'type' => 1, 'start' => 1),
+ 'li' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'type' => 1, 'value' => 1),
+ 'dl' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+ 'dt' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+ 'dd' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+ 'table' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'width' => 1, 'summary' => 1, 'border' => 1, 'cellspacing' => 1, 'cellpadding' => 1, 'align' => 1, 'valign' => 1, 'bgcolor' => 1),
+ 'tbody' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'align' => 1, 'valign' => 1),
+ 'thead' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'align' => 1, 'valign' => 1),
+ 'tfoot' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'align' => 1, 'valign' => 1),
+ 'colgroup' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'width' => 1, 'span' => 1, 'align' => 1, 'valign' => 1),
+ 'col' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'width' => 1, 'span' => 1, 'align' => 1, 'valign' => 1),
+ 'tr' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'align' => 1, 'valign' => 1, 'bgcolor' => 1),
+ 'th' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'align' => 1, 'valign' => 1, 'abbr' => 1, 'axis' => 1, 'headers' => 1, 'scope' => 1, 'rowspan' => 1, 'colspan' => 1, 'bgcolor' => 1),
+ 'td' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'align' => 1, 'valign' => 1, 'abbr' => 1, 'axis' => 1, 'headers' => 1, 'scope' => 1, 'rowspan' => 1, 'colspan' => 1, 'bgcolor' => 1),
+ 'a' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'href' => 1, 'name' => 1, 'rel' => 1, 'rev' => 1, 'accesskey' => 1, 'tabindex' => 1, 'title' => 1),
+ 'img' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'src' => 1, 'alt' => 1, 'name' => 1, 'height' => 1, 'width' => 1, 'border' => 1, 'title' => 1, 'align' => 1),
+ 'tt' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+ 'i' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+ 'b' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+ 'big' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+ 'small' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+ 's' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+ 'u' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+ 'font' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'size' => 1, 'color' => 1, 'face' => 1),
+ 'hr' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1), 'noshade' => 1, 'align' => 1, 'size' => 1, 'width' => 1),
+ 'wbr' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+ 'nobr' => array('id' => 1, 'class' => 1, 'style' => array('cssfilter' => 1)),
+ );
+
+ public function __construct($args = null) {
+ parent::__construct();
+ }
+
+ public function process($text) {
+ $this->formatted_text = $this->getFormatter()->raw_node(kses($text, self::$allowed, array('http', 'https', 'ftp')));
+ return $this;
+ }
+}
--- /dev/null
+<?php
+namespace sfjp\Wiki\Processor;
+class Pre extends Base {
+ public function process($text) {
+ $fmt = $this->getFormatter();
+ $this->formatted_text = $fmt->raw_node("<pre>" . htmlspecialchars($text) . "</pre>");
+ if (!$this->hasContext("parent_processor"))
+ $this->formatted_text .= $fmt->cleanup();
+ return $this;
+ }
+}
\ No newline at end of file
--- /dev/null
+<?php
+namespace sfjp\Wiki\Processor;
+use sfjp\Wiki\Formatter;
+use sfjp\Wiki\Exception;
+class Trac extends Base {
+ protected $inlinestyle_rules = array(
+ array("bolditalic", "'''''"),
+ array("bold", "'''", ''),
+ array("italic", "(?<!')''"),
+ array("underline", "__"),
+ array("strike", "~~"),
+ array("subscript", ",,"),
+ array("superscript", "\^"),
+ array("monospace", "`"),
+ );
+
+ protected $blockstyle_rules = array(
+ array("list", '^( +)(\*|(?:\d+|[ivx]+|[IVX]+|[a-z]+|[A-Z]+)\.) '),
+ array("escaped", '^ +(!)(\*|(?:\d+|[ivx]+|[IVX]+|[a-z]+|[A-Z]+)\.) '),
+ array("quote", "^(>(?:\s*>)*) "),
+ array("escaped", "^(!)>+ "),
+ array("heading", '^(=+)\s+(.*?)(?:\s+\1)?(?:\s+#(\S+))?\s*\r?$'),
+ array("escaped", '^(!)=+ '),
+ array("define_list", '^(.+?)::\r?$'),
+ array("line", '^----+'),
+ array("escaped", '^(!)----'),
+ array("table", '^\|\|(.*\|\|)+\r?$'),
+ array("escaped", '^(!)\|\|'),
+ array("clear_all", '^\r?$'),
+ );
+
+ protected $uri_rules = array(
+ array('(?:https?|ftp)', '\/\/[^\x00-\x20"<>]+'),
+ array('wiki', '(?:"[^\x00-\x1f\x22\x27\x5b-\x5d\x60:]+"|(?:[a-z0-9-]+:)?[^\x00-\x22\x24-\x2c\x3a-\x40\x5b-\x5e\x60\x7b-\x7e]+)'),
+ array('(?:tracker|ticket)', '\d+'),
+ array('(?:cvs|svn)', '\S+'),
+ array('(?:id|users?)', '[a-zA-Z0-9_-]+'),
+ array('comment', '\d+:\d+:\d+'),
+ array('release', '\S+'),
+ array('isbn', '[A-Za-z0-9-]+'),
+ array('prweb', '[\x21\x23-\x26\x28-\x3b\x3d\x3f-\x5a\x5e-\x7e]+'),
+ array('projects?', '[a-z0-9-]+'),
+ array('mailto', '[!#$%&*+\/=?^_+~0-9A-Za-z.-]+@[0-9A-Za-z.-]+'),
+ );
+
+ public $block_stack;
+ public $inline_stack;
+ protected $_pending_paragraph;
+
+ protected static $_static_var_cache = array();
+
+ function __construct($args = null) {
+ parent::__construct($args);
+ $this->formatter = new Formatter\HTML();
+ $this->formatter->setProcessor($this);
+ // TODO: really OK? This makes cyclic reference. PHP don't support weak ref?
+ $this->const_cache_base = "SFJP_WIKIPROCESSOR_CACHE";
+ $this->interwiki_processor = null;
+ $this->reset();
+ }
+
+ public function getCurrentLine() {
+ return $this->current_line;
+ }
+
+ public function getCurrentLineNo() {
+ return $this->current_lineno;
+ }
+
+ protected function getConstCache($name) {
+ $const_name = $this->const_cache_base . "_${name}";
+ return defined($const_name) ? constant($const_name) : null;
+ }
+
+ protected function setConstCache($name, $value) {
+ $const_name = $this->const_cache_base . "_${name}";
+ if (defined($const_name))
+ throw new Exception\InternalError("[BUG] const '$name' was already initialized.");
+ define($const_name, $value);
+ return $value;
+ }
+
+ public function getUriRegex($complete = true) {
+ $ret = $this->getConstCache("uri_regex");
+ if (isset($ret))
+ return $complete ? "/(!)?${ret}/" :$ret;
+
+ $ret = $this->getConstCache("uri_regex");
+
+ $regex = array();
+ foreach ($this->uri_rules as $rule) {
+ $regex []= $rule[0].":".$rule[1];
+ }
+ $ret = "(?:" . join('|', $regex) . ")";
+
+ $this->setConstCache("uri_regex", $ret);
+ return $complete ? "/(!)?${ret}/" :$ret;
+ }
+
+ public function getInlinestyleRegex($complete = true) {
+ $ret = $this->getConstCache("inlinestyle_regex");
+ if (isset($ret))
+ return $complete ? "/(!)?${ret}/" :$ret;
+
+ $regex = '(?:' .
+ join('|',
+ array_map(create_function('$a', 'return "($a[1])";'),
+ $this->inlinestyle_rules))
+ . ')';
+
+ $ret = $this->setConstCache("inlinestyle_regex", $regex);
+ return $complete ? "/(!)?${ret}/" :$ret;
+ }
+
+ public function getBlockstyleRules() {
+ return $this->blockstyle_rules;
+ }
+
+ public function reset() {
+ parent::reset();
+ $this->block_stack = array();
+ $this->inline_stack = array();
+ $this->current_lineno = 0;
+ $this->current_line = "";
+ $this->_pending_paragraph = false;
+ }
+
+ public function getInterwikiProcessor() {
+ return $this->interwiki_processor;
+ }
+
+ public function setInterwikiProcessor($f) {
+ $this->interwiki_processor($f);
+ }
+
+ /**
+ * process
+ *
+ * @param mixed $string
+ * @access public
+ * @return return self instance
+ */
+ public function process($text = null) {
+ $ret = "";
+ $buf_proc = "";
+ if (isset($text))
+ $this->text = $text;
+ $child_processor = null;
+
+ $lines = preg_split("/(?<=\n)/", $this->text);
+ foreach ($lines as $line) {
+ $this->current_lineno++;
+ $this->current_line = str_replace("\r", "", $line);
+ if (isset($child_processor)) {
+ if (preg_match('/^\}\}\}/', $line)) {
+ $ret .= $child_processor->process($buf_proc)->getFormattedText();
+ $child_processor->setContext(array("parent_processor" => null));
+ $child_processor = null;
+ $buf_proc = "";
+ continue;
+ }
+ $buf_proc .= $line;
+ continue;
+ }
+ if (preg_match('/^\{\{\{(?:\s+(.*))?$/', $line, $match)) {
+ $args = preg_split('/\s+/', trim($match[1]));
+ $name = (count($args) > 0 && $args[0]) ? strtolower(array_shift($args)) : 'auto';
+ $ret .= $this->clear_blockstyle('indent');
+ $ret .= $this->clear_blockstyle('table');
+ $ret .= $this->clear_blockstyle('define');
+
+ try {
+ $child_processor = $this->get_plugin_instance("Processor\\$name");
+ } catch (Exception\Plugin_Error $e) {
+ $orig_error = $e;
+ try {
+ $child_processor = $this->get_plugin_instance("Processor\\Pre");
+ } catch (Exception\Plugin_Error $e) {
+ $ret .= $this->format_plugin_error($orig_error);
+ continue;
+ }
+ }
+
+ $child_processor->setContext($this->getContext());
+ $child_processor->setContext(array("parent_processor" => $this));
+ $child_processor->setFormatter($this->getFormatter());
+ $child_processor->setArgs($args);
+ continue;
+ }
+ $ret .= $this->parse_line($line);
+ }
+ if (isset($child_processor)) {
+ $ret .= $child_processor->process($buf_proc)->getFormattedText();
+ }
+ $ret .= $this->clear_inlinestyle();
+ $ret .= $this->clear_blockstyle();
+ if (!$this->hasContext("parent_processor")
+ && !$this->getContext("disable_formatter_cleanup"))
+ $ret .= $this->getFormatter()->cleanup();
+ $this->formatted_text = $ret;
+ return $this;
+ }
+
+ protected function run_processor($name, $text) {
+ // TODO: select processor
+ $processor = new Pre();
+ return $processor->process($text);
+ }
+
+ public function parse_line($line) {
+ $match = array();
+ $name = null;
+ $pat = null;
+ $ret = "";
+ foreach ($this->blockstyle_rules as $rule) {
+ $name = $rule[0];
+ $pat = $rule[1];
+ if (preg_match("/$pat/", $line, $match, PREG_OFFSET_CAPTURE))
+ break;
+ }
+ if (count($match) == 0) {
+ $name = "_default";
+ } else {
+ $ret .= $this->clear_inlinestyle();
+ }
+
+ if (is_callable(array(&$this, 'process_block_'.$name))) {
+ $ret .= call_user_func(array(&$this, 'process_block_'.$name), $match, $line);
+ } else {
+ error_log("[BUG] can't process block '$name'.");
+ $ret .= $this->process_block__default($match, $line);
+ }
+
+ return $ret;
+ }
+
+ protected function process_block_heading($match, $line) {
+ $ret = "";
+ $level = strlen($match[1][0]) + intval($this->getContext('head_excess'));
+ $elem_level = $level <= 6 ? $level : 6;
+ $headid = "";
+ if (array_key_exists(3, $match)) {
+ $headid = trim($match[3][0]);
+ } else {
+ $headid = "h${level}-"
+ . preg_replace_callback($this->getInlinestyleRegex(),
+ create_function('$m', 'return empty($m[1]) ? "" : substr($m[0], 1);'),
+ trim($match[2][0]));
+ }
+
+ $c = $this->incrementCounter("id:{$headid}");
+ if ($c > 1) $headid .= "-{$c}";
+
+ $elem_opt = array();
+ if ($this->getContext('gen_head_id')) {
+ $elem_opt['id'] = str_replace('%', '.', rawurlencode($headid));
+ }
+
+ $ret .= $this->clear_inlinestyle();
+ $ret .= $this->clear_blockstyle();
+ $ret .= $this->formatter->open_element("heading${elem_level}", $elem_opt);
+ $ret .= $this->parse_inline($match[2][0]);
+ $ret .= $this->formatter->close_element("heading{$elem_level}");
+ return $ret;
+ }
+
+ protected function process_block_list($match, $line) {
+ $ret = $this->clear_blockstyle('indent');
+ $ret .= $this->clear_blockstyle('paragraph');
+ $ret .= $this->clear_blockstyle('table');
+ $ret .= $this->clear_blockstyle('quote');
+ $ret .= $this->clear_blockstyle('define');
+ $level = intval((strlen($match[1][0])+1)/2);
+ $mark = $match[2][0];
+ $mark_types = array('list_mark', 'list_num', 'list_roma', 'list_ROMA', 'list_alpha', 'list_ALPHA');
+ if ($mark == "*") {
+ $type = "list_mark";
+ } else if (preg_match('/^\d+\./', $mark)) {
+ $type = "list_num";
+ } else if (preg_match('/^[ivx]+\./', $mark)) {
+ $type = "list_roma";
+ } else if (preg_match('/^[IVX]+\./', $mark)) {
+ $type = "list_ROMA";
+ } else if (preg_match('/^[a-z]+\./', $mark)) {
+ $type = "list_alpha";
+ } else if (preg_match('/^[A-Z]+\./', $mark)) {
+ $type = "list_ALPHA";
+ } else {
+ $msg = $this->getFormatter()->raw_node("[BUG] unkown mark '$mark'");
+ error_log($msg);
+ $ret = $this->getFormatter()->raw_node('<span class="wiki-system-error">'."$msg</span>");
+ }
+ $cur_level = $this->count_in_stack($mark_types, $this->block_stack);
+ if ($level == $cur_level) {
+ $ret .= $this->pop_blockstyle("list_item");
+ } else if ($level > $cur_level) {
+ while (1) {
+ $ret .= $this->push_blockstyle($type);
+ if ($this->count_in_stack($mark_types, $this->block_stack) >= $level) {
+ break;
+ }
+ $ret .= $this->push_blockstyle("list_item");
+ }
+ } else {
+ while ($this->count_in_stack($mark_types, $this->block_stack) > $level) {
+ $ret .= $this->pop_blockstyle("list_item");
+ }
+ }
+ if ($type != end($this->block_stack)) {
+ $ret .= $this->pop_blockstyle(end($this->block_stack));
+ $ret .= $this->push_blockstyle($type);
+ }
+ $ret .= $this->push_blockstyle("list_item");
+ $ret .= $this->parse_inline(substr($line, strlen($match[0][0])));
+ return $ret;
+ }
+
+ protected function process_block_table($match, $line) {
+ $cells = explode('||', $line);
+ array_shift($cells);
+ array_pop($cells);
+ $ret = "";
+
+ if (!$this->in_stack_p("table"))
+ $ret .= $this->push_blockstyle("table");
+
+ $ret .= $this->push_blockstyle("table_row");
+
+ foreach($cells as $c) {
+ $ret .= $this->push_blockstyle("table_col");
+ $ret .= $this->parse_inline($c);
+ $ret .= $this->pop_blockstyle("table_col");
+ }
+
+ $ret .= $this->pop_blockstyle("table_row");
+
+ $this->last_table_cells = count($cells);
+ return $ret;
+ }
+
+ protected function process_block_line($match, $line) {
+ return $this->clear_blockstyle()
+ . $this->formatter->open_element("line")
+ . $this->formatter->close_element("line");
+ }
+
+ protected function process_block_quote($match, $line) {
+ $level = count(explode('>', $match[1][0]))-1;
+ $ret = '';
+ $ret .= $this->clear_blockstyle('indent');
+ $ret .= $this->clear_blockstyle('table');
+ $ret .= $this->clear_blockstyle('define');
+ $ret .= $this->clear_blockstyle('list');
+ while ($level < $this->count_in_stack('quote', $this->block_stack)) {
+ $ret .= $this->pop_blockstyle('quote');
+ }
+ while ($level > $this->count_in_stack('quote', $this->block_stack)) {
+ if (end($this->block_stack) == 'paragraph') {
+ $ret .= $this->pop_blockstyle('paragraph');
+ }
+ $ret .= $this->push_blockstyle('quote');
+ }
+
+ if (end($this->block_stack) != 'paragraph') {
+ $ret .= $this->push_blockstyle("paragraph");
+ }
+ $ret .= $this->parse_inline(substr($line, strlen($match[0][0])));
+ return $ret;
+ }
+
+ protected function process_block_define_list($match, $line) {
+ return $this->getFormatter()->raw_node("<div class=\"wiki-system-error\">def list not implemented yet.</div>"); # TODO:
+ }
+
+ protected function process_block_escaped($match, $line) {
+ $l = substr($line, 0, $match[1][1]) . substr($line, $match[1][1]+1);
+ return $this->process_block__default(null, $l);
+ }
+
+ protected function process_block_clear_all($match, $line) {
+ return $this->clear_blockstyle();
+ }
+
+ protected function process_block__default($match, $line) {
+ $ret = "";
+ $last = end($this->block_stack);
+ $cur_level = $last ? $this->count_in_stack($last, $this->block_stack) : 0;
+ $match = null;
+ preg_match("/^ +/", $line, $match);
+ $level = count($match) ? intval((strlen($match[0])+1)/2) : 0;
+ if ($last == "list_item" && count($match)) {
+ // TODO: BAD WAY!
+ if ($level >= $cur_level &&
+ $level <= $cur_level + (prev($this->block_stack) == "list_mark" ? 1 : 2)) {
+ $level = $cur_level;
+ }
+ }
+
+ if ($level && $last && ($last == "indent" || $last == "list_item") &&
+ $level == $this->count_in_stack($last, $this->block_stack)) {
+ // nop to continue current block element
+ } elseif ($level) {
+ $ret .= $this->clear_blockstyle();
+ for ($i = 0; $level > $i; $i++) {
+ $this->clear_inlinestyle();
+ $ret .= $this->push_blockstyle("indent");
+ }
+ } elseif (!$last || $last != "paragraph" ||
+ ($level == 0 && count($this->block_stack) > 1)) {
+ $ret .= $this->clear_blockstyle();
+ $this->_pending_paragraph = true;
+ }
+
+ $ret .= $this->parse_inline(substr($line, count($match) ? strlen($match[0]) : 0));
+ return $ret;
+ }
+
+ protected function parse_inline($line) {
+ $ret = $this->parse_bracket_and_plugin($line);
+ if ($this->getContext('trac.keep_newline'))
+ $ret .= $this->getFormatter()->open_element('newline');
+ return $ret;
+ }
+
+ protected function parse_bracket_and_plugin($text) {
+ $match = array();
+ $fmt_text = "";
+ $regex = null;
+ $cache_name = 'parse_bracket_and_plugin';
+ if (!$regex = $this->getConstCache($cache_name)) {
+ $regex =
+ '/(!)? # 1: escape
+ (?:\{\{\{(.*?)\}\}\} # 2: {{{preformatted}}}
+ | \[\[([a-zA-Z_].*?)\]\] # 3: [[plugin()]]
+ | \[(?:wiki:)?\x22([^"]+?)\x22(?:\s+([^\]]+))?\] # 4, 5: ["quoted" lebel]
+ | \[([^\x20\]]+?)(?:\s+([^\]]+))?\] # 6, 7: [link lebel]
+ | wiki:\x22(.+?)\x22 # 8: Quoted WikiName
+ )/x';
+ $this->setConstCache($cache_name, $regex);
+ }
+ while (preg_match($regex, $text, $match, PREG_OFFSET_CAPTURE)) {
+ $str = $match[0][0];
+ if ($match[0][1] > 0) {
+ $fmt_text .= $this->_flush_pending_paragraph();
+ $fmt_text .= $this->parse_inlinestyle(substr($text, 0, $match[0][1]));
+ }
+ $text = substr($text, $match[0][1]+ strlen($match[0][0]));
+ if (isset($match[1]) && strlen($match[1][0])) { /* escaped */
+ $fmt_text .= $this->_flush_pending_paragraph();
+ $fmt_text .= $this->formatter->text_node(substr($str, 1));
+ continue;
+ } elseif (isset($match[2]) && strlen($match[2][0])) { /* inline preformatted */
+ $fmt_text .= $this->_flush_pending_paragraph();
+ $fmt_text .= $this->formatter->open_element('monospace');
+ $fmt_text .= $this->formatter->text_node($match[2][0]);
+ $fmt_text .= $this->formatter->close_element('monospace');
+ } elseif (isset($match[3]) && strlen($match[3][0])) { /* plugin */
+ $fmt_text .= $this->process_plugin($match[3][0]);
+ continue;
+ } else {
+ $fmt_text .= $this->_flush_pending_paragraph();
+ $link = null;
+ $label = null;
+ if (isset($match[3]) && strlen($match[4][0])) { /* quoted bracket */
+ if (!$this->getContext('disable.link.quoted_bracket')) {
+ $link = $this->gen_uri_link("wiki:\"{$match[4][0]}\"");
+ $label = (isset($match[5]) && strlen($match[5][0])) ? $match[5][0] : $match[4][0];
+ }
+ } elseif (isset($match[6]) && strlen($match[6][0])) { /* bracket link */
+ if (!$this->getContext('disable.link.bracket')) {
+ $link = $this->gen_uri_link($match[6][0]);
+ if (!$link && !strrchr($match[6][0], ':')) {
+ // forced as wikiname
+ $link = $this->gen_uri_link("wiki:{$match[6][0]}");
+ }
+ $label = (isset($match[7]) && strlen($match[7][0])) ? $match[7][0] : $match[6][0];
+ }
+ } elseif (isset($match[8]) && strlen($match[8][0])) { /* quoted wikiname */
+ $link = $this->gen_uri_link($str);
+ }
+ $fmt_text .= isset($link) ? $this->create_link((isset($label) ? $label : $str), $link) : $this->formatter->text_node($str);
+ continue;
+ }
+ }
+ $fmt_text .= $this->_flush_pending_paragraph();
+ $fmt_text .= $this->parse_inlinestyle($text);
+ return $fmt_text;
+ }
+
+ protected function parse_inlinestyle($text) {
+ $match = array();
+ $formatted_text = "";
+ while (preg_match($this->getInlinestyleRegex(), $text, $match, PREG_OFFSET_CAPTURE)) {
+ $leading_text = $this->parse_links(substr($text, 0, $match[0][1]));
+ $replace_elem = $this->inlinestyle_callback($match);
+ $formatted_text .= $leading_text . $replace_elem;
+ $text = substr($text, $match[0][1]+ strlen($match[0][0]));
+ }
+ $formatted_text .= $this->parse_links($text);
+ return $formatted_text;
+ }
+
+ protected function parse_links($text) {
+ $match = array();
+ $fmt_text = "";
+ $regex = null;
+ $cache_name = "parse_links";
+
+ if (!$regex = $this->getConstCache($cache_name)) {
+ $regex =
+ '/(!)? # 1: escape
+ (?:\#(\d+) # 2: #nnn tracker
+ | (?<![A-Za-z0-9.#-&-])r(\d+)(?![A-Za-z0-9.#-&-]) # 3: subversion revision
+ | (' . $this->getUriRegex(false) . ') # 4: URI
+ | (?<!\w)(?:[A-Z][a-z0-9]+){2,} # WikiName
+ )/x';
+ $this->setConstCache($cache_name, $regex);
+ }
+
+ while (preg_match($regex, $text, $match, PREG_OFFSET_CAPTURE)) {
+ $link = null;
+ $str = $match[0][0];
+ $fmt_text .= $this->formatter->text_node(substr($text, 0, $match[0][1]));
+ $text = substr($text, $match[0][1]+ strlen($match[0][0]));
+ if (isset($match[1]) && strlen($match[1][0])) { /* escaped */
+ $fmt_text .= $this->formatter->text_node(substr($str, 1));
+ continue;
+ } elseif (isset($match[2]) && strlen($match[2][0])) { /* #nnnn tracker */
+ $link = $this->gen_uri_link("ticket:{$match[2][0]}");
+ } elseif (isset($match[3]) && strlen($match[3][0])) { /* SVN */
+ if (!$this->getContext('disable.link.svn_revision')) {
+ $link = $this->getContext('sfjp.svn_rev_base_url') . $match[3][0];
+ $link = array("href" => $link, "class" => "svn");
+ }
+ } elseif (isset($match[4]) && strlen($match[4][0])) { /* URI */
+ $link = $this->gen_uri_link($str);
+ } else { /* WikiName */
+ if (!$this->getContext('disable.link.CamelCase')) {
+ $link = $this->getContext('wiki_baseurl');
+ if (substr($link, -1) !== "/")
+ $link .= '/';
+ $link .= rawurlencode($str);
+ }
+ }
+
+ if (isset($link)) {
+ $fmt_text .= $this->create_link($str, $link);
+ } else {
+ $fmt_text .= $this->formatter->text_node($str);
+ }
+ }
+ $fmt_text .= $this->formatter->text_node($text);
+ return $fmt_text;
+ }
+
+ public function gen_uri_link($str) {
+ $ret = null;
+ if (strpos($str, '/') === 0)
+ return array("href" => $str);
+ $part = explode(':', $str, 2);
+ if (count($part) == 1) return null;
+ $scheme = $part[0];
+ $body = $part[1];
+ if ($this->getContext("disable.link.scheme.{$scheme}"))
+ return null;
+ switch($scheme) {
+ // built-in schemes
+ case "http":
+ case "https":
+ case "ftp":
+ if (!preg_match('!//[^\x00-\x20"<>]+!', $body)) break;
+ $ret = array("href" => $str);
+ if (!($this->getContext("internal_url_regex") &&
+ preg_match("/".str_replace('/', '\/', $this->getContext("internal_url_regex"))."/", $str))) {
+ $ret["class"] = "external";
+ if ($this->getContext('nofollow_on_external_links'))
+ $ret["rel"] = "nofollow";
+ }
+ break;
+ case "wiki":
+ $wiki_allow_chars = '[^:\x00-\x1f\x21-\x23\x27\x5b-\x5d\x60]';
+ $m = null;
+ $fragment = null;
+ if (substr($body, 0, 1) == '"' && substr($body, -1, 1) == '"')
+ $body = substr($body, 1, strlen($body)-2);
+ if (($fragpos = strpos($body, '#')) !== false) {
+ $fragment = substr($body, $fragpos+1);
+ $body = substr($body, 0, $fragpos);
+ }
+ if (preg_match("/^([a-z0-9-]+):(${wiki_allow_chars}*)\$/", $body, $m)) { # wiki:group:PageName
+ $ret = $this->getContext('site_root_url')
+ . "/projects/$m[1]/wiki/"
+ . rawurlencode($m[2]);
+ $ret = array("href" => $ret, "class" => "external-wiki");
+ } elseif (preg_match("/^${wiki_allow_chars}+\$/", $body)) {
+ $ret = $this->getContext('wiki_baseurl');
+ if (substr($ret, -1) !== "/")
+ $ret .= '/';
+ $ret .= rawurlencode($body);
+ }
+ if (isset($fragment)) {
+ if (!isset($ret)) $ret = array("href" => "");
+ if (!is_array($ret)) $ret = array("href" => $ret);
+ $ret["href"] .= "#" . Formatter\Base::escape_id_value(str_replace('%', '.', rawurlencode($fragment)));
+ }
+ break;
+ case "tracker":
+ case "ticket":
+ if (preg_match('/^[0-9]+$/', $body)) {
+ if ($body > 50000) {
+ $ret = array("href" => $this->getContext('site_root_url').'/ticket/detail.php?id='.$body,
+ "class" => "tracker");
+ } else {
+ $ret = array("href" => $this->getContext('site_root_url')."/tracker.php?id={$body}",
+ "class" => "tracker");
+ }
+ }
+ break;
+ case "cvs":
+ if (preg_match('/^[a-z0-9_-]+:/', $body)) {
+ list($group, $path)= explode(':', $body, 2);
+ } else {
+ $group = $this->getContext('sfjp.group_name');
+ $path = $body;
+ }
+ if (substr($body, 0, 1) != '/')
+ $path = "/$path";
+ $ret = $this->getContext('cvs_base_url') . "/${group}${path}";
+ $ret = array("href" => $ret, "class" => "cvs");
+ break;
+ case "svn":
+ if (preg_match('/^[a-z0-9_-]+:/', $body)) {
+ list($group, $path)= explode(':', $body, 2);
+ } else {
+ $group = $this->getContext('sfjp.group_name');
+ $path = $body;
+ }
+ if (substr($body, 0, 1) != '/')
+ $path = "/$path";
+ $ret = $this->getContext('sfjp.svn_file_base_url') . $path;
+ $ret = array("href" => $ret, "class" => "svn");
+ break;
+ case "user":
+ case "users":
+ case "id":
+ if (preg_match('/^[a-z0-9_-]+$/', $body)) {
+ $ret = "/users/" . $body;
+ $ret = array("href" => $ret, "class" => "user");
+ if ($this->getContext('individual_usericon')) {
+ if (empty($ret['style'])) $ret['style'] = '';
+ $ret['style'] .= "background-image: url(".$this->getContext('individual_usericon')."{$body});";
+ }
+ if ($this->getContext('override_usericon_size')) {
+ if (empty($ret['style'])) $ret['style'] = '';
+ $ret['style'] .= "padding-left: ".$this->getContext('override_usericon_size')."px;";
+ }
+ }
+ break;
+ case "comment":
+ $parts = explode(':', $body, 3);
+ if (count($parts) == 2) {
+ $ret = $this->getContext('site_root_url')
+ . '/ticket/detail.php?id='.$parts[0].'&cid='.$parts[1];
+ } else if (!$parts[0]) {
+ $ret = $this->getContext('site_root_url')
+ . '/ticket/detail.php?id='.$parts[1].'&cid='.$parts[2];
+ } else {
+ $ret = $this->getContext('site_root_url')
+ . "/ticket/browse.php?group_id=${parts[0]}&tid=${parts[1]}#comment:${body}";
+ }
+ $ret = array("href" => $ret, "class" => "ticket");
+ break;
+ case "project":
+ case "projects":
+ if (preg_match('/^[a-z0-9_-]+$/', $body)) {
+ $ret = $this->getContext('site_root_url')
+ . "/projects/$body/";
+ $ret = array("href" => $ret, "class" => "project");
+ }
+ break;
+ case "prweb":
+ $m = array();
+ preg_match('/^https?:\/\/([^\/]+)/', $this->getContext('site_root_url'), $m);
+ $site_domain = $m[1];
+ $host = $this->getContext('sfjp.group_name').".${site_domain}";
+ $path = $body;
+
+ if (preg_match('/^([a-z0-9_-]+):(.*)$/', $body, $m)) { # prweb:project:path
+ $host = "$m[1].${site_domain}";
+ $path = $m[2];
+ }
+ if (substr($path, 0, 1) != "/")
+ $path = "/$path";
+ $ret = "http://${host}${path}";
+ $ret = array("href" => $ret, "class" => "project-web");
+ break;
+ case "release":
+ $ret = $this->getContext('')
+ . "/projects/" . $body;
+ $ret = array("href" => $ret, "class" => "release");
+ break;
+ case "isbn":
+ $ret = array("href" => "http://www.amazon.co.jp/gp/product/$body",
+ "class" => "isbnbook", "rel" => "nofollow");
+ if ($aid = $this->getContext('amazon_affiliate_id')) {
+ $ret['href'] .= "?tag={$aid}&linkCode=as2&camp=247&creative=1211&creativeASIN={$body}";
+ }
+ break;
+ case "mailto":
+ $ret = array("href" => "mailto:{$body}", "class" => "mail");
+ break;
+ default:
+ if ($this->getInterwikiProcessor() &&
+ is_callable(array($this->getInterwikiProcessor(), 'process'),
+ $body)){
+ $ret = $this->getInterwikiProcessor()->process($body);
+ }
+ break;
+ }
+ return $ret;
+ }
+
+ public function create_link($text, $args, $no_escape_html = false) {
+ if (!is_array($args))
+ $args = array("href" => $args);
+
+ if (array_key_exists('href', $args)) {
+ $args["href"] = $this->encode_url_badchar($args["href"]);
+ }
+
+ $fmt = $this->formatter;
+ $ret = "";
+ $ret .= $fmt->open_element("link", $args);
+ $ret .= $no_escape_html ? $fmt->raw_node($text) : $fmt->text_node($text);
+ $ret .= $fmt->close_element("link");
+ return $ret;
+ }
+
+ public function encode_url_badchar($str) {
+ return preg_replace_callback('/[^\x21\x23-\x26\x28-\x3b\x3d\x3f-\x5a\x5e-\x7e]+/',
+ create_function('$m', 'return rawurlencode($m[0]);'),
+ $str);
+ }
+
+ public function process_plugin($str) {
+ $match = null;
+ if (!preg_match('/^([^()]+)(?:\((.*)\))?$/', $str, $match))
+ return "";
+ $name = $match[1];
+ $args = isset($match[2]) && !empty($match[2]) ? preg_split('/\s*,\s*/', trim($match[2])) : array();
+
+ try {
+ $plugin_obj = $this->get_plugin_instance("Plugin\\{$name}");
+ $plug_ret = call_user_func(array($plugin_obj, "process"), $args);
+ if (!$this->is_vary) $this->is_vary = $plugin_obj->is_vary;
+ if ($plugin_obj->is_block) {
+ if ($this->_pending_paragraph) {
+ $this->_pending_paragraph = false;
+ }
+ if ($this->in_stack_p('paragraph')) {
+ $plug_ret = $this->pop_blockstyle('paragraph')
+ . $plug_ret . $this->push_blockstyle('paragraph');
+ }
+ } else {
+ $plug_ret = $this->_flush_pending_paragraph() . $plug_ret;
+ }
+ return $plug_ret;
+ } catch (Exception\Plugin_Error $e) {
+ return $this->format_plugin_error($e);
+ }
+ }
+
+ public function format_plugin_error($e) {
+ if ($this->getContext("suppress_plugin_error")) {
+ return '';
+ } else {
+ return $this->formatter->element('error', "Plugin Error: ".$e->getMessage());
+ }
+ }
+
+ public function count_in_stack($name, &$stack) {
+ if (!is_array($name))
+ $name = array($name);
+ $count = array_count_values($stack);
+ $ret = 0;
+ foreach ($name as $n) {
+ $ret += array_key_exists($n, $count) ? $count[$n] : 0;
+ }
+ return $ret;
+ }
+
+ public function in_stack_p($name) {
+ return in_array($name, $this->inline_stack) || in_array($name, $this->block_stack);
+ }
+
+ protected function push_blockstyle($name) {
+ $this->block_stack[] = $name;
+ return $this->formatter->open_element($name);
+ }
+
+ protected function pop_blockstyle($name) {
+ return $this->clear_blockstyle($name, 1);
+ }
+
+ protected function clear_inlinestyle($name = null) {
+ return $this->clear_stylestack($this->inline_stack, $name);
+ }
+
+ protected function clear_blockstyle($name = null, $max = null) {
+ $ret = $this->clear_inlinestyle();
+ $ret .= $this->clear_stylestack($this->block_stack, $name, $max);
+ return $ret;
+ }
+
+ protected function clear_stylestack(&$stack, $name, $max = null) {
+ $ret = "";
+ $i = 1;
+ if (isset($name) && !in_array($name, $stack)) {
+ // return if $name setted and not found in stack.
+ return $ret;
+ }
+ while ($elem = array_pop($stack)) {
+ $ret .= $this->formatter->close_element($elem);
+ if (isset($name) && $name == $elem) {
+ $i++;
+ if ($max && $i > $max || !in_array($name, $stack))
+ break;
+ }
+ }
+ return $ret;
+ }
+
+ protected function inlinestyle_callback($match) {
+ $rule = $this->get_matched_rule($match, $this->inlinestyle_rules);
+ if (!isset($rule) && $match[1][0] === "!")
+ return $this->formatter->text_node(substr($match[0][0], 1));
+ $name = $rule[0];
+ if (!in_array($name, $this->inline_stack)) {
+ $this->inline_stack[] = $name;
+ return $this->formatter->open_element($name);
+ } else {
+ $ret = "";
+ while ($name != ($cur_elem = array_pop($this->inline_stack))) {
+ $ret .= $this->formatter->close_element($cur_elem);
+ }
+ $ret .= $this->formatter->close_element($name);
+ return $ret;
+ }
+ }
+
+ protected function get_matched_rule($match, $rules, $rule_have_escape = true) {
+ if ($rule_have_escape && strlen($match[1][0])) /* ! escaped */
+ return null;
+ $excess = $rule_have_escape ? 2 : 1;
+ for($i = $excess; isset($match[$i]); $i++) {
+ if ($match[$i][1] >= 0)
+ break;
+ }
+ return $rules[$i - $excess];
+ }
+
+ private function _flush_pending_paragraph() {
+ if (!$this->_pending_paragraph) return '';
+ $this->_pending_paragraph = false;
+ return $this->push_blockstyle("paragraph");
+ }
+}
--- /dev/null
+<?php
+namespace sfjp\Wiki\Processor;
+class Trac_oneline extends Trac {
+ public $enable_plugin = false;
+
+ public function process($text = null) {
+ $ret ='';
+ $ret .= $this->parse_inline($text);
+ $ret .= $this->clear_inlinestyle();
+ $ret .= $this->getFormatter()->cleanup();
+ $this->formatted_text = $ret;
+ return $this;
+ }
+
+ public function process_plugin($str) {
+ if ($this->enable_plugin) {
+ return parent::process_plugin($str);
+ } else {
+ return '';
+ }
+ }
+}
--- /dev/null
+<?php
+namespace sfjp\Wiki\Storage;
+abstract class Base {
+ protected $context;
+ public function __construct($context = array()) {
+ $this->context = $context;
+ }
+ abstract public function get($page);
+ abstract public function set($page, $text);
+ abstract public function exists($page);
+ abstract public function remove($page);
+ abstract public function get_list();
+}
\ No newline at end of file
--- /dev/null
+<?php
+namespace sfjp\Wiki\Storage;
+class Dummy extends Base {
+ public function get($page) {
+ return false;
+ }
+ public function set($page, $text) {
+ return;
+ }
+ public function exists($page) {
+ return true;
+ }
+ public function remove($page) { return; }
+ public function get_list() { return array(); }
+}
\ No newline at end of file
--- /dev/null
+<?php
+namespace sfjp\Wiki\Storage;
+class File extends Base {
+ protected $datadir = './wikitexts';
+
+ function __construct($context = array()) {
+ parent::__construct($context);
+ if (!empty($context['storage.file.datadir']))
+ $this->datadir = $context['storage.file.datadir'];
+ }
+
+ public function get($page) {
+ return file_get_contents("{$this->datadir}/{$page}");
+ }
+
+ public function set($page, $text) {
+ if (!is_dir($this->datadir))
+ mkdir($this->datadir, 0755, true);
+ $file = "{$this->datadir}/{$page}";
+ $tmp = "{$this->datadir}/.{$page}.tmp.".getmypid();
+ file_put_contents($tmp, $text);
+ rename($tmp, $file);
+ }
+
+ public function exists($page) {
+ file_exists("{$this->datadir}/{$page}");
+ }
+
+ public function remove($page) {
+ unlink("{$this->datadir}/{$page}");
+ }
+
+ public function get_list() {
+ $dh = opendir($this->datadir);
+ if (!$dh) return array();
+
+ $ret = array();
+ while ($ent = readdir($dh)) {
+ if (substr($ent, 0, 1) == '.')
+ continue;
+ if (substr($ent, -1) == '~')
+ continue;
+ $ret []= $ent;
+ }
+ closedir($dh);
+ sort($ret, SORT_NATURAL|SORT_FLAG_CASE);
+ return $ret;
+ }
+}
\ No newline at end of file
--- /dev/null
+<?php
+if (!spl_autoload_functions() || !in_array('spl_autoload', spl_autoload_functions())) {
+ spl_autoload_extensions('.php,.class');
+ spl_autoload_register('spl_autoload');
+
+}
+
+class_alias('sfjp\Wiki\Exception\Base', 'sfjpWikiParseException');
+class_alias('sfjp\Wiki\Exception\Invalid_Argument', 'sfjpWikiProcessor_InvalidArgument');
+class_alias('sfjp\Wiki\Exception\Internal_Error', 'sfjpWikiProcessor_InternalError');
+
+class_alias('sfjp\Wiki\Parser', 'sfjpWikiParser');
+
+class_alias('sfjp\Wiki\Processor\Base', 'sfjpWikiProcessor_base');
+class_alias('sfjp\Wiki\Processor\Trac', 'sfjpWikiProcessor_trac');
+class_alias('sfjp\Wiki\Processor\Trac_Oneline', 'sfjpWikiProcessor_trac_oneline');
+class_alias('sfjp\Wiki\Processor\Auto', 'sfjpWikiProcessor_auto');
+class_alias('sfjp\Wiki\Processor\Pre', 'sfjpWikiProcessor_pre');
+class_alias('sfjp\Wiki\Processor\Code', 'sfjpWikiProcessor_code');
+class_alias('sfjp\Wiki\Processor\Comment', 'sfjpWikiProcessor_comment');
+class_alias('sfjp\Wiki\Processor\Denied', 'sfjpWikiProcessor_denied');
--- /dev/null
+<?php
+
+require_once 'PHPUnit/Framework/TestCase.php';
+
+class FormatTest extends PHPUnit_Framework_TestCase {
+ protected function setUp() {
+ $context = array(
+ 'sfjp.group_name' => 'unittest',
+ 'site_root_url' => 'http://sourceforge.jp',
+ 'internal_url_regex' => '^http://sourceforge\.jp/',
+ 'svn_base_url' => 'http://svn/view',
+ );
+ $this->p = new \sfjp\Wiki\Parser($context);
+ }
+
+ protected function tearDown() {
+
+ }
+
+ public function testNew() {
+ self::assertTrue(isset($this->p));
+ }
+
+ public function testParseSimpleRun() {
+ $this->try_parse('', '');
+ $this->try_parse('abc', '<p>abc</p>');
+ $this->try_parse("abc abc\nabc", "<p>abc abc\nabc</p>");
+ }
+
+ public function testParseHeadings() {
+ $this->try_parse('= abc =', '<h1 id="h1-abc">abc</h1>');
+ $this->try_parse("= 日本語 =\r\n", '<h1 id="h1-.E6.97.A5.E6.9C.AC.E8.AA.9E">日本語</h1>');
+ $this->try_parse('= abc', '<h1 id="h1-abc">abc</h1>');
+ $this->try_parse('= abc = ', '<h1 id="h1-abc">abc</h1>');
+ $this->try_parse('= abc ', '<h1 id="h1-abc">abc</h1>');
+ $this->try_parse('= abc = #moge', '<h1 id="moge">abc</h1>');
+ $this->try_parse('= abc #moge', '<h1 id="moge">abc</h1>');
+ $this->try_parse('=abc', '<p>=abc</p>');
+ $this->try_parse(' = abc', '<div class="indent">= abc</div>');
+ }
+
+ public function testParseListMark() {
+ $text = '
+ * list1
+ * level2
+ * level2, too.
+ continue list item.
+ continue!
+ continue also!
+ * moge
+ * level1 item
+ * level1
+ *this not list
+';
+
+ $expect = '<ul><li>list1
+<ul><li>level2
+</li><li>level2, too.
+continue list item.
+continue!
+continue also!
+</li><li>moge
+</li></ul></li><li>level1 item
+</li><li>level1
+*this not list
+</li></ul>';
+ self::assertEquals($expect, $this->p->parse($text));
+ }
+
+ public function testParseMixedList() {
+ $text = '
+ * ul
+ 1. ol num
+ a. ol alpha
+ A. ol ALPHA
+ * ul
+ i. ol roma
+ * ul
+ I. ol ROMA
+';
+
+ $expect = '<ul><li>ul
+</li></ul><ol><li>ol num
+</li></ol><ol style="list-style-type: lower-alpha;"><li>ol alpha
+</li></ol><ol style="list-style-type: upper-alpha;"><li>ol ALPHA
+</li></ol><ul><li>ul
+</li></ul><ol style="list-style-type: lower-roman;"><li>ol roma
+</li></ol><ul><li>ul
+</li></ul><ol style="list-style-type: upper-roman;"><li>ol ROMA
+</li></ol>';
+ self::assertEquals($expect, $this->p->parse($text));
+
+ $text = '
+ * list1
+ 1. level2
+ 100. ordered
+ 99. moge
+ * level1 item
+ * level2 unordered
+ 1. abc
+ 1. def
+ * foo
+ 1. level1 ordered
+ * unordered in ordered
+ 2. ordered list
+';
+
+ $expect = '<ul><li>list1
+<ol><li>level2
+</li><li>ordered
+</li><li>moge
+</li></ol></li><li>level1 item
+<ul><li>level2 unordered
+<ol><li>abc
+</li><li>def
+</li></ol></li><li>foo
+</li></ul></li></ul><ol><li>level1 ordered
+<ul><li>unordered in ordered
+</li></ul></li><li>ordered list
+</li></ol>';
+ self::assertEquals($expect, $this->p->parse($text));
+ }
+
+ /*
+ public function testParseParagraph() {
+ self::assertEquals('yet', $this->p->parse('=abc'));
+ }
+ */
+
+ public function testParseListNum() {
+ $text = '
+ 1. list1
+ 3. level2
+ 999999. level2, too.
+ 1234. moge
+ 3. level1 item
+ 0000. level1
+ 1.3. this not list
+';
+
+ $expect = '<ol><li>list1
+<ol><li>level2
+</li><li>level2, too.
+</li><li>moge
+</li></ol></li><li>level1 item
+</li><li>level1
+1.3. this not list
+</li></ol>';
+ self::assertEquals($expect, $this->p->parse($text));
+ }
+
+ /*
+ public function testParseDefinitionList() {
+ self::assertEquals('yet', $this->p->parse('=abc'));
+ }
+ */
+
+ public function testParseHTMLEscape() {
+ $this->try_parse("'''>moge'''<", '<p><strong>>moge</strong><</p>');
+ }
+
+ public function testParseInlineBold() {
+ $this->try_parse("'''moge'''", '<p><strong>moge</strong></p>');
+ }
+
+ public function testParseInlineItalic() {
+ $this->try_parse("''abc''", '<p><em>abc</em></p>');
+ $this->try_parse("''ab", '<p><em>ab</em></p>');
+ $this->try_parse("ab ''bc\n''cd", "<p>ab <em>bc\n</em>cd</p>");
+ }
+
+ public function testParseInlineBolditalic() {
+ $this->try_parse("'''''moge'''''",
+ '<p><strong><em>moge</em></strong></p>');
+ $this->try_parse("'''''''''abc'",
+ "<p><strong><em><strong>'abc'</strong></em></strong></p>");
+ }
+
+ public function testParseInlineUndeline() {
+ $this->try_parse("__abc__",
+ '<p><span style="text-decoration: underline;">abc</span></p>');
+ }
+
+ public function testParseInlineStrike() {
+ $this->try_parse('~~abc~~', '<p><del>abc</del></p>');
+ $this->try_parse('~~~abcef~~~~~', '<p><del>~abcef</del><del>~</del></p>');
+ }
+
+ public function testParseInlineSuperscript() {
+ $this->try_parse('^ abc^', '<p><sup> abc</sup></p>');
+ $this->try_parse('^^^ abc ^^', '<p><sup></sup><sup> abc </sup><sup></sup></p>');
+ $this->try_parse("head ^ abc\ndef^tail", "<p>head <sup> abc\ndef</sup>tail</p>");
+ }
+
+ public function testParseInlineSubscript() {
+ $this->try_parse(',,abc ,,', '<p><sub>abc </sub></p>');
+ }
+
+ public function testParseLinks() {
+ $this->try_parse('http://url.com/path', '<p><a href="http://url.com/path" class="external" rel="nofollow">http://url.com/path</a></p>');
+ $this->try_parse('https://url.com/path', '<p><a href="https://url.com/path" class="external" rel="nofollow">https://url.com/path</a></p>');
+ }
+
+ public function testParseBlacketLink() {
+ self::assertEquals(
+ '<p><a href="./a%2Fb%2F%E6%97%A5%E6%9C%AC%E8%AA%9E">a/b/日本語</a></p>',
+ $this->p->parse("[a/b/日本語]")
+ );
+ }
+
+ public function testParseLinkFragment() {
+ $this->try_parse('[test1#frag]', '<p><a href="./test1#frag">test1#frag</a></p>');
+ $this->try_parse('[test2#frag text]', '<p><a href="./test2#frag">text</a></p>');
+ $this->try_parse('wiki:test3#frag', '<p><a href="./test3#frag">wiki:test3#frag</a></p>');
+
+ $this->try_parse('wiki:"test4#frag"', '<p><a href="./test4#frag">wiki:"test4#frag"</a></p>');
+ $this->try_parse('["test5#frag" text]', '<p><a href="./test5#frag">text</a></p>');
+ $this->try_parse('[wiki:"test6#frag" text]', '<p><a href="./test6#frag">text</a></p>');
+ $this->try_parse('[wiki:"test7 page name#frag" text]', '<p><a href="./test7%20page%20name#frag">text</a></p>');
+ $this->try_parse('[#frag]', '<p><a href="#frag">#frag</a></p>');
+ $this->try_parse('["#frag" text]', '<p><a href="#frag">text</a></p>');
+
+ }
+
+ public function testParseQuote() {
+ self::assertEquals(
+ "<blockquote class=\"citation\"><blockquote class=\"citation\"><p>abc
+</p></blockquote><p>def
+ghi
+</p><blockquote class=\"citation\"><blockquote class=\"citation\"><p>jkl
+</p></blockquote></blockquote></blockquote><p>normal</p>",
+ $this->p->parse(">> abc\n> def\n> ghi\n>>> jkl\nnormal")
+ );
+$this->try_parse("> abc\n> > with space\n> > > 3rd", '<blockquote class="citation"><p>abc
+</p><blockquote class="citation"><p>with space
+</p><blockquote class="citation"><p> 3rd</p></blockquote></blockquote></blockquote>');
+ }
+
+ public function testParseIndent() {
+ $this->try_parse(" abc", '<div class="indent">abc</div>');
+ $this->try_parse(" abc\n def\nghi",
+ '<div class="indent">abc
+def
+</div><p>ghi</p>');
+ self::assertEquals('<div class="indent">abc
+def
+</div><div class="indent"><div class="indent">2nd nest
+</div></div><div class="indent"><div class="indent"><div class="indent">3rd
+</div></div></div><div class="indent"><div class="indent">2nd
+</div></div><ul><li>clear by list</li></ul>',
+ $this->p->parse(" abc\n def\n 2nd nest\n 3rd\n 2nd\n * clear by list"));
+ }
+
+ public function testParseInternalURIPrweb() {
+ $this->try_parse("prweb:/", '<p><a href="http://unittest.sourceforge.jp/" class="project-web">prweb:/</a></p>');
+ $this->try_parse("prweb:/path/to/file", '<p><a href="http://unittest.sourceforge.jp/path/to/file" class="project-web">prweb:/path/to/file</a></p>');
+ $this->try_parse("prweb:project-name:/url-to/the.page", '<p><a href="http://project-name.sourceforge.jp/url-to/the.page" class="project-web">prweb:project-name:/url-to/the.page</a></p>');
+ }
+
+ public function testParseInternalURIUser() {
+ $this->try_parse("user:sugi", '<p><a href="/users/sugi" class="user">user:sugi</a></p>');
+ $this->try_parse("id:sugi", '<p><a href="/users/sugi" class="user">id:sugi</a></p>');
+ $this->try_parse("users:sugi", '<p><a href="/users/sugi" class="user">users:sugi</a></p>');
+ }
+
+ public function testParseURIMailTo() {
+ $this->try_parse("mailto:sugi@osdn.jp", '<p><a href="mailto:sugi@osdn.jp" class="mail">mailto:sugi@osdn.jp</a></p>');
+ $this->try_parse("[mailto:a.b=c+d@e.f メール]", '<p><a href="mailto:a.b=c+d@e.f" class="mail">メール</a></p>');
+ $this->try_parse("mailto:bad@てすと", '<p>mailto:bad@てすと</p>');
+ }
+
+ public function testParseEscape() {
+ $this->try_parse("!`", '<p>`</p>');
+ $this->try_parse("!^てすと!^", '<p>^てすと^</p>');
+ $this->try_parse("!~~", '<p>~~</p>');
+ $this->try_parse("!__", '<p>__</p>');
+ $this->try_parse("!WikiName", '<p>WikiName</p>');
+ $this->try_parse("![[Plugin]]", '<p>[[Plugin]]</p>');
+ $this->try_parse("!!", '<p>!!</p>');
+ }
+
+ public function testParseEscapeBlock() {
+ $this->try_parse("!> Equote", '<p>> Equote</p>');
+ $this->try_parse("!------", '<p>------</p>');
+ $this->try_parse("!||escaped||table||", '<p>||escaped||table||</p>');
+ $this->try_parse("!= not header =", '<p>= not header =</p>');
+ $this->try_parse(" !* abc", '<div class="indent">* abc</div>');
+ }
+
+ public function testHttpUrl() {
+ self::assertTrue(!!$this->p->processor->gen_uri_link("http://てすと"));
+ self::assertTrue(!!$this->p->processor->gen_uri_link("http://abc"));
+ self::assertTrue(!$this->p->processor->gen_uri_link("http://"));
+ self::assertTrue(!$this->p->processor->gen_uri_link("http:// abc"));
+ }
+
+ public function testIsbnLink() {
+ $this->try_parse("isbn:123",
+ '<p><a href="http://www.amazon.co.jp/gp/product/123" class="isbnbook" rel="nofollow">isbn:123</a></p>');
+ $this->p->setContext(array('amazon_affiliate_id' => 'afid-test'));
+ $this->try_parse("isbn:123",
+ '<p><a href="http://www.amazon.co.jp/gp/product/123?tag=afid-test&linkCode=as2&camp=247&creative=1211&creativeASIN=123" class="isbnbook" rel="nofollow">isbn:123</a></p>');
+ }
+
+ public function testParseGroupWikiPage() {
+ $this->try_parse('wiki:groupname:PageName', '<p><a href="http://sourceforge.jp/projects/groupname/wiki/PageName" class="external-wiki">wiki:groupname:PageName</a></p>');
+ $this->try_parse('wiki:group-name:PageName', '<p><a href="http://sourceforge.jp/projects/group-name/wiki/PageName" class="external-wiki">wiki:group-name:PageName</a></p>');
+ $this->try_parse('wiki:group_name:PageName', '<p><a href="./group_name">wiki:group_name</a>:<a href="./PageName">PageName</a></p>');
+
+ }
+
+ public function testParseOrer() {
+ $this->try_parse('[PageName [[BR]]]',
+ '<p><a href="./PageName">[[BR</a>]]</p>');
+ /* do not support currently
+ $this->try_parse("abc'''def[PageName la''bee''eel]tex'''t",
+ '<p>abc<b>def<a href="./PageName">la<i>bee</i>eel</a>tex</b>t</p>');
+ */
+ $this->try_parse("abc'''def[PageName la''bee''eel]tex'''t",
+ "<p>abc<strong>def<a href=\"./PageName\">la''bee''eel</a>tex</strong>t</p>");
+
+ $this->try_parse('[__Page,,Name^^ label]', '<p><a href="./__Page%2C%2CName%5E%5E">label</a></p>');
+ $this->try_parse('["__Page,,Name^^" label]', '<p><a href="./__Page%2C%2CName%5E%5E">label</a></p>');
+ $this->try_parse('[wiki:"__Page,,Name^^" label]', '<p><a href="./__Page%2C%2CName%5E%5E">label</a></p>');
+ $this->try_parse('http://aaaa[bbb]ccc', '<p><a href="http://aaaa" class="external" rel="nofollow">http://aaaa</a><a href="./bbb">bbb</a>ccc</p>');
+ $this->try_parse('!http://aaaa[bbb]ccc', '<p>http://aaaa<a href="./bbb">bbb</a>ccc</p>');
+ $this->try_parse('http://aaaa![bbb]ccc', '<p><a href="http://aaaa" class="external" rel="nofollow">http://aaaa</a>[bbb]ccc</p>');
+ $this->try_parse('!http://aaaa![bbb]ccc', '<p>http://aaaa[bbb]ccc</p>');
+ }
+
+ public function testQuotedLink() {
+ $this->try_parse('["test page__ name"]', '<p><a href="./test%20page__%20name">test page__ name</a></p>');
+ $this->try_parse('["a b c" label]', '<p><a href="./a%20b%20c">label</a></p>');
+ $this->try_parse('[wiki:"a b c" label]', '<p><a href="./a%20b%20c">label</a></p>');
+ $this->try_parse('wiki:",,a b c__"', '<p><a href="./%2C%2Ca%20b%20c__">wiki:",,a b c__"</a></p>');
+ }
+
+ public function testBracket() {
+ $this->try_parse('[http://www label]', '<p><a href="http://www" class="external" rel="nofollow">label</a></p>');
+ $this->try_parse('[WikiName label]', '<p><a href="./WikiName">label</a></p>');
+ $this->try_parse('[normal text]', '<p><a href="./normal">text</a></p>');
+ }
+
+ public function testParseFalseLink() {
+ $this->try_parse("[0]", '<p><a href="./0">0</a></p>');
+ $this->try_parse('["0"]', '<p><a href="./0">0</a></p>');
+ $this->try_parse("[0 text]", '<p><a href="./0">text</a></p>');
+ $this->try_parse("[0#0]", '<p><a href="./0#badid-0">0#0</a></p>');
+ $this->try_parse('["0#0" text]', '<p><a href="./0#badid-0">text</a></p>');
+ $this->try_parse("wiki:0", '<p><a href="./0">wiki:0</a></p>');
+ $this->try_parse("wiki:0#0", '<p><a href="./0#badid-0">wiki:0#0</a></p>');
+ $this->try_parse("[#0]", '<p><a href="#badid-0">#0</a></p>');
+ $this->try_parse("[#0 text]", '<p><a href="#badid-0">text</a></p>');
+ }
+
+ public function testTableAndHr() {
+ $this->try_parse('
+----
+||table||
+----
+||table||
+----
+',
+ '<hr /><table class="wikitable"><tbody><tr><td>table</td></tr></tbody></table><hr /><table class="wikitable"><tbody><tr><td>table</td></tr></tbody></table><hr />');
+ }
+
+
+ public function testBlock() {
+ $this->try_parse("{{{
+moge
+}}}
+", '<pre>moge
+</pre>');
+ $this->try_parse("{{{
+moge
+", '<pre>moge
+</pre>');
+ }
+
+ public function testInlinePreformatted() {
+ $this->try_parse("''ab''c''d{{{ef g''}} }}}h''}}}i''j.",
+ "<p><em>ab</em>c<em>d<tt>ef g''}} </tt>h</em>}}}i<em>j.</em></p>");
+ }
+
+ public function testInternalLink() {
+ $this->try_parse("[http://www.yahoo.co.jp/ external]",
+ '<p><a href="http://www.yahoo.co.jp/" class="external" rel="nofollow">external</a></p>');
+ $this->try_parse("[http://sourceforge.jp/ internal]",
+ '<p><a href="http://sourceforge.jp/">internal</a></p>');
+ $this->try_parse("[http://sourceforge.jp/projects/test/moge internal]",
+ '<p><a href="http://sourceforge.jp/projects/test/moge">internal</a></p>');
+ $this->try_parse("[http://test.sourceforge.jp/ external]",
+ '<p><a href="http://test.sourceforge.jp/" class="external" rel="nofollow">external</a></p>');
+ }
+
+ public function testHeadingCounter() {
+ $this->try_parse("
+== a
+== b
+== a
+== a
+",
+ '<h2 id="h2-a">a</h2><h2 id="h2-b">b</h2><h2 id="h2-a-2">a</h2><h2 id="h2-a-3">a</h2>');
+ }
+
+ public function testUselessParagraph() {
+ $po_out = '<div class="pageoutline"><div class="pageoutline-title"><div class="action"><button type="button" onClick="javascript:togglePageOutline(this)"><img src="//static.sourceforge.jp/wiki/images/icons/roll-up.gif" border="0"></button></div>Outline</div></div>';
+ $this->try_parse("[[PageOutline]]
+
+a
+
+[[PageOutline]]
+
+b
+",
+ "$po_out
+<p>a
+</p>$po_out
+<p>b
+</p>");
+
+ $this->try_parse("
+
+{{{
+pre
+}}}
+
+
+",
+ '<pre>pre
+</pre>');
+
+ $this->try_parse("a
+
+{{{ html
+<i>html block</i>
+}}}
+
+b
+",
+ '<p>a
+</p><i>html block</i>
+<p>b
+</p>');
+
+ }
+
+ public function testTableAndHTMLBlockBug() {
+ $this->try_parse('||table||
+{{{ html
+<ins>HTML</ins>
+}}}', '<table class="wikitable"><tbody><tr><td>table</td></tr></tbody></table><ins>HTML</ins>
+');
+ }
+
+ public function testSlashLinks() {
+ $this->try_parse('[/path/to/]', '<p><a href="/path/to/">/path/to/</a></p>');
+ $this->try_parse('[/path/to/ moge]', '<p><a href="/path/to/">moge</a></p>');
+ $this->try_parse('[//server.com/path moge]', '<p><a href="//server.com/path">moge</a></p>');
+ }
+
+ public function testTicketComment() {
+ $this->try_parse('comment:4:15142:1235097254',
+ '<p><a href="http://sourceforge.jp/ticket/browse.php?group_id=4&tid=15142#comment:4:15142:1235097254" class="ticket">comment:4:15142:1235097254</a></p>');
+ $this->try_parse('comment:1 comment:foo', '<p>comment:1 comment:foo</p>');
+ $this->try_parse('[comment:4:15142:1235097254 hiromichi-m] への返信',
+ '<p><a href="http://sourceforge.jp/ticket/browse.php?group_id=4&tid=15142#comment:4:15142:1235097254" class="ticket">hiromichi-m</a> への返信</p>');
+ $this->try_parse('[comment::123:456 グループID省略]なコメント', '<p><a href="http://sourceforge.jp/ticket/detail.php?id=123&cid=456" class="ticket">グループID省略</a>なコメント</p>');
+ $this->try_parse('[comment::123:456 グループID0]なコメントリンク', '<p><a href="http://sourceforge.jp/ticket/detail.php?id=123&cid=456" class="ticket">グループID0</a>なコメントリンク</p>');
+ $this->try_parse('[comment:123:456 2引数]なコメントリンク', '<p><a href="http://sourceforge.jp/ticket/detail.php?id=123&cid=456" class="ticket">2引数</a>なコメントリンク</p>');
+ }
+
+ public function testIRI() {
+ $iri = 'http://假定された有機交流電燈の.ひとつの青い照明です/';
+ $url = 'http://%E5%81%87%E5%AE%9A%E3%81%95%E3%82%8C%E3%81%9F%E6%9C%89%E6%A9%9F%E4%BA%A4%E6%B5%81%E9%9B%BB%E7%87%88%E3%81%AE.%E3%81%B2%E3%81%A8%E3%81%A4%E3%81%AE%E9%9D%92%E3%81%84%E7%85%A7%E6%98%8E%E3%81%A7%E3%81%99/';
+ $this->try_parse("$iri ひかりはたもち、その電燈は失はれ", "<p><a href=\"{$url}\" class=\"external\" rel=\"nofollow\">$iri</a> ひかりはたもち、その電燈は失はれ</p>");
+ $this->try_parse("[$iri]", "<p><a href=\"$url\" class=\"external\" rel=\"nofollow\">$iri</a></p>");
+ $this->try_parse("[$iri ひかりはたもち、その電燈は失はれ]", "<p><a href=\"$url\" class=\"external\" rel=\"nofollow\">ひかりはたもち、その電燈は失はれ</a></p>");
+ $this->try_parse('[/納豆.html ひきわり]', '<p><a href="/%E7%B4%8D%E8%B1%86.html">ひきわり</a></p>');
+ }
+
+ public function testSVNRev() {
+ $a_attr = 'href="http://svn/view?view=rev&root=unittest&revision=123" class="svn"';
+ $this->try_parse('r123', "<p><a $a_attr>r123</a></p>");
+ $this->try_parse('abcr123', '<p>abcr123</p>');
+ $this->try_parse('r123abc', '<p>r123abc</p>');
+ $this->try_parse('lead r123 trail', "<p>lead <a $a_attr>r123</a> trail</p>");
+ $this->try_parse('日本語r123テキスト', "<p>日本語<a $a_attr>r123</a>テキスト</p>");
+ }
+
+
+ public function testWithUTF8() {
+ ini_set('default_charset', 'utf-8');
+ ini_set('mbstring.internal_encoding', 'utf-8');
+ ini_set('mbstring.detect_order', 'EUC-JP,UTF-8,SJIS,JIS');
+ ini_set('mbstring.language', 'Japanese');
+ $input = '!SourceForge.JPシステムのバグを見つけた場合には [/projects/sourceforge/ticket?type%5B%5D=113&status%5B%5D=1 バグ]、
+CVSリポジトリの調整やアカウント削除等のサポート要求は [/projects/sourceforge/ticket?type%5B%5D=114&status%5B%5D=1 サポートリクエスト]へ、システムへの追加機能の要望等は [/projects/sourceforge/ticket?type%5B%5D=115&status%5B%5D=1 機能リクエスト]へ登録いただくようお願いいたします。その他の問い合わせについては、[/docs/SourceForge.JP%E3%81%AE%E9%80%A3%E7%B5%A1%E5%85%88 連絡先についての文書]をよくお読みください。';
+ $exp = '<p>SourceForge.JPシステムのバグを見つけた場合には <a href="/projects/sourceforge/ticket?type%5B%5D=113&status%5B%5D=1">バグ</a>、
+CVSリポジトリの調整やアカウント削除等のサポート要求は <a href="/projects/sourceforge/ticket?type%5B%5D=114&status%5B%5D=1">サポートリクエスト</a>へ、システムへの追加機能の要望等は <a href="/projects/sourceforge/ticket?type%5B%5D=115&status%5B%5D=1">機能リクエスト</a>へ登録いただくようお願いいたします。その他の問い合わせについては、<a href="/docs/SourceForge.JP%E3%81%AE%E9%80%A3%E7%B5%A1%E5%85%88">連絡先についての文書</a>をよくお読みください。</p>';
+ $this->try_parse($input, $exp);
+ }
+
+ public function testTracKeepNewLineMode() {
+ $this->p->setContext(array('trac.keep_newline' => true));
+ $this->try_parse('
+改行を
+全部
+br に
+マップする
+モード
+
+
+
+
+パラグラフは適切に分割される必要がある
+
+> block
+> quote
+> text
+さて
+', '<p>改行を
+<br />全部
+<br />br に
+<br />マップする
+<br />モード
+<br /></p><p>パラグラフは適切に分割される必要がある
+<br /></p><blockquote class="citation"><p>block
+<br />quote
+<br />text
+<br /></p></blockquote><p>さて
+<br /></p>');
+ $this->try_parse('
+ * リストでも改行保持
+ のテスト
+ * です
+', '<ul><li>リストでも改行保持
+<br />のテスト
+<br /></li><li>です
+<br /></li></ul>');
+ }
+
+ public function testInlinePluginAtStartOfLine() {
+ $this->try_parse('[[br]]test!', '<p><br />test!</p>');
+ }
+
+ public function testHashTrackerLink() {
+ $this->try_parse('#10', '<p><a href="http://sourceforge.jp/tracker.php?id=10" class="tracker">#10</a></p>');
+ $this->try_parse('#50010', '<p><a href="http://sourceforge.jp/ticket/detail.php?id=50010" class="tracker">#50010</a></p>');
+ }
+
+
+ public function testDisabledLinkWithExclamation() {
+ $context = array(
+ 'sfjp.group_name' => 'unittest',
+ 'site_root_url' => 'http://sourceforge.jp',
+ 'internal_url_regex' => '^http://sourceforge\.jp/',
+ 'svn_base_url' => 'http://svn/view',
+ 'disable.link.CamelCase' => true,
+ );
+ $p = new \sfjp\Wiki\Parser($context);
+ self::assertEquals('<p>CamelCase</p>', $p->parse('!CamelCase'));
+ }
+
+
+ protected function try_parse($text, $expect) {
+ self::assertEquals($expect, $this->p->parse($text));
+ }
+
+}
+
+// vim: set sts=4 sw=4 expandtab:
--- /dev/null
+<?php
+
+require_once 'PHPUnit/Framework/TestCase.php';
+
+class ParserTest extends PHPUnit_Framework_TestCase {
+ protected function setUp() {
+ $context = array(
+ 'sfjp.group_name' => 'unittest',
+ 'site_root_url' => 'http://sourceforge.jp',
+ 'internal_url_regex' => '^http://sourceforge\.jp/',
+ 'svn_base_url' => 'http://svn/view',
+ );
+ $this->p = new \sfjp\Wiki\Parser($context);
+ }
+
+ protected function tearDown() {
+
+ }
+
+ public function testContextKeyTransition() {
+ $v = 'hoge';
+ $this->p->setContext(array('plugin.order' => $v));
+ self::assertEquals($v, $this->p->getContext('plugin.order'));
+ self::assertEquals($v, $this->p->getContext('extension.acl_order'));
+
+ $v = 'fuga';
+ $this->p->setContext(array('extension.acl_order' => $v));
+ self::assertEquals($v, $this->p->getContext('plugin.order'));
+ self::assertEquals($v, $this->p->getContext('extension.acl_order'));
+ }
+}
+
+// vim: set sts=4 sw=4 expandtab:
--- /dev/null
+<?php
+
+require_once 'PHPUnit/Framework/TestCase.php';
+
+class StorageTest extends PHPUnit_Framework_TestCase {
+ protected $storage_path;
+ protected $s;
+
+ protected function setUp(){
+ $this->storage_path = dirname(__FILE__) . "filestore-test";
+ $this->s = new sfjp\Wiki\Storage\File(array('storage.file.datadir' => $this->storage_path));
+ }
+
+ protected function tearDown() {
+ $this->cleanup();
+ }
+
+ protected function cleanup() {
+ if (!is_dir($this->storage_path))
+ return;
+ $dh = opendir($this->storage_path);
+ if (!$dh) return;
+ while ($ent = readdir($dh)) {
+ if ($ent == '.' || $ent == '..')
+ continue;
+ unlink("{$this->storage_path}/{$ent}");
+ }
+ closedir($dh);
+ rmdir($this->storage_path);
+ }
+
+ public function testNew() {
+ self::assertTrue(isset($this->s));
+ }
+
+
+ public function testGetSet() {
+ $text = "hoge\nfuga\r\nyeah!";
+ $this->s->set('page1', $text);
+ self::assertEquals($text, $this->s->get('page1'));
+ }
+
+ public function testList() {
+ $this->cleanup();
+ $this->s->set('foo', 'foo 1');
+ $this->s->set('Bar', 'bar 2');
+ self::assertTrue(in_array('foo', $this->s->get_list()));
+ self::assertTrue(in_array('Bar', $this->s->get_list()));
+ self::assertTrue(!in_array('boo', $this->s->get_list()));
+ }
+
+ public function testRemove() {
+ $this->s->set('hoge', '1');
+ self::assertTrue(in_array('hoge', $this->s->get_list()));
+ $this->s->remove('hoge');
+ self::assertTrue(!in_array('hoge', $this->s->get_list()));
+ }
+}
--- /dev/null
+#!/usr/bin/phpunit
+<?php
+require_once 'PHPUnit/Framework/TestSuite.php';
+require_once dirname(__FILE__)."/lib/kses.php";
+
+spl_autoload_extensions('.php');
+spl_autoload_register('spl_autoload');
+chdir(dirname(__FILE__)."/..");
+
+class AllTests
+{
+ public static function suite()
+ {
+ $suite = new PHPUnit_Framework_TestSuite('sfjpWikiAll');
+
+ $dir = dirname(__FILE__);
+ $dh = opendir($dir);
+ while (($file = readdir($dh)) !== false) {
+ if (!preg_match('/^(.*Test)\.php$/', $file, $m))
+ continue;
+ require_once "{$dir}/{$file}";
+ $suite->addTestSuite($m[1]);
+ }
+ closedir($dh);
+ return $suite;
+ }
+
+ protected function setUp()
+ {
+ spl_autoload_register();
+ }
+
+}
+
--- /dev/null
+<?php
+
+# kses 0.2.2 - HTML/XHTML filter that only allows some elements and attributes
+# Copyright (C) 2002, 2003, 2005 Ulf Harnhammar
+#
+# This program is free software and open source software; you can redistribute
+# it and/or modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; either version 2 of the License,
+# or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA or visit
+# http://www.gnu.org/licenses/gpl.html
+#
+# *** CONTACT INFORMATION ***
+#
+# E-mail: metaur at users dot sourceforge dot net
+# Web page: http://sourceforge.net/projects/kses
+# Paper mail: Ulf Harnhammar
+# Ymergatan 17 C
+# 753 25 Uppsala
+# SWEDEN
+#
+# [kses strips evil scripts!]
+#
+
+#
+# Changes:
+# * 2008-01-18 Tatsuki Sugiura <sugi@osdn.jp>
+# * add callback attribute filter
+#
+
+
+function kses($string, $allowed_html, $allowed_protocols =
+ array('http', 'https', 'ftp', 'news', 'nntp', 'telnet',
+ 'gopher', 'mailto'))
+###############################################################################
+# This function makes sure that only the allowed HTML element names, attribute
+# names and attribute values plus only sane HTML entities will occur in
+# $string. You have to remove any slashes from PHP's magic quotes before you
+# call this function.
+###############################################################################
+{
+ $string = kses_no_null($string);
+ $string = kses_js_entities($string);
+ $string = kses_normalize_entities($string);
+ $string = kses_hook($string);
+ $allowed_html_fixed = kses_array_lc($allowed_html);
+ return kses_split($string, $allowed_html_fixed, $allowed_protocols);
+} # function kses
+
+
+function kses_hook($string)
+###############################################################################
+# You add any kses hooks here.
+###############################################################################
+{
+ return $string;
+} # function kses_hook
+
+
+function kses_version()
+###############################################################################
+# This function returns kses' version number.
+###############################################################################
+{
+ return '0.2.2';
+} # function kses_version
+
+
+function kses_split($string, $allowed_html, $allowed_protocols)
+###############################################################################
+# This function searches for HTML tags, no matter how malformed. It also
+# matches stray ">" characters.
+###############################################################################
+{
+ return preg_replace_callback('%(<'. # EITHER: <
+ '[^>]*'. # things that aren't >
+ '(>|$)'. # > or end of string
+ '|>)%', # OR: just a >
+ function ($m) use ($allowed_html, $allowed_protocols) { return kses_split2($m[1], $allowed_html, $allowed_protocols); },
+ $string);
+} # function kses_split
+
+
+function kses_split2($string, $allowed_html, $allowed_protocols)
+###############################################################################
+# This function does a lot of work. It rejects some very malformed things
+# like <:::>. It returns an empty string, if the element isn't allowed (look
+# ma, no strip_tags()!). Otherwise it splits the tag into an element and an
+# attribute list.
+###############################################################################
+{
+ $string = kses_stripslashes($string);
+
+ if (substr($string, 0, 1) != '<')
+ return '>';
+ # It matched a ">" character
+
+ if (!preg_match('%^<\s*(/\s*)?([a-zA-Z0-9]+)([^>]*)>?$%', $string, $matches))
+ return '';
+ # It's seriously malformed
+
+ $slash = trim($matches[1]);
+ $elem = $matches[2];
+ $attrlist = $matches[3];
+
+ if (!@isset($allowed_html[strtolower($elem)]))
+ return '';
+ # They are using a not allowed HTML element
+
+ if ($slash != '')
+ return "<$slash$elem>";
+ # No attributes are allowed for closing elements
+
+ return kses_attr("$slash$elem", $attrlist, $allowed_html,
+ $allowed_protocols);
+} # function kses_split2
+
+
+function kses_attr($element, $attr, $allowed_html, $allowed_protocols)
+###############################################################################
+# This function removes all attributes, if none are allowed for this element.
+# If some are allowed it calls kses_hair() to split them further, and then it
+# builds up new HTML code from the data that kses_hair() returns. It also
+# removes "<" and ">" characters, if there are any left. One more thing it
+# does is to check if the tag has a closing XHTML slash, and if it does,
+# it puts one in the returned code as well.
+###############################################################################
+{
+# Is there a closing XHTML slash at the end of the attributes?
+
+ $xhtml_slash = '';
+ if (preg_match('%\s/\s*$%', $attr))
+ $xhtml_slash = ' /';
+
+# Are any attributes allowed at all for this element?
+
+ if (@count($allowed_html[strtolower($element)]) == 0)
+ return "<$element$xhtml_slash>";
+
+# Split it
+
+ $attrarr = kses_hair($attr, $allowed_protocols);
+
+# Go through $attrarr, and save the allowed attributes for this element
+# in $attr2
+
+ $attr2 = '';
+
+ foreach ($attrarr as $arreach)
+ {
+ if (!@isset($allowed_html[strtolower($element)]
+ [strtolower($arreach['name'])]))
+ continue; # the attribute is not allowed
+
+ $current = $allowed_html[strtolower($element)]
+ [strtolower($arreach['name'])];
+
+ if (!is_array($current))
+ $attr2 .= ' '.$arreach['whole'];
+ # there are no checks
+
+ else
+ {
+ # there are some checks
+ $ok = true;
+ foreach ($current as $currkey => $currval)
+ if (!kses_check_attr_val($arreach['value'], $arreach['vless'],
+ $currkey, $currval))
+ { $ok = false; break; }
+
+ if ($ok)
+ $attr2 .= ' '.$arreach['whole']; # it passed them
+ } # if !is_array($current)
+ } # foreach
+
+# Remove any "<" or ">" characters
+
+ $attr2 = preg_replace('/[<>]/', '', $attr2);
+
+ return "<$element$attr2$xhtml_slash>";
+} # function kses_attr
+
+
+function kses_hair($attr, $allowed_protocols)
+###############################################################################
+# This function does a lot of work. It parses an attribute list into an array
+# with attribute data, and tries to do the right thing even if it gets weird
+# input. It will add quotes around attribute values that don't have any quotes
+# or apostrophes around them, to make it easier to produce HTML code that will
+# conform to W3C's HTML specification. It will also remove bad URL protocols
+# from attribute values.
+###############################################################################
+{
+ $attrarr = array();
+ $mode = 0;
+ $attrname = '';
+
+# Loop through the whole attribute list
+
+ while (strlen($attr) != 0)
+ {
+ $working = 0; # Was the last operation successful?
+
+ switch ($mode)
+ {
+ case 0: # attribute name, href for instance
+
+ if (preg_match('/^([-a-zA-Z]+)/', $attr, $match))
+ {
+ $attrname = $match[1];
+ $working = $mode = 1;
+ $attr = preg_replace('/^[-a-zA-Z]+/', '', $attr);
+ }
+
+ break;
+
+ case 1: # equals sign or valueless ("selected")
+
+ if (preg_match('/^\s*=\s*/', $attr)) # equals sign
+ {
+ $working = 1; $mode = 2;
+ $attr = preg_replace('/^\s*=\s*/', '', $attr);
+ break;
+ }
+
+ if (preg_match('/^\s+/', $attr)) # valueless
+ {
+ $working = 1; $mode = 0;
+ $attrarr[] = array
+ ('name' => $attrname,
+ 'value' => '',
+ 'whole' => $attrname,
+ 'vless' => 'y');
+ $attr = preg_replace('/^\s+/', '', $attr);
+ }
+
+ break;
+
+ case 2: # attribute value, a URL after href= for instance
+
+ if (preg_match('/^"([^"]*)"(\s+|$)/', $attr, $match))
+ # "value"
+ {
+ if (strtolower($attrname) !== "style") {
+ $thisval = kses_bad_protocol($match[1], $allowed_protocols);
+ } else {
+ $thisval = $match[1];
+ }
+
+ $attrarr[] = array
+ ('name' => $attrname,
+ 'value' => $thisval,
+ 'whole' => "$attrname=\"$thisval\"",
+ 'vless' => 'n');
+ $working = 1; $mode = 0;
+ $attr = preg_replace('/^"[^"]*"(\s+|$)/', '', $attr);
+ break;
+ }
+
+ if (preg_match("/^'([^']*)'(\s+|$)/", $attr, $match))
+ # 'value'
+ {
+ if (strtolower($attrname) !== "style") {
+ $thisval = kses_bad_protocol($match[1], $allowed_protocols);
+ } else {
+ $thisval = $match[1];
+ }
+
+ $attrarr[] = array
+ ('name' => $attrname,
+ 'value' => $thisval,
+ 'whole' => "$attrname='$thisval'",
+ 'vless' => 'n');
+ $working = 1; $mode = 0;
+ $attr = preg_replace("/^'[^']*'(\s+|$)/", '', $attr);
+ break;
+ }
+
+ if (preg_match("%^([^\s\"']+)(\s+|$)%", $attr, $match))
+ # value
+ {
+ if (strtolower($attrname) !== "style") {
+ $thisval = kses_bad_protocol($match[1], $allowed_protocols);
+ } else {
+ $thisval = $match[1];
+ }
+
+ $attrarr[] = array
+ ('name' => $attrname,
+ 'value' => $thisval,
+ 'whole' => "$attrname=\"$thisval\"",
+ 'vless' => 'n');
+ # We add quotes to conform to W3C's HTML spec.
+ $working = 1; $mode = 0;
+ $attr = preg_replace("%^[^\s\"']+(\s+|$)%", '', $attr);
+ }
+
+ break;
+ } # switch
+
+ if ($working == 0) # not well formed, remove and try again
+ {
+ $attr = kses_html_error($attr);
+ $mode = 0;
+ }
+ } # while
+
+ if ($mode == 1)
+ # special case, for when the attribute list ends with a valueless
+ # attribute like "selected"
+ $attrarr[] = array
+ ('name' => $attrname,
+ 'value' => '',
+ 'whole' => $attrname,
+ 'vless' => 'y');
+
+ return $attrarr;
+} # function kses_hair
+
+
+function kses_check_attr_val($value, $vless, $checkname, $checkvalue)
+###############################################################################
+# This function performs different checks for attribute values. The currently
+# implemented checks are "maxlen", "minlen", "maxval", "minval" and "valueless"
+# with even more checks to come soon.
+###############################################################################
+{
+ $ok = true;
+
+ switch (strtolower($checkname))
+ {
+ case 'maxlen':
+ # The maxlen check makes sure that the attribute value has a length not
+ # greater than the given value. This can be used to avoid Buffer Overflows
+ # in WWW clients and various Internet servers.
+
+ if (strlen($value) > $checkvalue)
+ $ok = false;
+ break;
+
+ case 'minlen':
+ # The minlen check makes sure that the attribute value has a length not
+ # smaller than the given value.
+
+ if (strlen($value) < $checkvalue)
+ $ok = false;
+ break;
+
+ case 'maxval':
+ # The maxval check does two things: it checks that the attribute value is
+ # an integer from 0 and up, without an excessive amount of zeroes or
+ # whitespace (to avoid Buffer Overflows). It also checks that the attribute
+ # value is not greater than the given value.
+ # This check can be used to avoid Denial of Service attacks.
+
+ if (!preg_match('/^\s{0,6}[0-9]{1,6}\s{0,6}$/', $value))
+ $ok = false;
+ if ($value > $checkvalue)
+ $ok = false;
+ break;
+
+ case 'minval':
+ # The minval check checks that the attribute value is a positive integer,
+ # and that it is not smaller than the given value.
+
+ if (!preg_match('/^\s{0,6}[0-9]{1,6}\s{0,6}$/', $value))
+ $ok = false;
+ if ($value < $checkvalue)
+ $ok = false;
+ break;
+
+ case 'valueless':
+ # The valueless check checks if the attribute has a value
+ # (like <a href="blah">) or not (<option selected>). If the given value
+ # is a "y" or a "Y", the attribute must not have a value.
+ # If the given value is an "n" or an "N", the attribute must have one.
+
+ if (strtolower($checkvalue) != $vless)
+ $ok = false;
+ break;
+
+ case 'cssfilter':
+ # The value is checked CSS expression
+ $ok = $checkvalue ? kses_verify_css($value) : true;
+ break;
+
+ case 'callback':
+ # The value is checked specified function
+ if (is_callable($checkvalue))
+ {
+ $ok = call_user_func($checkvalue, $value);
+ }
+ else
+ {
+ $ok = false;
+ trigger_error("can't call callback function '$checkvalue'");
+ }
+ break;
+ } # switch
+
+ return $ok;
+} # function kses_check_attr_val
+
+
+function kses_bad_protocol($string, $allowed_protocols)
+###############################################################################
+# This function removes all non-allowed protocols from the beginning of
+# $string. It ignores whitespace and the case of the letters, and it does
+# understand HTML entities. It does its work in a while loop, so it won't be
+# fooled by a string like "javascript:javascript:alert(57)".
+###############################################################################
+{
+ $string = kses_no_null($string);
+ # commented out to stop breaking multibyte chars
+ # see http://groups.google.com/group/wp-ja-pkg/browse_thread/thread/36c994dbd9276754/c3c0adb86a8b725d for details.
+ # - sugi 2008-06-12
+
+ #$string = preg_replace('/\xad+/', '', $string); # deals with Opera "feature"
+ $string2 = $string.'a';
+
+ while ($string != $string2)
+ {
+ $string2 = $string;
+ $string = kses_bad_protocol_once($string, $allowed_protocols);
+ } # while
+
+ return $string;
+} # function kses_bad_protocol
+
+
+function kses_no_null($string)
+###############################################################################
+# This function removes any NULL characters in $string.
+###############################################################################
+{
+ $string = preg_replace('/\0+/', '', $string);
+ $string = preg_replace('/(\\\\0)+/', '', $string);
+
+ return $string;
+} # function kses_no_null
+
+
+function kses_stripslashes($string)
+###############################################################################
+# This function changes the character sequence \" to just "
+# It leaves all other slashes alone. It's really weird, but the quoting from
+# preg_replace(//e) seems to require this.
+###############################################################################
+{
+ return preg_replace('%\\\\"%', '"', $string);
+} # function kses_stripslashes
+
+
+function kses_array_lc($inarray)
+###############################################################################
+# This function goes through an array, and changes the keys to all lower case.
+###############################################################################
+{
+ $outarray = array();
+
+ foreach ($inarray as $inkey => $inval)
+ {
+ $outkey = strtolower($inkey);
+ $outarray[$outkey] = array();
+
+ foreach ($inval as $inkey2 => $inval2)
+ {
+ $outkey2 = strtolower($inkey2);
+ $outarray[$outkey][$outkey2] = $inval2;
+ } # foreach $inval
+ } # foreach $inarray
+
+ return $outarray;
+} # function kses_array_lc
+
+
+function kses_js_entities($string)
+###############################################################################
+# This function removes the HTML JavaScript entities found in early versions of
+# Netscape 4.
+###############################################################################
+{
+ return preg_replace('%&\s*\{[^}]*(\}\s*;?|$)%', '', $string);
+} # function kses_js_entities
+
+
+function kses_html_error($string)
+###############################################################################
+# This function deals with parsing errors in kses_hair(). The general plan is
+# to remove everything to and including some whitespace, but it deals with
+# quotes and apostrophes as well.
+###############################################################################
+{
+ return preg_replace('/^("[^"]*("|$)|\'[^\']*(\'|$)|\S)*\s*/', '', $string);
+} # function kses_html_error
+
+
+function kses_bad_protocol_once($string, $allowed_protocols)
+###############################################################################
+# This function searches for URL protocols at the beginning of $string, while
+# handling whitespace and HTML entities.
+###############################################################################
+{
+ return preg_replace_callback('/^((&[^;]*;|[\sA-Za-z0-9])*)'.
+ '(:|:|&#[Xx]3[Aa];)\s*/',
+ function ($m) { return kses_bad_protocol_once2($m[1], $allowed_protocols); },
+ $string);
+} # function kses_bad_protocol_once
+
+
+function kses_bad_protocol_once2($string, $allowed_protocols)
+###############################################################################
+# This function processes URL protocols, checks to see if they're in the white-
+# list or not, and returns different data depending on the answer.
+###############################################################################
+{
+ $string2 = kses_decode_entities($string);
+ $string2 = preg_replace('/\s/', '', $string2);
+ $string2 = kses_no_null($string2);
+ $string2 = preg_replace('/\xad+/', '', $string2);
+ # deals with Opera "feature"
+ $string2 = strtolower($string2);
+
+ $allowed = false;
+ foreach ($allowed_protocols as $one_protocol)
+ if (strtolower($one_protocol) == $string2)
+ {
+ $allowed = true;
+ break;
+ }
+
+ if ($allowed)
+ return "$string2:";
+ else
+ return '';
+} # function kses_bad_protocol_once2
+
+
+function kses_normalize_entities($string)
+###############################################################################
+# This function normalizes HTML entities. It will convert "AT&T" to the correct
+# "AT&T", ":" to ":", "&#XYZZY;" to "&#XYZZY;" and so on.
+###############################################################################
+{
+# Disarm all entities by converting & to &
+
+ $string = str_replace('&', '&', $string);
+
+# Change back the allowed entities in our entity whitelist
+
+ $string = preg_replace('/&([A-Za-z][A-Za-z0-9]{0,19});/',
+ '&\\1;', $string);
+ $string = preg_replace_callback('/&#0*([0-9]{1,5});/',
+ function ($m) { return kses_normalize_entities2($m[1]); },
+ $string);
+ $string = preg_replace('/&#([Xx])0*(([0-9A-Fa-f]{2}){1,2});/',
+ '&#\\1\\2;', $string);
+
+ return $string;
+} # function kses_normalize_entities
+
+
+function kses_normalize_entities2($i)
+###############################################################################
+# This function helps kses_normalize_entities() to only accept 16 bit values
+# and nothing more for &#number; entities.
+###############################################################################
+{
+ return (($i > 65535) ? "&#$i;" : "&#$i;");
+} # function kses_normalize_entities2
+
+
+function kses_decode_entities($string)
+###############################################################################
+# This function decodes numeric HTML entities (A and A). It doesn't
+# do anything with other entities like ä, but we don't need them in the
+# URL protocol whitelisting system anyway.
+###############################################################################
+{
+ $string = preg_replace('/&#([0-9]+);/', function($m){return chr($m[1]);}, $string);
+ $string = preg_replace('/&#[Xx]([0-9A-Fa-f]+);/', function($m){return chr(hexdec($m[1]));},
+ $string);
+
+ return $string;
+} # function kses_decode_entities
+
+function _sanitize_hexentity($match) {
+ return '&#' . intval("0".substr($match[0], 2, strlen($match[0]) -3)) . ';';
+}
+
+function kses_verify_css($cssstr)
+###############################################################################
+# CSS expression checker for IE.
+# See URLs below for details;
+# - http://openmya.hacker.jp/hasegawa/security/expression.txt
+# - http://archive.openmya.devnull.jp/2006.08/msg00369.html
+# - https://www.webappsec.jp/modules/bwiki/index.php?IE%A4%CEexpression%A4%C8url
+###############################################################################
+{
+ if (!$cssstr) return true;
+ $ok = false;
+ $cssstr = preg_replace_callback('/&#x[0-9A-F]+;/i', '_sanitize_hexentity', $cssstr);
+ $cssstr = mb_decode_numericentity($cssstr, array(0, 0xFFFFFF, 0, 0xFFFFFF), 'UTF-8');
+ $cssstr = preg_replace('/\x5c(\d+)/', '&#x\1;', $cssstr);
+ $cssstr = preg_replace_callback('/&#x[0-9A-F]+;/i', '_sanitize_hexentity', $cssstr);
+ $cssstr = mb_decode_numericentity($cssstr, array(0, 0xFFFFFF, 0, 0xFFFFFF), 'UTF-8');
+ $cssstr = preg_replace('{/\*.*?\*/|\x00+}', '', $cssstr);
+ $enc_orig = mb_regex_encoding();
+ mb_regex_encoding('UTF-8');
+ # e x p r e s s i o n
+ # FF45 FF58 FF50 FF52 FF45 FF53 FF53 FF49 FF4F FF4E
+ # FF25 FF38 FF30 FF32 FF25 FF33 FF33 FF29 FF2F FF2E
+ # 0280 0274
+ # 027F
+ $ok = !mb_eregi('javascript:|@import|[e\x{ff45}\x{ff25}][x\x{ff58}\x{ff38}][p\x{ff50}\x{ff30}][r\x{ff52}\x{ff32}\x{0280}][e\x{ff45}\x{ff25}][s\x{ff53}\x{ff33}]{2}[i\x{ff49}\x{ff29}][o\x{ff4f}\x{ff2f}][n\x{ff4e}\x{ff2e}\x{0274}\x{207f}]', $cssstr);
+ mb_regex_encoding($enc_orig);
+ return $ok;
+}
+
+?>