From c4f9eab9a433202ac2cb0325d3e83a4a2b636057 Mon Sep 17 00:00:00 2001 From: Tatsuki Sugiura Date: Tue, 28 Jul 2015 19:04:11 +0900 Subject: [PATCH] Import current code. --- sfjp/wiki/exception/base.php | 3 + sfjp/wiki/exception/internal_error.php | 4 + sfjp/wiki/exception/invalid_argument.php | 4 + sfjp/wiki/exception/plugin_error.php | 3 + sfjp/wiki/formatter/base.php | 48 ++ sfjp/wiki/formatter/buffered_delegator.php | 166 ++++++ sfjp/wiki/formatter/html.php | 141 +++++ sfjp/wiki/parser.php | 57 ++ sfjp/wiki/plugin/base.php | 93 ++++ sfjp/wiki/plugin/br.php | 16 + sfjp/wiki/plugin/footnote.php | 43 ++ sfjp/wiki/plugin/pagebreak.php | 10 + sfjp/wiki/plugin/pageoutline.php | 67 +++ sfjp/wiki/processor/auto.php | 21 + sfjp/wiki/processor/base.php | 280 ++++++++++ sfjp/wiki/processor/code.php | 15 + sfjp/wiki/processor/comment.php | 8 + sfjp/wiki/processor/denied.php | 18 + sfjp/wiki/processor/html.php | 73 +++ sfjp/wiki/processor/pre.php | 11 + sfjp/wiki/processor/trac.php | 867 +++++++++++++++++++++++++++++ sfjp/wiki/processor/trac_oneline.php | 22 + sfjp/wiki/storage/base.php | 13 + sfjp/wiki/storage/dummy.php | 15 + sfjp/wiki/storage/file.php | 49 ++ sfjpWikiParser.php | 21 + test/FormatTest.php | 572 +++++++++++++++++++ test/ParserTest.php | 33 ++ test/StorageTest.php | 58 ++ test/all.php | 34 ++ test/lib/kses.php | 628 +++++++++++++++++++++ 31 files changed, 3393 insertions(+) create mode 100644 sfjp/wiki/exception/base.php create mode 100644 sfjp/wiki/exception/internal_error.php create mode 100644 sfjp/wiki/exception/invalid_argument.php create mode 100644 sfjp/wiki/exception/plugin_error.php create mode 100644 sfjp/wiki/formatter/base.php create mode 100644 sfjp/wiki/formatter/buffered_delegator.php create mode 100644 sfjp/wiki/formatter/html.php create mode 100644 sfjp/wiki/parser.php create mode 100644 sfjp/wiki/plugin/base.php create mode 100644 sfjp/wiki/plugin/br.php create mode 100644 sfjp/wiki/plugin/footnote.php create mode 100644 sfjp/wiki/plugin/pagebreak.php create mode 100644 sfjp/wiki/plugin/pageoutline.php create mode 100644 sfjp/wiki/processor/auto.php create mode 100644 sfjp/wiki/processor/base.php create mode 100644 sfjp/wiki/processor/code.php create mode 100644 sfjp/wiki/processor/comment.php create mode 100644 sfjp/wiki/processor/denied.php create mode 100644 sfjp/wiki/processor/html.php create mode 100644 sfjp/wiki/processor/pre.php create mode 100644 sfjp/wiki/processor/trac.php create mode 100644 sfjp/wiki/processor/trac_oneline.php create mode 100644 sfjp/wiki/storage/base.php create mode 100644 sfjp/wiki/storage/dummy.php create mode 100644 sfjp/wiki/storage/file.php create mode 100644 sfjpWikiParser.php create mode 100644 test/FormatTest.php create mode 100644 test/ParserTest.php create mode 100644 test/StorageTest.php create mode 100755 test/all.php create mode 100644 test/lib/kses.php diff --git a/sfjp/wiki/exception/base.php b/sfjp/wiki/exception/base.php new file mode 100644 index 0000000..93b853a --- /dev/null +++ b/sfjp/wiki/exception/base.php @@ -0,0 +1,3 @@ +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 diff --git a/sfjp/wiki/formatter/buffered_delegator.php b/sfjp/wiki/formatter/buffered_delegator.php new file mode 100644 index 0000000..45cfd8b --- /dev/null +++ b/sfjp/wiki/formatter/buffered_delegator.php @@ -0,0 +1,166 @@ +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)) . ")
"; + foreach ($node->children as $c) { + $ret .= $this->_inspect_node($c, $level+1); + } + return $ret; + } +} \ No newline at end of file diff --git a/sfjp/wiki/formatter/html.php b/sfjp/wiki/formatter/html.php new file mode 100644 index 0000000..acef207 --- /dev/null +++ b/sfjp/wiki/formatter/html.php @@ -0,0 +1,141 @@ +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).""; + case "table": + return + self::tag_builder("table", + array_merge(array("class" => "wikitable"), + $opt)) + . ''; + 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 ""; + case "table": + return ""; + case "link": + return ""; + 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 ""; + } + } + } + + 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 diff --git a/sfjp/wiki/parser.php b/sfjp/wiki/parser.php new file mode 100644 index 0000000..14feee5 --- /dev/null +++ b/sfjp/wiki/parser.php @@ -0,0 +1,57 @@ +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(); + } +} diff --git a/sfjp/wiki/plugin/base.php b/sfjp/wiki/plugin/base.php new file mode 100644 index 0000000..d058b83 --- /dev/null +++ b/sfjp/wiki/plugin/base.php @@ -0,0 +1,93 @@ +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 diff --git a/sfjp/wiki/plugin/br.php b/sfjp/wiki/plugin/br.php new file mode 100644 index 0000000..d1eab7b --- /dev/null +++ b/sfjp/wiki/plugin/br.php @@ -0,0 +1,16 @@ +getFormatter()->open_element('newline'); + } +} \ No newline at end of file diff --git a/sfjp/wiki/plugin/footnote.php b/sfjp/wiki/plugin/footnote.php new file mode 100644 index 0000000..22d8c39 --- /dev/null +++ b/sfjp/wiki/plugin/footnote.php @@ -0,0 +1,43 @@ +formatter; + $ret = ''; + if ($this->count == 1) { + $ret .= $fmt->raw_node('
    '); + } + $ret .= $fmt->raw_node("
  1. count}\">" + . "count}\">*{$this->count}" + . "{$this->message}
  2. "); + if (FootNote::$counter - 1 == $this->count) { + $ret .= $fmt->raw_node('
