2 // PukiWiki - Yet another WikiWikiWeb clone.
5 // 2003-2020 PukiWiki Development Team
6 // 2001-2002 Originally written by yu-ji
7 // License: GPL v2 or (at your option) any later version
9 // Hyperlink-related functions
11 // To get page exists or filetimes without accessing filesystem
12 // Type: array (page => filetime)
13 $_cached_page_filetime = null;
15 // Get filetime from cache
16 function fast_get_filetime($page)
18 global $_cached_page_filetime;
19 if (is_null($_cached_page_filetime)) {
20 return get_filetime($page);
22 if (isset($_cached_page_filetime[$page])) {
23 return $_cached_page_filetime[$page];
25 return get_filetime($page);
28 // Hyperlink decoration
29 function make_link($string, $page = '')
34 if (! isset($converter)) $converter = new InlineConverter();
36 $clone = $converter->get_clone($converter);
38 return $clone->convert($string, ($page != '') ? $page : $vars['page']);
41 // Converters of inline element
44 var $converters; // as array()
49 function get_clone($obj) {
51 if (! isset($clone_exists)) {
52 if (version_compare(PHP_VERSION, '5.0.0', '<')) {
53 $clone_exists = false;
65 $converters = array();
66 foreach ($this->converters as $key=>$converter) {
67 $converters[$key] = $this->get_clone($converter);
69 $this->converters = $converters;
72 function InlineConverter($converters = NULL, $excludes = NULL)
74 $this->__construct($converters, $excludes);
76 function __construct($converters = NULL, $excludes = NULL)
78 if ($converters === NULL) {
80 'plugin', // Inline plugins
83 'url_interwiki', // URLs (interwiki definition)
84 'mailto', // mailto: URL schemes
85 'interwikiname', // InterWikiNames
86 'autoalias', // AutoAlias
87 'autolink', // AutoLinks
88 'bracketname', // BracketNames
89 'wikiname', // WikiNames
90 'autoalias_a', // AutoAlias(alphabet)
91 'autolink_a', // AutoLinks(alphabet)
95 if ($excludes !== NULL)
96 $converters = array_diff($converters, $excludes);
98 $this->converters = $patterns = array();
101 foreach ($converters as $name) {
102 $classname = 'Link_' . $name;
103 $converter = new $classname($start);
104 $pattern = $converter->get_pattern();
105 if ($pattern === FALSE) continue;
107 $patterns[] = '(' . "\n" . $pattern . "\n" . ')';
108 $this->converters[$start] = $converter;
109 $start += $converter->get_count();
112 $this->pattern = join('|', $patterns);
115 function convert($string, $page)
118 $this->result = array();
120 $string = preg_replace_callback('/' . $this->pattern . '/x' . get_preg_u(),
121 array(& $this, 'replace'), $string);
123 $arr = explode("\x08", make_line_rules(htmlsc($string)));
125 while (! empty($arr)) {
126 $retval .= array_shift($arr) . array_shift($this->result);
131 function replace($arr)
133 $obj = $this->get_converter($arr);
135 $this->result[] = ($obj !== NULL && $obj->set($arr, $this->page) !== FALSE) ?
136 $obj->toString() : make_line_rules(htmlsc($arr[0]));
138 return "\x08"; // Add a mark into latest processed part
141 function get_objects($string, $page)
143 $matches = $arr = array();
144 preg_match_all('/' . $this->pattern . '/x' . get_preg_u(),
145 $string, $matches, PREG_SET_ORDER);
146 foreach ($matches as $match) {
147 $obj = $this->get_converter($match);
148 if ($obj->set($match, $page) !== FALSE) {
149 $arr[] = $this->get_clone($obj);
150 if ($obj->body != '')
151 $arr = array_merge($arr, $this->get_objects($obj->body, $page));
157 function & get_converter(& $arr)
159 foreach (array_keys($this->converters) as $start) {
160 if ($arr[$start] == $arr[0])
161 return $this->converters[$start];
167 // Base class of inline elements
170 var $start; // Origin number of parentheses (0 origin)
171 var $text; // Matched string
179 function Link($start)
181 $this->__construct($start);
183 function __construct($start)
185 $this->start = $start;
188 // Return a regex pattern to match
189 function get_pattern() {}
191 // Return number of parentheses (except (?:...) )
192 function get_count() {}
194 // Set pattern that matches
195 function set($arr, $page) {}
197 function toString() {}
199 // Private: Get needed parts from a matched array()
200 function splice($arr)
202 $count = $this->get_count() + 1;
203 $arr = array_pad(array_splice($arr, $this->start, $count), $count, '');
204 $this->text = $arr[0];
208 // Set basic parameters
209 function setParam($page, $name, $body, $type = '', $alias = '')
211 static $converter = NULL;
217 if (! PKWK_DISABLE_INLINE_IMAGE_FROM_URI &&
218 is_url($alias) && preg_match('/\.(gif|png|jpe?g)$/i', $alias)) {
219 $alias = '<img src="' . htmlsc($alias) . '" alt="' . $name . '" />';
220 } else if ($alias != '') {
221 if ($converter === NULL)
222 $converter = new InlineConverter(array('plugin'));
224 $alias = make_line_rules($converter->convert($alias, $page));
226 // BugTrack/669: A hack removing anchor tags added by AutoLink
227 $alias = preg_replace('#</?a[^>]*>#i', '', $alias);
229 $this->alias = $alias;
236 class Link_plugin extends Link
241 function Link_plugin($start)
243 $this->__construct($start);
245 function __construct($start)
247 parent::__construct($start);
250 function get_pattern()
252 $this->pattern = <<<EOD
255 (\w+) # (2) plugin name
258 ((?:(?!\)[;{]).)*) # (3) parameter
267 ((?:(?R)|(?!};).)*) # (4) body
279 function set($arr, $page)
281 list($all, $this->plain, $name, $this->param, $body) = $this->splice($arr);
283 // Re-get true plugin name and patameters (for PHP 4.1.2)
285 if (preg_match('/^' . $this->pattern . '/x' . get_preg_u(), $all, $matches)
286 && $matches[1] != $this->plain)
287 list(, $this->plain, $name, $this->param) = $matches;
289 return parent::setParam($page, $name, $body, 'plugin');
294 $body = ($this->body == '') ? '' : make_link($this->body);
297 // Try to call the plugin
298 if (exist_plugin_inline($this->name))
299 $str = do_plugin_inline($this->name, $this->param, $body);
301 if ($str !== FALSE) {
302 return $str; // Succeed
304 // No such plugin, or Failed
305 $body = (($body == '') ? '' : '{' . $body . '}') . ';';
306 return make_line_rules(htmlsc('&' . $this->plain) . $body);
312 class Link_note extends Link
314 function Link_note($start)
316 $this->__construct($start);
318 function __construct($start)
320 parent::__construct($start);
323 function get_pattern()
327 ((?>(?=\(\()(?R)|(?!\)\)).)*) # (1) note body
337 function set($arr, $page)
339 global $foot_explain, $vars;
342 list(, $body) = $this->splice($arr);
344 if (PKWK_ALLOW_RELATIVE_FOOTNOTE_ANCHOR) {
347 $script = get_page_uri($page);
350 $note = make_link($body);
353 $foot_explain[$id] = '<a id="notefoot_' . $id . '" href="' .
354 $script . '#notetext_' . $id . '" class="note_super">*' .
355 $id . '</a>' . "\n" .
356 '<span class="small">' . $note . '</span><br />';
358 // A hyperlink, content-body to footnote
359 if (! is_numeric(PKWK_FOOTNOTE_TITLE_MAX) || PKWK_FOOTNOTE_TITLE_MAX <= 0) {
362 $title = strip_tags($note);
363 $count = mb_strlen($title, SOURCE_ENCODING);
364 $title = mb_substr($title, 0, PKWK_FOOTNOTE_TITLE_MAX, SOURCE_ENCODING);
365 $abbr = (PKWK_FOOTNOTE_TITLE_MAX < $count) ? '...' : '';
366 $title = ' title="' . $title . $abbr . '"';
368 $name = '<a id="notetext_' . $id . '" href="' . $script .
369 '#notefoot_' . $id . '" class="note_super"' . $title .
372 return parent::setParam($page, $name, $body);
382 class Link_url extends Link
384 function Link_url($start)
386 $this->__construct($start);
388 function __construct($start)
390 parent::__construct($start);
393 function get_pattern()
395 $s1 = $this->start + 1;
397 ((?:\[\[))? # (1) open bracket
399 ((?:(?!\]\]).)+) # (3) alias name
403 (?:(?:https?|ftp|news):\/\/|mailto:)[\w\/\@\$()!?&%#:;.,~'=*+-]+
405 (?($s1)\]\]) # close bracket
414 function set($arr, $page)
416 list(, , , $alias, $name) = $this->splice($arr);
417 return parent::setParam($page, htmlsc($name),
418 '', 'url', $alias == '' ? $name : $alias);
426 $rel = ' rel="nofollow"';
428 return '<a href="' . $this->name . '"' . $rel . '>' . $this->alias . '</a>';
432 // URLs (InterWiki definition on "InterWikiName")
433 class Link_url_interwiki extends Link
435 function Link_url_interwiki($start)
437 $this->__construct($start);
439 function __construct($start)
441 parent::__construct($start);
444 function get_pattern()
449 (?:(?:https?|ftp|news):\/\/|\.\.?\/)[!~*'();\/?:\@&=+\$,%#\w.-]*
462 function set($arr, $page)
464 list(, $name, $alias) = $this->splice($arr);
465 return parent::setParam($page, htmlsc($name), '', 'url', $alias);
470 return '<a href="' . $this->name . '" rel="nofollow">' . $this->alias . '</a>';
474 // mailto: URL schemes
475 class Link_mailto extends Link
477 var $is_image, $image;
479 function Link_mailto($start)
481 $this->__construct($start);
483 function __construct($start)
485 parent::__construct($start);
488 function get_pattern()
490 $s1 = $this->start + 1;
494 ((?:(?!\]\]).)+)(?:>|:) # (1) alias
496 ([\w.-]+@[\w-]+\.[\w.-]+) # (2) mailto
497 (?($s1)\]\]) # close bracket if (1)
506 function set($arr, $page)
508 list(, $alias, $name) = $this->splice($arr);
509 return parent::setParam($page, $name, '', 'mailto', $alias == '' ? $name : $alias);
514 return '<a href="mailto:' . $this->name . '" rel="nofollow">' . $this->alias . '</a>';
518 // InterWikiName-rendered URLs
519 class Link_interwikiname extends Link
525 function Link_interwikiname($start)
527 $this->__construct($start);
529 function __construct($start)
531 parent::__construct($start);
534 function get_pattern()
536 $s2 = $this->start + 2;
537 $s5 = $this->start + 5;
541 ((?:(?!\]\]).)+)> # (1) alias
543 (\[\[)? # (2) open bracket
544 ((?:(?!\s|:|\]\]).)+) # (3) InterWiki
545 (?<! > | >\[\[ ) # not '>' or '>[['
548 (\[\[)? # (5) open bracket
550 (?($s5)\]\]) # close bracket if (5)
552 (?($s2)\]\]) # close bracket if (2)
562 function set($arr, $page)
564 list(, $alias, , $name, $this->param) = $this->splice($arr);
567 if (preg_match('/^([^#]+)(#[A-Za-z][\w-]*)$/', $this->param, $matches))
568 list(, $this->param, $this->anchor) = $matches;
570 $url = get_interwiki_url($name, $this->param);
571 $this->url = ($url === FALSE) ?
572 get_base_uri() . '?' . pagename_urlencode('[[' . $name . ':' . $this->param . ']]') :
575 return parent::setParam(
577 htmlsc($name . ':' . $this->param),
580 $alias == '' ? $name . ':' . $this->param : $alias
586 return '<a href="' . $this->url . $this->anchor . '" title="' .
587 $this->name . '" rel="nofollow">' . $this->alias . '</a>';
592 class Link_bracketname extends Link
596 function Link_bracketname($start)
598 $this->__construct($start);
600 function __construct($start)
602 parent::__construct($start);
605 function get_pattern()
607 global $WikiName, $BracketName;
609 $s2 = $this->start + 2;
612 (?:((?:(?!\]\]).)+)>)? # (1) Alias
613 (\[\[)? # (2) Open bracket
619 (\#(?:[a-zA-Z][\w-]*)?)? # (4) Anchor
620 (?($s2)\]\]) # Close bracket if (2)
630 function set($arr, $page)
634 list(, $alias, , $name, $this->anchor) = $this->splice($arr);
635 if ($name == '' && $this->anchor == '') return FALSE;
637 if ($name == '' || ! preg_match('/^' . $WikiName . '$/', $name)) {
638 if ($alias == '') $alias = $name . $this->anchor;
640 $name = get_fullname($name, $page);
641 if (! is_pagename($name)) return FALSE;
645 return parent::setParam($page, $name, '', 'pagename', $alias);
650 return make_pagelink(
660 class Link_wikiname extends Link
662 function Link_wikiname($start)
664 $this->__construct($start);
666 function __construct($start)
668 parent::__construct($start);
671 function get_pattern()
673 global $WikiName, $nowikiname;
675 return $nowikiname ? FALSE : '(' . $WikiName . ')';
683 function set($arr, $page)
685 list($name) = $this->splice($arr);
686 return parent::setParam($page, $name, '', 'pagename', $name);
691 return make_pagelink(
701 class Link_autolink extends Link
703 var $forceignorepages = array();
705 var $auto_a; // alphabet only
707 function Link_autolink($start)
709 $this->__construct($start);
711 function __construct($start)
715 parent::__construct($start);
717 if (! $autolink || ! file_exists(CACHE_DIR . 'autolink.dat'))
720 @list($auto, $auto_a, $forceignorepages) = file(CACHE_DIR . 'autolink.dat');
722 $this->auto_a = $auto_a;
723 $this->forceignorepages = explode("\t", trim($forceignorepages));
726 function get_pattern()
728 return isset($this->auto) ? '(' . $this->auto . ')' : FALSE;
736 function set($arr, $page)
740 list($name) = $this->splice($arr);
742 // Ignore pages listed, or Expire ones not found
743 if (in_array($name, $this->forceignorepages) || ! is_page($name))
746 return parent::setParam($page, $name, '', 'pagename', $name);
751 return make_pagelink($this->name, $this->alias, '', $this->page, TRUE);
755 class Link_autolink_a extends Link_autolink
757 function Link_autolink_a($start)
759 $this->__construct($start);
761 function __construct($start)
763 parent::__construct($start);
766 function get_pattern()
768 return isset($this->auto_a) ? '(' . $this->auto_a . ')' : FALSE;
773 class Link_autoalias extends Link
775 var $forceignorepages = array();
777 var $auto_a; // alphabet only
780 function Link_autoalias($start)
782 global $autoalias, $aliaspage;
784 parent::Link($start);
786 if (! $autoalias || ! file_exists(CACHE_DIR . PKWK_AUTOALIAS_REGEX_CACHE) || $this->page == $aliaspage)
791 @list($auto, $auto_a, $forceignorepages) = file(CACHE_DIR . PKWK_AUTOALIAS_REGEX_CACHE);
793 $this->auto_a = $auto_a;
794 $this->forceignorepages = explode("\t", trim($forceignorepages));
797 function get_pattern()
799 return isset($this->auto) ? '(' . $this->auto . ')' : FALSE;
805 function set($arr,$page)
807 list($name) = $this->splice($arr);
808 // Ignore pages listed
809 if (in_array($name, $this->forceignorepages) || get_autoalias_right_link($name) == '') {
812 return parent::setParam($page,$name,'','pagename',$name);
817 $this->alias = get_autoalias_right_link($this->name);
818 if ($this->alias != '') {
819 $link = '[[' . $this->name . '>' . $this->alias . ']]';
820 return make_link($link);
826 class Link_autoalias_a extends Link_autoalias
828 function Link_autoalias_a($start)
830 parent::Link_autoalias($start);
832 function get_pattern()
834 return isset($this->auto_a) ? '(' . $this->auto_a . ')' : FALSE;
838 // Make hyperlink for the page
839 function make_pagelink($page, $alias = '', $anchor = '', $refer = '', $isautolink = FALSE)
841 global $vars, $link_compact, $related, $_symbol_noexists;
843 $script = get_base_uri();
844 $s_page = htmlsc(strip_bracket($page));
845 $s_alias = ($alias == '') ? $s_page : $alias;
847 if ($page == '') return '<a href="' . $anchor . '">' . $s_alias . '</a>';
849 $r_page = pagename_urlencode($page);
850 $r_refer = ($refer == '') ? '' : '&refer=' . rawurlencode($refer);
852 $page_filetime = fast_get_filetime($page);
853 $is_page = $page_filetime !== 0;
854 if (! isset($related[$page]) && $page !== $vars['page'] && is_page)
855 $related[$page] = $page_filetime;
857 if ($isautolink || $is_page) {
858 // Hyperlink to the page
859 $attrs = get_filetime_a_attrs($page_filetime);
862 $al_left = '<!--autolink-->';
863 $al_right = '<!--/autolink-->';
865 $al_left = $al_right = '';
867 $title_attr_html = '';
868 if ($s_page !== $s_alias) {
869 $title_attr_html = ' title="' . $s_page . '"';
871 return $al_left . '<a ' . 'href="' . $script . '?' . $r_page . $anchor .
872 '"' . $title_attr_html . ' class="' .
873 $attrs['class'] . '" data-mtime="' . $attrs['data_mtime'] .
874 '">' . $s_alias . '</a>' . $al_right;
876 // Support Page redirection
877 $redirect_page = get_pagename_on_redirect($page);
878 if ($redirect_page !== false) {
879 return make_pagelink($redirect_page, $s_alias);
882 if (PKWK_READONLY) return $s_alias; // No dacorations
884 if ($_symbol_noexists !== '') {
885 $symbol_html = '<span style="user-select:none;">' .
886 htmlsc($_symbol_noexists) . '</span>';
888 $href = $script . '?cmd=edit&page=' . $r_page . $r_refer;
889 if ($link_compact && $_symbol_noexists != '') {
890 $retval = '<a href="' . $href . '">' . $_symbol_noexists . '</a>';
893 $retval = '<a href="' . $href . '">' . $s_alias . '</a>';
894 return '<span class="noexists">' . $retval . $symbol_html . '</span>';
899 // Resolve relative / (Unix-like)absolute path of the page
900 function get_fullname($name, $refer)
905 if ($name == '' || $name == './') return $refer;
908 if ($name{0} == '/') {
909 $name = substr($name, 1);
910 return ($name == '') ? $defaultpage : $name;
913 // Relative path from 'Here'
914 if (substr($name, 0, 2) == './') {
915 $arrn = preg_split('#/#', $name, -1, PREG_SPLIT_NO_EMPTY);
917 return join('/', $arrn);
920 // Relative path from dirname()
921 if (substr($name, 0, 3) == '../') {
922 $arrn = preg_split('#/#', $name, -1, PREG_SPLIT_NO_EMPTY);
923 $arrp = preg_split('#/#', $refer, -1, PREG_SPLIT_NO_EMPTY);
925 while (! empty($arrn) && $arrn[0] == '..') {
929 $name = ! empty($arrp) ? join('/', array_merge($arrp, $arrn)) :
930 (! empty($arrn) ? $defaultpage . '/' . join('/', $arrn) : $defaultpage);
936 // Render an InterWiki into a URL
937 function get_interwiki_url($name, $param)
939 global $WikiName, $interwiki;
940 static $interwikinames;
941 static $encode_aliases = array('sjis'=>'SJIS', 'euc'=>'EUC-JP', 'utf8'=>'UTF-8');
943 if (! isset($interwikinames)) {
944 $interwikinames = $matches = array();
945 foreach (get_source($interwiki) as $line)
946 if (preg_match('/\[(' . '(?:(?:https?|ftp|news):\/\/|\.\.?\/)' .
947 '[!~*\'();\/?:\@&=+\$,%#\w.-]*)\s([^\]]+)\]\s?([^\s]*)/',
949 $interwikinames[$matches[2]] = array($matches[1], $matches[3]);
952 if (! isset($interwikinames[$name])) return FALSE;
954 list($url, $opt) = $interwikinames[$name];
959 case '': /* FALLTHROUGH */
960 case 'std': // Simply URL-encode the string, whose base encoding is the internal-encoding
961 $param = rawurlencode($param);
964 case 'asis': /* FALLTHROUGH */
965 case 'raw' : // Truly as-is
968 case 'yw': // YukiWiki
969 if (! preg_match('/' . $WikiName . '/', $param))
970 $param = '[[' . mb_convert_encoding($param, 'SJIS', SOURCE_ENCODING) . ']]';
973 case 'moin': // MoinMoin
974 $param = str_replace('%', '_', rawurlencode($param));
978 // Alias conversion of $opt
979 if (isset($encode_aliases[$opt])) $opt = & $encode_aliases[$opt];
981 // Encoding conversion into specified encode, and URLencode
982 if (strpos($url, '$1') === FALSE && substr($url, -1) === '?') {
984 $param = pagename_urlencode(mb_convert_encoding($param, $opt, SOURCE_ENCODING));
986 $param = rawurlencode(mb_convert_encoding($param, $opt, SOURCE_ENCODING));
990 // Replace or Add the parameter
991 if (strpos($url, '$1') !== FALSE) {
992 $url = str_replace('$1', $param, $url);
998 if ($len > 512) die_message('InterWiki URL too long: ' . $len . ' characters');
1003 function get_autoticketlink_def_page()
1005 return 'AutoTicketLinkName';
1009 * Get AutoTicketLink - JIRA projects from AutoTiketLinkName page
1011 function get_ticketlink_jira_projects()
1013 $autoticketlink_def_page = get_autoticketlink_def_page();
1014 $active_jira_base_url = null;
1015 $jira_projects = array();
1016 foreach (get_source($autoticketlink_def_page) as $line) {
1017 if (substr($line, 0, 1) !== '-') {
1018 $active_jira_base_url = null;
1022 if (preg_match('/^-\s*(jira)\s+(https?:\/\/[!~*\'();\/?:\@&=+\$,%#\w.-]+)\s*$/', $line, $m)) {
1023 $active_jira_base_url = $m[2];
1024 } else if (preg_match('/^--\s*([A-Z][A-Z0-9]{1,10}(?:_[A-Z0-9]{1,10}){0,2})(\s+(.+?))?\s*$/', $line, $m)) {
1025 if ($active_jira_base_url) {
1026 $project_key = $m[1];
1028 array_push($jira_projects, array(
1031 'base_url' => $active_jira_base_url,
1035 $active_jira_base_url = null;
1038 return $jira_projects;
1041 function init_autoticketlink_def_page()
1043 $autoticketlink_def_page = get_autoticketlink_def_page();
1044 if (is_page($autoticketlink_def_page)) {
1049 * AutoTicketLink definition [#def]
1051 Reference: https://pukiwiki.osdn.jp/?AutoTicketLink
1053 - jira https://site1.example.com/jira/browse/
1054 -- AAA Project title \$1
1055 -- BBB Project title \$1
1056 - jira https://site2.example.com/jira/browse/
1057 -- PROJECTA Site2 \$1
1059 (Default definition) pukiwiki.ini.php
1060 $ticket_jira_default_site = array(
1061 'title' => 'My JIRA - \$1',
1062 'base_url' => 'https://issues.example.com/jira/browse/',
1065 page_write($autoticketlink_def_page, $body);
1068 function init_autoalias_def_page()
1070 global $aliaspage; // 'AutoAliasName'
1071 $autoticketlink_def_page = get_autoticketlink_def_page();
1072 if (is_page($aliaspage)) {
1077 *AutoAliasName [#qf9311bb]
1078 AutoAlias definition
1080 Reference: https://pukiwiki.osdn.jp/?AutoAlias
1082 * PukiWiki [#ee87d39e]
1083 -[[pukiwiki.official>https://pukiwiki.osdn.jp/]]
1084 -[[pukiwiki.dev>https://pukiwiki.osdn.jp/dev/]]
1086 page_write($aliaspage, $body);
1087 update_autoalias_cache_file();