OSDN Git Service

Better late than never. Added simple credits, workdays(years), and license notificati...
[pukiwiki/pukiwiki.git] / lib / make_link.php
1 <?php
2 // PukiWiki - Yet another WikiWikiWeb clone.
3 // $Id: make_link.php,v 1.19 2005/04/29 09:10:11 henoheno Exp $
4 // Copywrite (C) 2003-2005 PukiWiki Developers Team
5 // License: GPL v2 or (at your option) any later version
6 //
7 // Hyperlink-related functions
8
9 // Hyperlink decoration
10 function make_link($string, $page = '')
11 {
12         global $vars;
13         static $converter;
14
15         if (! isset($converter)) $converter = new InlineConverter();
16
17         $clone = $converter->get_clone($converter);
18
19         return $clone->convert($string, ($page != '') ? $page : $vars['page']);
20 }
21
22 // Converters of inline element
23 class InlineConverter
24 {
25         var $converters; // as array()
26         var $pattern;
27         var $pos;
28         var $result;
29
30         function get_clone($obj) {
31                 static $clone_func;
32
33                 if (! isset($clone_func)) {
34                         if (version_compare(PHP_VERSION, '5.0.0', '<')) {
35                                 $clone_func = create_function('$a', 'return $a;');
36                         } else {
37                                 $clone_func = create_function('$a', 'return clone $a;');
38                         }
39                 }
40                 return $clone_func($obj);
41         }
42
43         function __clone() {
44                 $converters = array();
45                 foreach ($this->converters as $key=>$converter) {
46                         $converters[$key] = $this->get_clone($converter);
47                 }
48                 $this->converters = $converters;
49         }
50
51         function InlineConverter($converters = NULL, $excludes = NULL)
52         {
53                 if ($converters === NULL) {
54                         $converters = array(
55                                 'plugin',        // Inline plugins
56                                 'note',          // Footnotes
57                                 'url',           // URLs
58                                 'url_interwiki', // URLs (interwiki definition)
59                                 'mailto',        // mailto: URL schemes
60                                 'interwikiname', // InterWikiNames
61                                 'autolink',      // AutoLinks
62                                 'bracketname',   // BracketNames
63                                 'wikiname',      // WikiNames
64                                 'autolink_a',    // AutoLinks(alphabet)
65                         );
66                 }
67
68                 if ($excludes !== NULL)
69                         $converters = array_diff($converters, $excludes);
70
71                 $this->converters = $patterns = array();
72                 $start = 1;
73
74                 foreach ($converters as $name) {
75                         $classname = 'Link_' . $name;
76                         $converter = new $classname($start);
77                         $pattern   = $converter->get_pattern();
78                         if ($pattern === FALSE) continue;
79
80                         $patterns[] = '(' . "\n" . $pattern . "\n" . ')';
81                         $this->converters[$start] = $converter;
82                         $start += $converter->get_count();
83                         ++$start;
84                 }
85                 $this->pattern = join('|', $patterns);
86         }
87
88         function convert($string, $page)
89         {
90                 $this->page   = $page;
91                 $this->result = array();
92
93                 $string = preg_replace_callback('/' . $this->pattern . '/x',
94                         array(& $this, 'replace'), $string);
95
96                 $arr = explode("\x08", make_line_rules(htmlspecialchars($string)));
97                 $retval = '';
98                 while (! empty($arr)) {
99                         $retval .= array_shift($arr) . array_shift($this->result);
100                 }
101                 return $retval;
102         }
103
104         function replace($arr)
105         {
106                 $obj = $this->get_converter($arr);
107
108                 $this->result[] = ($obj !== NULL && $obj->set($arr, $this->page) !== FALSE) ?
109                         $obj->toString() : make_line_rules(htmlspecialchars($arr[0]));
110
111                 return "\x08"; // Add a mark into latest processed part
112         }
113
114         function get_objects($string, $page)
115         {
116                 $matches = $arr = array();
117                 preg_match_all('/' . $this->pattern . '/x', $string, $matches, PREG_SET_ORDER);
118                 foreach ($matches as $match) {
119                         $obj = $this->get_converter($match);
120                         if ($obj->set($match, $page) !== FALSE) {
121                                 $arr[] = $this->get_clone($obj);
122                                 if ($obj->body != '')
123                                         $arr = array_merge($arr, $this->get_objects($obj->body, $page));
124                         }
125                 }
126                 return $arr;
127         }
128
129         function & get_converter(& $arr)
130         {
131                 foreach (array_keys($this->converters) as $start) {
132                         if ($arr[$start] == $arr[0])
133                                 return $this->converters[$start];
134                 }
135                 return NULL;
136         }
137 }
138
139 // Base class of inline elements
140 class Link
141 {
142         var $start;   // Origin number of parentheses (0 origin)
143         var $text;    // Matched string
144
145         var $type;
146         var $page;
147         var $name;
148         var $body;
149         var $alias;
150
151         // Constructor
152         function Link($start)
153         {
154                 $this->start = $start;
155         }
156
157         // Return a regex pattern to match
158         function get_pattern() {}
159
160         // Return number of parentheses (except (?:...) )
161         function get_count() {}
162
163         // Set pattern that matches
164         function set($arr, $page) {}
165
166         function toString() {}
167
168         // Private: Get needed parts from a matched array()
169         function splice($arr) {
170                 $count = $this->get_count() + 1;
171                 $arr   = array_pad(array_splice($arr, $this->start, $count), $count, '');
172                 $this->text = $arr[0];
173                 return $arr;
174         }
175
176         // Set basic parameters
177         function setParam($page, $name, $body, $type = '', $alias = '')
178         {
179                 static $converter = NULL;
180
181                 $this->page = $page;
182                 $this->name = $name;
183                 $this->body = $body;
184                 $this->type = $type;
185                 if (is_url($alias) && preg_match('/\.(gif|png|jpe?g)$/i', $alias)) {
186                         $alias = htmlspecialchars($alias);
187                         $alias = '<img src="' . $alias . '" alt="' . $name . '" />';
188                 } else if ($alias != '') {
189                         if ($converter === NULL)
190                                 $converter = new InlineConverter(array('plugin'));
191
192                         $alias = make_line_rules($converter->convert($alias, $page));
193
194                         // BugTrack/669: A hack removing anchor tags added by AutoLink
195                         $alias = preg_replace('#</?a[^>]*>#i', '', $alias);
196                 }
197                 $this->alias = $alias;
198
199                 return TRUE;
200         }
201 }
202
203 // Inline plugins
204 class Link_plugin extends Link
205 {
206         var $pattern;
207         var $plain,$param;
208
209         function Link_plugin($start)
210         {
211                 parent::Link($start);
212         }
213
214         function get_pattern()
215         {
216                 $this->pattern = <<<EOD
217 &
218 (      # (1) plain
219  (\w+) # (2) plugin name
220  (?:
221   \(
222    ((?:(?!\)[;{]).)*) # (3) parameter
223   \)
224  )?
225 )
226 EOD;
227                 return <<<EOD
228 {$this->pattern}
229 (?:
230  \{
231   ((?:(?R)|(?!};).)*) # (4) body
232  \}
233 )?
234 ;
235 EOD;
236         }
237
238         function get_count()
239         {
240                 return 4;
241         }
242
243         function set($arr, $page)
244         {
245                 list($all, $this->plain, $name, $this->param, $body) = $this->splice($arr);
246
247                 // Re-get true plugin name and patameters (for PHP 4.1.2)
248                 $matches = array();
249                 if (preg_match('/^' . $this->pattern . '/x', $all, $matches)
250                         && $matches[1] != $this->plain) 
251                         list(, $this->plain, $name, $this->param) = $matches;
252
253                 return parent::setParam($page, $name, $body, 'plugin');
254         }
255
256         function toString()
257         {
258                 $body = ($this->body == '') ? '' : make_link($this->body);
259                 $str = FALSE;
260
261                 // Try to call the plugin
262                 if (exist_plugin_inline($this->name))
263                         $str = do_plugin_inline($this->name, $this->param, $body);
264
265                 if ($str !== FALSE) {
266                         return $str; // Succeed
267                 } else {
268                         // No such plugin, or Failed
269                         $body = (($body == '') ? '' : '{' . $body . '}') . ';';
270                         return make_line_rules(htmlspecialchars('&' . $this->plain) . $body);
271                 }
272         }
273 }
274
275 // Footnotes
276 class Link_note extends Link
277 {
278         function Link_note($start)
279         {
280                 parent::Link($start);
281         }
282
283         function get_pattern()
284         {
285                 return <<<EOD
286 \(\(
287  ((?:(?R)|(?!\)\)).)*) # (1) note body
288 \)\)
289 EOD;
290         }
291
292         function get_count()
293         {
294                 return 1;
295         }
296
297         function set($arr, $page)
298         {
299                 global $foot_explain, $vars;
300                 static $note_id = 0;
301
302                 list(, $body) = $this->splice($arr);
303
304                 $script = '';
305                 if (! PKWK_ALLOW_RELATIVE_FOOTNOTE_ANCHOR)
306                         $script = get_script_uri() . '?' . $page;
307
308                 $id   = ++$note_id;
309                 $note = make_link($body);
310                 $page = isset($vars['page']) ? rawurlencode($vars['page']) : '';
311
312                 // Footnote
313                 $foot_explain[$id] = '<a id="notefoot_' . $id . '" href="' .
314                         $script . '#notetext_' . $id . '" class="note_super">*' .
315                         $id . '</a>' . "\n" .
316                         '<span class="small">' . $note . '</span><br />';
317
318                 // A hyperlink, content-body to footnote
319                 $name = '<a id="notetext_' . $id . '" href="' . $script .
320                         '#notefoot_' . $id . '" class="note_super" title="' .
321                         htmlspecialchars(strip_tags($note)) . '">*' . $id . '</a>';
322
323                 return parent::setParam($page, $name, $body);
324         }
325
326         function toString()
327         {
328                 return $this->name;
329         }
330 }
331
332 // URLs
333 class Link_url extends Link
334 {
335         function Link_url($start)
336         {
337                 parent::Link($start);
338         }
339
340         function get_pattern()
341         {
342                 $s1 = $this->start + 1;
343                 return <<<EOD
344 (\[\[             # (1) open bracket
345  ((?:(?!\]\]).)+) # (2) alias
346  (?:>|:)
347 )?
348 (                 # (3) url
349  (?:(?:https?|ftp|news):\/\/|mailto:)[\w\/\@\$()!?&%#:;.,~'=*+-]+
350 )
351 (?($s1)\]\])      # close bracket
352 EOD;
353         }
354
355         function get_count()
356         {
357                 return 3;
358         }
359
360         function set($arr, $page)
361         {
362                 list(, , $alias, $name) = $this->splice($arr);
363                 return parent::setParam($page, htmlspecialchars($name),
364                         '', 'url', $alias == '' ? $name : $alias);
365         }
366
367         function toString()
368         {
369                 if (FALSE) {
370                         $rel = '';
371                 } else {
372                         $rel = ' rel="nofollow"';
373                 }
374                 return '<a href="' . $this->name . '"' . $rel . '>' . $this->alias . '</a>';
375         }
376 }
377
378 // URLs (InterWiki definition on "InterWikiName")
379 class Link_url_interwiki extends Link
380 {
381         function Link_url_interwiki($start)
382         {
383                 parent::Link($start);
384         }
385
386         function get_pattern()
387         {
388                 return <<<EOD
389 \[       # open bracket
390 (        # (1) url
391  (?:(?:https?|ftp|news):\/\/|\.\.?\/)[!~*'();\/?:\@&=+\$,%#\w.-]*
392 )
393 \s
394 ([^\]]+) # (2) alias
395 \]       # close bracket
396 EOD;
397         }
398
399         function get_count()
400         {
401                 return 2;
402         }
403
404         function set($arr, $page)
405         {
406                 list(, $name, $alias) = $this->splice($arr);
407                 return parent::setParam($page, htmlspecialchars($name), '', 'url', $alias);
408         }
409
410         function toString()
411         {
412                 return '<a href="' . $this->name . '" rel="nofollow">' . $this->alias . '</a>';
413         }
414 }
415
416 // mailto: URL schemes
417 class Link_mailto extends Link
418 {
419         var $is_image, $image;
420
421         function Link_mailto($start)
422         {
423                 parent::Link($start);
424         }
425
426         function get_pattern()
427         {
428                 $s1 = $this->start + 1;
429                 return <<<EOD
430 (?:
431  \[\[
432  ((?:(?!\]\]).)+)(?:>|:)  # (1) alias
433 )?
434 ([\w.-]+@[\w-]+\.[\w.-]+) # (2) mailto
435 (?($s1)\]\])              # close bracket if (1)
436 EOD;
437         }
438
439         function get_count()
440         {
441                 return 2;
442         }
443
444         function set($arr, $page)
445         {
446                 list(, $alias, $name) = $this->splice($arr);
447                 return parent::setParam($page, $name, '', 'mailto', $alias == '' ? $name : $alias);
448         }
449         
450         function toString()
451         {
452                 return '<a href="mailto:' . $this->name . '" rel="nofollow">' . $this->alias . '</a>';
453         }
454 }
455
456 // InterWikiName-rendered URLs
457 class Link_interwikiname extends Link
458 {
459         var $url    = '';
460         var $param  = '';
461         var $anchor = '';
462
463         function Link_interwikiname($start)
464         {
465                 parent::Link($start);
466         }
467
468         function get_pattern()
469         {
470                 $s2 = $this->start + 2;
471                 $s5 = $this->start + 5;
472                 return <<<EOD
473 \[\[                  # open bracket
474 (?:
475  ((?:(?!\]\]).)+)>    # (1) alias
476 )?
477 (\[\[)?               # (2) open bracket
478 ((?:(?!\s|:|\]\]).)+) # (3) InterWiki
479 (?<! > | >\[\[ )      # not '>' or '>[['
480 :                     # separator
481 (                     # (4) param
482  (\[\[)?              # (5) open bracket
483  (?:(?!>|\]\]).)+
484  (?($s5)\]\])         # close bracket if (5)
485 )
486 (?($s2)\]\])          # close bracket if (2)
487 \]\]                  # close bracket
488 EOD;
489         }
490
491         function get_count()
492         {
493                 return 5;
494         }
495
496         function set($arr, $page)
497         {
498                 global $script;
499
500                 list(, $alias, , $name, $this->param) = $this->splice($arr);
501
502                 $matches = array();
503                 if (preg_match('/^([^#]+)(#[A-Za-z][\w-]*)$/', $this->param, $matches))
504                         list(, $this->param, $this->anchor) = $matches;
505
506                 $url = get_interwiki_url($name, $this->param);
507                 $this->url = ($url === FALSE) ?
508                         $script . '?' . rawurlencode('[[' . $name . ':' . $this->param . ']]') :
509                         htmlspecialchars($url);
510
511                 return parent::setParam(
512                         $page,
513                         htmlspecialchars($name . ':' . $this->param),
514                         '',
515                         'InterWikiName',
516                         $alias == '' ? $name . ':' . $this->param : $alias
517                 );
518         }
519
520         function toString()
521         {
522                 return '<a href="' . $this->url . $this->anchor . '" title="' .
523                         $this->name . '" rel="nofollow">' . $this->alias . '</a>';
524         }
525 }
526
527 // BracketNames
528 class Link_bracketname extends Link
529 {
530         var $anchor, $refer;
531
532         function Link_bracketname($start)
533         {
534                 parent::Link($start);
535         }
536
537         function get_pattern()
538         {
539                 global $WikiName, $BracketName;
540
541                 $s2 = $this->start + 2;
542                 return <<<EOD
543 \[\[                     # Open bracket
544 (?:((?:(?!\]\]).)+)>)?   # (1) Alias
545 (\[\[)?                  # (2) Open bracket
546 (                        # (3) PageName
547  (?:$WikiName)
548  |
549  (?:$BracketName)
550 )?
551 (\#(?:[a-zA-Z][\w-]*)?)? # (4) Anchor
552 (?($s2)\]\])             # Close bracket if (2)
553 \]\]                     # Close bracket
554 EOD;
555         }
556
557         function get_count()
558         {
559                 return 4;
560         }
561
562         function set($arr, $page)
563         {
564                 global $WikiName;
565
566                 list(, $alias, , $name, $this->anchor) = $this->splice($arr);
567                 if ($name == '' && $this->anchor == '') return FALSE;
568
569                 if ($name == '' || ! preg_match('/^' . $WikiName . '$/', $name)) {
570                         if ($alias == '') $alias = $name . $this->anchor;
571                         if ($name != '') {
572                                 $name = get_fullname($name, $page);
573                                 if (! is_pagename($name)) return FALSE;
574                         }
575                 }
576
577                 return parent::setParam($page, $name, '', 'pagename', $alias);
578         }
579
580         function toString()
581         {
582                 return make_pagelink(
583                         $this->name,
584                         $this->alias,
585                         $this->anchor,
586                         $this->page
587                 );
588         }
589 }
590
591 // WikiNames
592 class Link_wikiname extends Link
593 {
594         function Link_wikiname($start)
595         {
596                 parent::Link($start);
597         }
598
599         function get_pattern()
600         {
601                 global $WikiName, $nowikiname;
602
603                 return $nowikiname ? FALSE : '(' . $WikiName . ')';
604         }
605
606         function get_count()
607         {
608                 return 1;
609         }
610
611         function set($arr, $page)
612         {
613                 list($name) = $this->splice($arr);
614                 return parent::setParam($page, $name, '', 'pagename', $name);
615         }
616
617         function toString()
618         {
619                 return make_pagelink(
620                         $this->name,
621                         $this->alias,
622                         '',
623                         $this->page
624                 );
625         }
626 }
627
628 // AutoLinks
629 class Link_autolink extends Link
630 {
631         var $forceignorepages = array();
632         var $auto;
633         var $auto_a; // alphabet only
634
635         function Link_autolink($start)
636         {
637                 global $autolink;
638
639                 parent::Link($start);
640
641                 if (! $autolink || ! file_exists(CACHE_DIR . 'autolink.dat'))
642                         return;
643
644                 @list($auto, $auto_a, $forceignorepages) = file(CACHE_DIR . 'autolink.dat');
645                 $this->auto   = $auto;
646                 $this->auto_a = $auto_a;
647                 $this->forceignorepages = explode("\t", trim($forceignorepages));
648         }
649
650         function get_pattern()
651         {
652                 return isset($this->auto) ? '(' . $this->auto . ')' : FALSE;
653         }
654
655         function get_count()
656         {
657                 return 1;
658         }
659
660         function set($arr, $page)
661         {
662                 global $WikiName;
663
664                 list($name) = $this->splice($arr);
665
666                 // Ignore pages listed, or Expire ones not found
667                 if (in_array($name, $this->forceignorepages) || ! is_page($name))
668                         return FALSE;
669
670                 return parent::setParam($page, $name, '', 'pagename', $name);
671         }
672
673         function toString()
674         {
675                 return make_pagelink($this->name, $this->alias, '', $this->page);
676         }
677 }
678
679 class Link_autolink_a extends Link_autolink
680 {
681         function Link_autolink_a($start)
682         {
683                 parent::Link_autolink($start);
684         }
685
686         function get_pattern()
687         {
688                 return isset($this->auto_a) ? '(' . $this->auto_a . ')' : FALSE;
689         }
690 }
691
692 // Make hyperlink for the page
693 function make_pagelink($page, $alias = '', $anchor = '', $refer = '')
694 {
695         global $script, $vars, $link_compact, $related, $_symbol_noexists;
696
697         $s_page = htmlspecialchars(strip_bracket($page));
698         $s_alias = ($alias == '') ? $s_page : $alias;
699
700         if ($page == '') return '<a href="' . $anchor . '">' . $s_alias . '</a>';
701
702         $r_page  = rawurlencode($page);
703         $r_refer = ($refer == '') ? '' : '&amp;refer=' . rawurlencode($refer);
704
705         if (! isset($related[$page]) && $page != $vars['page'] && is_page($page))
706                 $related[$page] = get_filetime($page);
707
708         if (is_page($page)) {
709                 // Hyperlinks
710                 $passage = get_pg_passage($page, FALSE);
711                 $title   = $link_compact ? '' : ' title="' . $s_page . $passage . '"';
712                 return '<a href="' . $script . '?' . $r_page . $anchor . '"' . $title . '>' .
713                         $s_alias . '</a>';
714         } else if (PKWK_READONLY) {
715                 // Without hyperlink (= Suppress dangling link)
716                 return $s_alias;
717         } else {
718                 // Dangling links
719                 $retval = $s_alias . '<a href="' .
720                         $script . '?cmd=edit&amp;page=' . $r_page . $r_refer . '">' .
721                         $_symbol_noexists . '</a>';
722                 if (! $link_compact)
723                         $retval = '<span class="noexists">' . $retval . '</span>';
724                 return $retval;
725         }
726 }
727
728 // Resolve relative / (Unix-like)absolute path of the page
729 function get_fullname($name, $refer)
730 {
731         global $defaultpage;
732
733         // 'Here'
734         if ($name == '' || $name == './') return $refer;
735
736         // Absolute path
737         if ($name{0} == '/') {
738                 $name = substr($name, 1);
739                 return ($name == '') ? $defaultpage : $name;
740         }
741
742         // Relative path from 'Here'
743         if (substr($name, 0, 2) == './') {
744                 $arrn    = preg_split('#/#', $name, -1, PREG_SPLIT_NO_EMPTY);
745                 $arrn[0] = $refer;
746                 return join('/', $arrn);
747         }
748
749         // Relative path from dirname()
750         if (substr($name, 0, 3) == '../') {
751                 $arrn = preg_split('#/#', $name,  -1, PREG_SPLIT_NO_EMPTY);
752                 $arrp = preg_split('#/#', $refer, -1, PREG_SPLIT_NO_EMPTY);
753
754                 while (! empty($arrn) && $arrn[0] == '..') {
755                         array_shift($arrn);
756                         array_pop($arrp);
757                 }
758                 $name = ! empty($arrp) ? join('/', array_merge($arrp, $arrn)) :
759                         (! empty($arrn) ? $defaultpage . '/' . join('/', $arrn) : $defaultpage);
760         }
761
762         return $name;
763 }
764
765 // Render an InterWiki into a URL
766 function get_interwiki_url($name, $param)
767 {
768         global $WikiName, $interwiki;
769         static $interwikinames;
770         static $encode_aliases = array('sjis'=>'SJIS', 'euc'=>'EUC-JP', 'utf8'=>'UTF-8');
771
772         if (! isset($interwikinames)) {
773                 $interwikinames = $matches = array();
774                 foreach (get_source($interwiki) as $line)
775                         if (preg_match('/\[(' . '(?:(?:https?|ftp|news):\/\/|\.\.?\/)' .
776                             '[!~*\'();\/?:\@&=+\$,%#\w.-]*)\s([^\]]+)\]\s?([^\s]*)/',
777                             $line, $matches))
778                                 $interwikinames[$matches[2]] = array($matches[1], $matches[3]);
779         }
780
781         if (! isset($interwikinames[$name])) return FALSE;
782
783         list($url, $opt) = $interwikinames[$name];
784
785         // Encoding
786         switch ($opt) {
787
788         case '':
789         case 'std': // As-Is (Internal encoding of this PukiWiki will be used)
790                 $param = rawurlencode($param);
791                 break;
792
793         case 'asis': // As-Is
794         case 'raw':
795                 // $param = htmlspecialchars($param);
796                 break;
797
798         case 'yw': // YukiWiki
799                 if (! preg_match('/' . $WikiName . '/', $param))
800                         $param = '[[' . mb_convert_encoding($param, 'SJIS', SOURCE_ENCODING) . ']]';
801                 // $param = htmlspecialchars($param);
802                 break;
803
804         case 'moin': // MoinMoin
805                 $param = str_replace('%', '_', rawurlencode($param));
806                 break;
807
808         default:
809                 // Alias conversion
810                 if (isset($encode_aliases[$opt])) $opt = $encode_aliases[$opt];
811                 // Encoding conversion into specified encode, and URLencode
812                 $param = rawurlencode(mb_convert_encoding($param, $opt, 'auto'));
813         }
814
815         // Replace parameters
816         if (strpos($url, '$1') !== FALSE) {
817                 $url = str_replace('$1', $param, $url);
818         } else {
819                 $url .= $param;
820         }
821
822         $len = strlen($url);
823         if ($len > 512) die_message('InterWiki URL too long: ' . $len . ' characters');
824
825         return $url;
826 }
827 ?>