'); + } + 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( + "count}\" class=\"footnote-ref\">" + . "count}\">*{$proc->count}"); + } +} \ No newline at end of file diff --git a/sfjp/wiki/plugin/pagebreak.php b/sfjp/wiki/plugin/pagebreak.php new file mode 100644 index 0000000..d53c2a5 --- /dev/null +++ b/sfjp/wiki/plugin/pagebreak.php @@ -0,0 +1,10 @@ +getFormatter()->raw_node('
'); + } +} \ No newline at end of file diff --git a/sfjp/wiki/plugin/pageoutline.php b/sfjp/wiki/plugin/pageoutline.php new file mode 100644 index 0000000..bae632f --- /dev/null +++ b/sfjp/wiki/plugin/pageoutline.php @@ -0,0 +1,67 @@ +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( + '
' + . '
' + . '' + . '
' + . $this->__($opt['title']) + . '
' . $parser->parse($text) . '
'); + } + + +} \ No newline at end of file diff --git a/sfjp/wiki/processor/auto.php b/sfjp/wiki/processor/auto.php new file mode 100644 index 0000000..75cea59 --- /dev/null +++ b/sfjp/wiki/processor/auto.php @@ -0,0 +1,21 @@ +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; + } +} diff --git a/sfjp/wiki/processor/base.php b/sfjp/wiki/processor/base.php new file mode 100644 index 0000000..8b8711f --- /dev/null +++ b/sfjp/wiki/processor/base.php @@ -0,0 +1,280 @@ + '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); + } + } +} diff --git a/sfjp/wiki/processor/code.php b/sfjp/wiki/processor/code.php new file mode 100644 index 0000000..26f1570 --- /dev/null +++ b/sfjp/wiki/processor/code.php @@ -0,0 +1,15 @@ +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; + } +} diff --git a/sfjp/wiki/processor/comment.php b/sfjp/wiki/processor/comment.php new file mode 100644 index 0000000..7ac4322 --- /dev/null +++ b/sfjp/wiki/processor/comment.php @@ -0,0 +1,8 @@ +formatted_text = ''; + return $this; + } +} \ No newline at end of file diff --git a/sfjp/wiki/processor/denied.php b/sfjp/wiki/processor/denied.php new file mode 100644 index 0000000..ef481bc --- /dev/null +++ b/sfjp/wiki/processor/denied.php @@ -0,0 +1,18 @@ +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 = '[Plugin Load Denied: ' . htmlspecialchars($this->name) . ']'; + } + return $this; + } +} \ No newline at end of file diff --git a/sfjp/wiki/processor/html.php b/sfjp/wiki/processor/html.php new file mode 100644 index 0000000..50d543c --- /dev/null +++ b/sfjp/wiki/processor/html.php @@ -0,0 +1,73 @@ + 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; + } +} diff --git a/sfjp/wiki/processor/pre.php b/sfjp/wiki/processor/pre.php new file mode 100644 index 0000000..4c965cb --- /dev/null +++ b/sfjp/wiki/processor/pre.php @@ -0,0 +1,11 @@ +getFormatter(); + $this->formatted_text = $fmt->raw_node("
" . htmlspecialchars($text) . "
"); + if (!$this->hasContext("parent_processor")) + $this->formatted_text .= $fmt->cleanup(); + return $this; + } +} \ No newline at end of file diff --git a/sfjp/wiki/processor/trac.php b/sfjp/wiki/processor/trac.php new file mode 100644 index 0000000..bdfb5e4 --- /dev/null +++ b/sfjp/wiki/processor/trac.php @@ -0,0 +1,867 @@ +(?:\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(''."$msg"); + } + $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("
def list not implemented yet.
"); # 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 + | (?getUriRegex(false) . ') # 4: URI + | (?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"); + } +} diff --git a/sfjp/wiki/processor/trac_oneline.php b/sfjp/wiki/processor/trac_oneline.php new file mode 100644 index 0000000..aa7984b --- /dev/null +++ b/sfjp/wiki/processor/trac_oneline.php @@ -0,0 +1,22 @@ +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 ''; + } + } +} diff --git a/sfjp/wiki/storage/base.php b/sfjp/wiki/storage/base.php new file mode 100644 index 0000000..c0c4ff9 --- /dev/null +++ b/sfjp/wiki/storage/base.php @@ -0,0 +1,13 @@ +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 diff --git a/sfjp/wiki/storage/dummy.php b/sfjp/wiki/storage/dummy.php new file mode 100644 index 0000000..0a9944b --- /dev/null +++ b/sfjp/wiki/storage/dummy.php @@ -0,0 +1,15 @@ +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 diff --git a/sfjpWikiParser.php b/sfjpWikiParser.php new file mode 100644 index 0000000..dbe6369 --- /dev/null +++ b/sfjpWikiParser.php @@ -0,0 +1,21 @@ + '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', '

abc

'); + $this->try_parse("abc abc\nabc", "

abc abc\nabc

"); + } + + public function testParseHeadings() { + $this->try_parse('= abc =', '

abc

'); + $this->try_parse("= 日本語 =\r\n", '

日本語

'); + $this->try_parse('= abc', '

abc

'); + $this->try_parse('= abc = ', '

abc

'); + $this->try_parse('= abc ', '

abc

'); + $this->try_parse('= abc = #moge', '

abc

'); + $this->try_parse('= abc #moge', '

abc

'); + $this->try_parse('=abc', '

=abc

'); + $this->try_parse(' = abc', '
= abc
'); + } + + public function testParseListMark() { + $text = ' + * list1 + * level2 + * level2, too. + continue list item. + continue! + continue also! + * moge + * level1 item + * level1 + *this not list +'; + + $expect = ''; + 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 = '
  1. ol num +
  1. ol alpha +
  1. ol ALPHA +
  1. ol roma +
  1. ol ROMA +
'; + 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 = '
  1. level1 ordered +
    • unordered in ordered +
  2. ordered list +
'; + 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 = '
  1. list1 +
    1. level2 +
    2. level2, too. +
    3. moge +
  2. level1 item +
  3. level1 +1.3. this not list +
'; + self::assertEquals($expect, $this->p->parse($text)); + } + + /* + public function testParseDefinitionList() { + self::assertEquals('yet', $this->p->parse('=abc')); + } + */ + + public function testParseHTMLEscape() { + $this->try_parse("'''>moge'''<", '

>moge<

'); + } + + public function testParseInlineBold() { + $this->try_parse("'''moge'''", '

moge

'); + } + + public function testParseInlineItalic() { + $this->try_parse("''abc''", '

abc

'); + $this->try_parse("''ab", '

ab

'); + $this->try_parse("ab ''bc\n''cd", "

ab bc\ncd

"); + } + + public function testParseInlineBolditalic() { + $this->try_parse("'''''moge'''''", + '

moge

'); + $this->try_parse("'''''''''abc'", + "

'abc'

"); + } + + public function testParseInlineUndeline() { + $this->try_parse("__abc__", + '

abc

'); + } + + public function testParseInlineStrike() { + $this->try_parse('~~abc~~', '

abc

'); + $this->try_parse('~~~abcef~~~~~', '

~abcef~

'); + } + + public function testParseInlineSuperscript() { + $this->try_parse('^ abc^', '

abc

'); + $this->try_parse('^^^ abc ^^', '

abc

'); + $this->try_parse("head ^ abc\ndef^tail", "

head abc\ndeftail

"); + } + + public function testParseInlineSubscript() { + $this->try_parse(',,abc ,,', '

abc

'); + } + + public function testParseLinks() { + $this->try_parse('http://url.com/path', '

http://url.com/path

'); + $this->try_parse('https://url.com/path', '

https://url.com/path

'); + } + + public function testParseBlacketLink() { + self::assertEquals( + '

a/b/日本語

', + $this->p->parse("[a/b/日本語]") + ); + } + + public function testParseLinkFragment() { + $this->try_parse('[test1#frag]', '

test1#frag

'); + $this->try_parse('[test2#frag text]', '

text

'); + $this->try_parse('wiki:test3#frag', '

wiki:test3#frag

'); + + $this->try_parse('wiki:"test4#frag"', '

wiki:"test4#frag"

'); + $this->try_parse('["test5#frag" text]', '

text

'); + $this->try_parse('[wiki:"test6#frag" text]', '

text

'); + $this->try_parse('[wiki:"test7 page name#frag" text]', '

text

'); + $this->try_parse('[#frag]', '

#frag

'); + $this->try_parse('["#frag" text]', '

text

'); + + } + + public function testParseQuote() { + self::assertEquals( + "

abc +

def +ghi +

jkl +

normal

", + $this->p->parse(">> abc\n> def\n> ghi\n>>> jkl\nnormal") + ); +$this->try_parse("> abc\n> > with space\n> > > 3rd", '

abc +

with space +

3rd

'); + } + + public function testParseIndent() { + $this->try_parse(" abc", '
abc
'); + $this->try_parse(" abc\n def\nghi", + '
abc +def +

ghi

'); + self::assertEquals('
abc +def +
2nd nest +
3rd +
2nd +
', + $this->p->parse(" abc\n def\n 2nd nest\n 3rd\n 2nd\n * clear by list")); + } + + public function testParseInternalURIPrweb() { + $this->try_parse("prweb:/", '

prweb:/

'); + $this->try_parse("prweb:/path/to/file", '

prweb:/path/to/file

'); + $this->try_parse("prweb:project-name:/url-to/the.page", '

prweb:project-name:/url-to/the.page

'); + } + + public function testParseInternalURIUser() { + $this->try_parse("user:sugi", '

user:sugi

'); + $this->try_parse("id:sugi", '

id:sugi

'); + $this->try_parse("users:sugi", '

users:sugi

'); + } + + public function testParseURIMailTo() { + $this->try_parse("mailto:sugi@osdn.jp", '

mailto:sugi@osdn.jp

'); + $this->try_parse("[mailto:a.b=c+d@e.f メール]", '

メール

'); + $this->try_parse("mailto:bad@てすと", '

mailto:bad@てすと

'); + } + + public function testParseEscape() { + $this->try_parse("!`", '

`

'); + $this->try_parse("!^てすと!^", '

^てすと^

'); + $this->try_parse("!~~", '

~~

'); + $this->try_parse("!__", '

__

'); + $this->try_parse("!WikiName", '

WikiName

'); + $this->try_parse("![[Plugin]]", '

[[Plugin]]

'); + $this->try_parse("!!", '

!!

'); + } + + public function testParseEscapeBlock() { + $this->try_parse("!> Equote", '

> Equote

'); + $this->try_parse("!------", '

------

'); + $this->try_parse("!||escaped||table||", '

||escaped||table||

'); + $this->try_parse("!= not header =", '

= not header =

'); + $this->try_parse(" !* abc", '
* abc
'); + } + + 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", + '

isbn:123

'); + $this->p->setContext(array('amazon_affiliate_id' => 'afid-test')); + $this->try_parse("isbn:123", + '

isbn:123

'); + } + + public function testParseGroupWikiPage() { + $this->try_parse('wiki:groupname:PageName', '

wiki:groupname:PageName

'); + $this->try_parse('wiki:group-name:PageName', '

wiki:group-name:PageName

'); + $this->try_parse('wiki:group_name:PageName', '

wiki:group_name:PageName

'); + + } + + public function testParseOrer() { + $this->try_parse('[PageName [[BR]]]', + '

[[BR]]

'); + /* do not support currently + $this->try_parse("abc'''def[PageName la''bee''eel]tex'''t", + '

abcdeflabeeeeltext

'); + */ + $this->try_parse("abc'''def[PageName la''bee''eel]tex'''t", + "

abcdefla''bee''eeltext

"); + + $this->try_parse('[__Page,,Name^^ label]', '

label

'); + $this->try_parse('["__Page,,Name^^" label]', '

label

'); + $this->try_parse('[wiki:"__Page,,Name^^" label]', '

label

'); + $this->try_parse('http://aaaa[bbb]ccc', '

http://aaaabbbccc

'); + $this->try_parse('!http://aaaa[bbb]ccc', '

http://aaaabbbccc

'); + $this->try_parse('http://aaaa![bbb]ccc', '

http://aaaa[bbb]ccc

'); + $this->try_parse('!http://aaaa![bbb]ccc', '

http://aaaa[bbb]ccc

'); + } + + public function testQuotedLink() { + $this->try_parse('["test page__ name"]', '

test page__ name

'); + $this->try_parse('["a b c" label]', '

label

'); + $this->try_parse('[wiki:"a b c" label]', '

label

'); + $this->try_parse('wiki:",,a b c__"', '

wiki:",,a b c__"

'); + } + + public function testBracket() { + $this->try_parse('[http://www label]', '

label

'); + $this->try_parse('[WikiName label]', '

label

'); + $this->try_parse('[normal text]', '

text

'); + } + + public function testParseFalseLink() { + $this->try_parse("[0]", '

0

'); + $this->try_parse('["0"]', '

0

'); + $this->try_parse("[0 text]", '

text

'); + $this->try_parse("[0#0]", '

0#0

'); + $this->try_parse('["0#0" text]', '

text

'); + $this->try_parse("wiki:0", '

wiki:0

'); + $this->try_parse("wiki:0#0", '

wiki:0#0

'); + $this->try_parse("[#0]", '

#0

'); + $this->try_parse("[#0 text]", '

text

'); + } + + public function testTableAndHr() { + $this->try_parse(' +---- +||table|| +---- +||table|| +---- +', + '
table

table

'); + } + + + public function testBlock() { + $this->try_parse("{{{ +moge +}}} +", '
moge
+
'); + $this->try_parse("{{{ +moge +", '
moge
+
'); + } + + public function testInlinePreformatted() { + $this->try_parse("''ab''c''d{{{ef g''}} }}}h''}}}i''j.", + "

abcdef g''}} h}}}ij.

"); + } + + public function testInternalLink() { + $this->try_parse("[http://www.yahoo.co.jp/ external]", + '

external

'); + $this->try_parse("[http://sourceforge.jp/ internal]", + '

internal

'); + $this->try_parse("[http://sourceforge.jp/projects/test/moge internal]", + '

internal

'); + $this->try_parse("[http://test.sourceforge.jp/ external]", + '

external

'); + } + + public function testHeadingCounter() { + $this->try_parse(" +== a +== b +== a +== a +", + '

a

b

a

a

'); + } + + public function testUselessParagraph() { + $po_out = '
Outline
'; + $this->try_parse("[[PageOutline]] + +a + +[[PageOutline]] + +b +", + "$po_out +

a +

$po_out +

b +

"); + + $this->try_parse(" + +{{{ +pre +}}} + + +", + '
pre
+
'); + + $this->try_parse("a + +{{{ html +html block +}}} + +b +", + '

a +

html block +

b +

'); + + } + + public function testTableAndHTMLBlockBug() { + $this->try_parse('||table|| +{{{ html +HTML +}}}', '
table
HTML +'); + } + + public function testSlashLinks() { + $this->try_parse('[/path/to/]', '

/path/to/

'); + $this->try_parse('[/path/to/ moge]', '

moge

'); + $this->try_parse('[//server.com/path moge]', '

moge

'); + } + + public function testTicketComment() { + $this->try_parse('comment:4:15142:1235097254', + '

comment:4:15142:1235097254

'); + $this->try_parse('comment:1 comment:foo', '

comment:1 comment:foo

'); + $this->try_parse('[comment:4:15142:1235097254 hiromichi-m] への返信', + '

hiromichi-m への返信

'); + $this->try_parse('[comment::123:456 グループID省略]なコメント', '

グループID省略なコメント

'); + $this->try_parse('[comment::123:456 グループID0]なコメントリンク', '

グループID0なコメントリンク

'); + $this->try_parse('[comment:123:456 2引数]なコメントリンク', '

2引数なコメントリンク

'); + } + + 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 ひかりはたもち、その電燈は失はれ", "

$iri ひかりはたもち、その電燈は失はれ

"); + $this->try_parse("[$iri]", "

$iri

"); + $this->try_parse("[$iri ひかりはたもち、その電燈は失はれ]", "

ひかりはたもち、その電燈は失はれ

"); + $this->try_parse('[/納豆.html ひきわり]', '

ひきわり

'); + } + + public function testSVNRev() { + $a_attr = 'href="http://svn/view?view=rev&root=unittest&revision=123" class="svn"'; + $this->try_parse('r123', "

r123

"); + $this->try_parse('abcr123', '

abcr123

'); + $this->try_parse('r123abc', '

r123abc

'); + $this->try_parse('lead r123 trail', "

lead r123 trail

"); + $this->try_parse('日本語r123テキスト', "

日本語r123テキスト

"); + } + + + 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 = '

SourceForge.JPシステムのバグを見つけた場合には バグ、 +CVSリポジトリの調整やアカウント削除等のサポート要求は サポートリクエストへ、システムへの追加機能の要望等は 機能リクエストへ登録いただくようお願いいたします。その他の問い合わせについては、連絡先についての文書をよくお読みください。

'; + $this->try_parse($input, $exp); + } + + public function testTracKeepNewLineMode() { + $this->p->setContext(array('trac.keep_newline' => true)); + $this->try_parse(' +改行を +全部 +br に +マップする +モード + + + + +パラグラフは適切に分割される必要がある + +> block +> quote +> text +さて +', '

改行を +
全部 +
br に +
マップする +
モード +

パラグラフは適切に分割される必要がある +

block +
quote +
text +

さて +

'); + $this->try_parse(' + * リストでも改行保持 + のテスト + * です +', ''); + } + + public function testInlinePluginAtStartOfLine() { + $this->try_parse('[[br]]test!', '


test!

'); + } + + public function testHashTrackerLink() { + $this->try_parse('#10', '

#10

'); + $this->try_parse('#50010', '

#50010

'); + } + + + 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('

CamelCase

', $p->parse('!CamelCase')); + } + + + protected function try_parse($text, $expect) { + self::assertEquals($expect, $this->p->parse($text)); + } + +} + +// vim: set sts=4 sw=4 expandtab: diff --git a/test/ParserTest.php b/test/ParserTest.php new file mode 100644 index 0000000..fde38ed --- /dev/null +++ b/test/ParserTest.php @@ -0,0 +1,33 @@ + '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: diff --git a/test/StorageTest.php b/test/StorageTest.php new file mode 100644 index 0000000..5b48f5d --- /dev/null +++ b/test/StorageTest.php @@ -0,0 +1,58 @@ +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())); + } +} diff --git a/test/all.php b/test/all.php new file mode 100755 index 0000000..a93e083 --- /dev/null +++ b/test/all.php @@ -0,0 +1,34 @@ +#!/usr/bin/phpunit +addTestSuite($m[1]); + } + closedir($dh); + return $suite; + } + + protected function setUp() + { + spl_autoload_register(); + } + +} + diff --git a/test/lib/kses.php b/test/lib/kses.php new file mode 100644 index 0000000..9009f1a --- /dev/null +++ b/test/lib/kses.php @@ -0,0 +1,628 @@ + +# * 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 ) or not (