]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/stdlib.php
Add some sanity checking for pagenames.
[SourceForge/phpwiki.git] / lib / stdlib.php
1 <?php //rcs_id('$Id: stdlib.php,v 1.142 2003-02-25 22:19:46 dairiki Exp $');
2
3 /*
4   Standard functions for Wiki functionality
5     WikiURL($pagename, $args, $get_abs_url)
6     IconForLink($protocol_or_url)
7     LinkURL($url, $linktext)
8     LinkImage($url, $alt)
9
10     SplitQueryArgs ($query_args)
11     LinkPhpwikiURL($url, $text)
12     ConvertOldMarkup($content)
13     
14     class Stack { push($item), pop(), cnt(), top() }
15
16     split_pagename ($page)
17     NoSuchRevision ($request, $page, $version)
18     TimezoneOffset ($time, $no_colon)
19     Iso8601DateTime ($time)
20     Rfc2822DateTime ($time)
21     CTime ($time)
22     __printf ($fmt)
23     __sprintf ($fmt)
24     __vsprintf ($fmt, $args)
25     better_srand($seed = '')
26     count_all($arg)
27     isSubPage($pagename)
28     subPageSlice($pagename, $pos)
29     explodePageList($input, $perm = false)
30
31   function: LinkInterWikiLink($link, $linktext)
32   moved to: lib/interwiki.php
33   function: linkExistingWikiWord($wikiword, $linktext, $version)
34   moved to: lib/Theme.php
35   function: LinkUnknownWikiWord($wikiword, $linktext)
36   moved to: lib/Theme.php
37   function: UpdateRecentChanges($dbi, $pagename, $isnewpage) 
38   gone see: lib/plugin/RecentChanges.php
39 */
40
41 define('MAX_PAGENAME_LENGTH', 100);
42
43             
44 /**
45  * Convert string to a valid XML identifier.
46  *
47  * XML 1.0 identifiers are of the form: [A-Za-z][A-Za-z0-9:_.-]*
48  *
49  * We would like to have, e.g. named anchors within wiki pages
50  * names like "Table of Contents" --- clearly not a valid XML
51  * fragment identifier.
52  *
53  * This function implements a one-to-one map from {any string}
54  * to {valid XML identifiers}.
55  *
56  * It does this by
57  * converting all bytes not in [A-Za-z0-9:_-],
58  * and any leading byte not in [A-Za-z] to 'xbb.',
59  * where 'bb' is the hexadecimal representation of the
60  * character.
61  *
62  * As a special case, the empty string is converted to 'empty.'
63  *
64  * @param string $str
65  * @return string
66  */
67 function MangleXmlIdentifier($str) 
68 {
69     if (!$str)
70         return 'empty.';
71     
72     return preg_replace('/[^-_:A-Za-z0-9]|(?<=^)[^A-Za-z]/e',
73                         "'x' . sprintf('%02x', ord('\\0')) . '.'",
74                         $str);
75 }
76     
77
78 /**
79  * Generates a valid URL for a given Wiki pagename.
80  * @param mixed $pagename If a string this will be the name of the Wiki page to link to.
81  *                        If a WikiDB_Page object function will extract the name to link to.
82  *                        If a WikiDB_PageRevision object function will extract the name to link to.
83  * @param array $args 
84  * @param boolean $get_abs_url Default value is false.
85  * @return string The absolute URL to the page passed as $pagename.
86  */
87 function WikiURL($pagename, $args = '', $get_abs_url = false) {
88     $anchor = false;
89     
90     if (is_object($pagename)) {
91         if (isa($pagename, 'WikiDB_Page')) {
92             $pagename = $pagename->getName();
93         }
94         elseif (isa($pagename, 'WikiDB_PageRevision')) {
95             $page = $pagename->getPage();
96             $args['version'] = $pagename->getVersion();
97             $pagename = $page->getName();
98         }
99         elseif (isa($pagename, 'WikiPageName')) {
100             $anchor = $pagename->anchor;
101             $pagename = $pagename->name;
102         }
103     }
104     
105     if (is_array($args)) {
106         $enc_args = array();
107         foreach  ($args as $key => $val) {
108             if (!is_array($val)) // ugly hack for getURLtoSelf() which also takes POST vars
109               $enc_args[] = urlencode($key) . '=' . urlencode($val);
110         }
111         $args = join('&', $enc_args);
112     }
113
114     if (USE_PATH_INFO) {
115         $url = $get_abs_url ? SERVER_URL . VIRTUAL_PATH . "/" : "";
116         $url .= preg_replace('/%2f/i', '/', rawurlencode($pagename));
117         if ($args)
118             $url .= "?$args";
119     }
120     else {
121         $url = $get_abs_url ? SERVER_URL . SCRIPT_NAME : basename(SCRIPT_NAME);
122         $url .= "?pagename=" . rawurlencode($pagename);
123         if ($args)
124             $url .= "&$args";
125     }
126     if ($anchor)
127         $url .= "#" . MangleXmlIdentifier($anchor);
128     return $url;
129 }
130
131 /**
132  * Generates icon in front of links.
133  *
134  * @param string $protocol_or_url URL or protocol to determine which icon to use.
135  *
136  * @return HtmlElement HtmlElement object that contains data to create img link to
137  * icon for use with url or protocol passed to the function. False if no img to be
138  * displayed.
139  */
140 function IconForLink($protocol_or_url) {
141     global $Theme;
142     if ($filename_suffix = false) {
143         // display apache style icon for file type instead of protocol icon
144         // - archive: unix:gz,bz2,tgz,tar,z; mac:dmg,dmgz,bin,img,cpt,sit; pc:zip;
145         // - document: html, htm, text, txt, rtf, pdf, doc
146         // - non-inlined image: jpg,jpeg,png,gif,tiff,tif,swf,pict,psd,eps,ps
147         // - audio: mp3,mp2,aiff,aif,au
148         // - multimedia: mpeg,mpg,mov,qt
149     } else {
150         list ($proto) = explode(':', $protocol_or_url, 2);
151         $src = $Theme->getLinkIconURL($proto);
152         if ($src)
153             return HTML::img(array('src' => $src, 'alt' => "", 'class' => 'linkicon', 'border' => 0));
154         else
155             return false;
156     }
157 }
158
159 /**
160  * Glue icon in front of text.
161  *
162  * @param string $protocol_or_url Protocol or URL.  Used to determine the
163  * proper icon.
164  * @param string $text The text.
165  * @return XmlContent.
166  */
167 function PossiblyGlueIconToText($proto_or_url, $text) {
168     $icon = IconForLink($proto_or_url);
169     if ($icon) {
170         preg_match('/^\s*(\S*)(.*?)\s*$/', $text, $m);
171         list (, $first_word, $tail) = $m;
172         $text = HTML::span(array('style' => 'white-space: nowrap'),
173                            $icon, $first_word);
174         if ($tail)
175             $text = HTML($text, $tail);
176     }
177     return $text;
178 }
179
180 /**
181  * Determines if the url passed to function is safe, by detecting if the characters
182  * '<', '>', or '"' are present.
183  *
184  * @param string $url URL to check for unsafe characters.
185  * @return boolean True if same, false else.
186  */
187 function IsSafeURL($url) {
188     return !ereg('[<>"]', $url);
189 }
190
191 /**
192  * Generates an HtmlElement object to store data for a link.
193  *
194  * @param string $url URL that the link will point to.
195  * @param string $linktext Text to be displayed as link.
196  * @return HtmlElement HtmlElement object that contains data to construct an html link.
197  */
198 function LinkURL($url, $linktext = '') {
199     // FIXME: Is this needed (or sufficient?)
200     if(! IsSafeURL($url)) {
201         $link = HTML::strong(HTML::u(array('class' => 'baduri'),
202                                      _("BAD URL -- remove all of <, >, \"")));
203     }
204     else {
205         if (!$linktext)
206             $linktext = preg_replace("/mailto:/A", "", $url);
207         
208         $link = HTML::a(array('href' => $url),
209                         PossiblyGlueIconToText($url, $linktext));
210         
211     }
212     $link->setAttr('class', $linktext ? 'namedurl' : 'rawurl');
213     return $link;
214 }
215
216
217 function LinkImage($url, $alt = false) {
218     // FIXME: Is this needed (or sufficient?)
219     if(! IsSafeURL($url)) {
220         $link = HTML::strong(HTML::u(array('class' => 'baduri'),
221                                      _("BAD URL -- remove all of <, >, \"")));
222     }
223     else {
224         if (empty($alt))
225             $alt = $url;
226         $link = HTML::img(array('src' => $url, 'alt' => $alt));
227     }
228     $link->setAttr('class', 'inlineimage');
229     return $link;
230 }
231
232
233
234 class Stack {
235     var $items = array();
236     var $size = 0;
237     
238     function push($item) {
239         $this->items[$this->size] = $item;
240         $this->size++;
241         return true;
242     }  
243     
244     function pop() {
245         if ($this->size == 0) {
246             return false; // stack is empty
247         }  
248         $this->size--;
249         return $this->items[$this->size];
250     }  
251     
252     function cnt() {
253         return $this->size;
254     }  
255     
256     function top() {
257         if($this->size)
258             return $this->items[$this->size - 1];
259         else
260             return '';
261     }
262     
263 }  
264 // end class definition
265
266 function SplitQueryArgs ($query_args = '') 
267 {
268     $split_args = split('&', $query_args);
269     $args = array();
270     while (list($key, $val) = each($split_args))
271         if (preg_match('/^ ([^=]+) =? (.*) /x', $val, $m))
272             $args[$m[1]] = $m[2];
273     return $args;
274 }
275
276 function LinkPhpwikiURL($url, $text = '') {
277     $args = array();
278     
279     if (!preg_match('/^ phpwiki: ([^?]*) [?]? (.*) $/x', $url, $m)) {
280         return HTML::strong(array('class' => 'rawurl'),
281                             HTML::u(array('class' => 'baduri'),
282                                     _("BAD phpwiki: URL")));
283     }
284
285     if ($m[1])
286         $pagename = urldecode($m[1]);
287     $qargs = $m[2];
288     
289     if (empty($pagename) &&
290         preg_match('/^(diff|edit|links|info)=([^&]+)$/', $qargs, $m)) {
291         // Convert old style links (to not break diff links in
292         // RecentChanges).
293         $pagename = urldecode($m[2]);
294         $args = array("action" => $m[1]);
295     }
296     else {
297         $args = SplitQueryArgs($qargs);
298     }
299
300     if (empty($pagename))
301         $pagename = $GLOBALS['request']->getArg('pagename');
302
303     if (isset($args['action']) && $args['action'] == 'browse')
304         unset($args['action']);
305     
306     /*FIXME:
307       if (empty($args['action']))
308       $class = 'wikilink';
309       else if (is_safe_action($args['action']))
310       $class = 'wikiaction';
311     */
312     if (empty($args['action']) || is_safe_action($args['action']))
313         $class = 'wikiaction';
314     else {
315         // Don't allow administrative links on unlocked pages.
316         $page = $GLOBALS['request']->getPage();
317         if (!$page->get('locked'))
318             return HTML::span(array('class' => 'wikiunsafe'),
319                               HTML::u(_("Lock page to enable link")));
320         $class = 'wikiadmin';
321     }
322     
323     if (!$text)
324         $text = HTML::span(array('class' => 'rawurl'), $url);
325
326     return HTML::a(array('href'  => WikiURL(CanonizePagename($pagename), $args),
327                          'class' => $class),
328                    $text);
329 }
330
331 /**
332  * A class to assist in parsing wiki pagenames.
333  *
334  * Now with subpages and anchors, parsing and passing around
335  * pagenames is more complicated.  This should help.
336  */
337 class WikiPagename
338 {
339     /** Short name for page.
340      *
341      * This is the value of $name passed to the constructor.
342      * (For use, e.g. as a default label for links to the page.)
343      */
344     var $shortName;
345
346     /** The full page name.
347      *
348      * This is the full name of the page (without anchor).
349      */
350     var $name;
351     
352     /** The anchor.
353      *
354      * This is the referenced anchor within the page, or the empty string.
355      */
356     var $anchor;
357     
358     /** Constructor
359      *
360      * @param mixed $name Page name.
361      * WikiDB_Page, WikiDB_PageRevision, or string.
362      * This can be a relative subpage name (like '/SubPage'),
363      * or can be the empty string to refer to the $basename.
364      *
365      * @param string $anchor For links to anchors in page.
366      *
367      * @param mixed $basename Page name from which to interpret
368      * relative or other non-fully-specified page names.
369      */
370     function WikiPageName($name, $basename=false, $anchor=false) {
371         if (is_string($name)) {
372             $this->shortName = $name;
373         
374             if ($anchor === false and preg_match('/\A(.*)#(.*?)?\Z/', $name, $m))
375                 list(, $name, $anchor) = $m;
376             
377             if (empty($name) or $name[0] == SUBPAGE_SEPARATOR) {
378                 if ($basename)
379                     $name = $this->_pagename($basename) . $name;
380                 else
381                     $name = $this->_normalize_bad_pagename($name);
382             }
383         }
384         else {
385             $name = $this->_pagename($name);
386             $this->shortName = $name;
387         }
388
389         $this->name = $this->CanonizePagename($name);
390         $this->anchor = (string)$anchor;
391     }
392
393     function getParent() {
394         $name = $this->name;
395         if (!($tail = strrchr($name, SUBPAGE_SEPARATOR)))
396             return false;
397         return substr($name, 0, -strlen($tail));
398     }
399     
400     function _pagename($page) {
401         if (isa($page, 'WikiDB_Page'))
402             return $page->getName();
403         elseif (isa($page, 'WikiDB_PageRevision'))
404             return $page->getPageName();
405         elseif (isa($page, 'WikiPageName'))
406             return $page->name;
407         if (!is_string($page)) {
408             print "PAGE: " . gettype($page) . " " . get_class($page) . "<br>\n";
409         }
410         //assert(is_string($page));
411         return $page;
412     }
413
414     function _normalize_bad_pagename($name) {
415         trigger_error("Bad pagename: " . $name, E_USER_WARNING);
416
417         // Punt...  You really shouldn't get here.
418         if (empty($name)) {
419             global $request;
420             return $request->getArg('pagename');
421         }
422         assert($name[0] == SUBPAGE_SEPARATOR);
423         return substr($name, 1);
424     }
425
426     /** Sanify pagename.
427      *
428      * There are certain restrictions on valid page names.  This
429      * function converts a page name to an allowable form.  The caller
430      * should ensure that the return value is not the empty string.
431      *
432      * @param string $pagename   Input pagename.
433      *
434      * @param bool $fail_on_error If true, this function will return
435      * false if the page name is not already in canonical form.  (The
436      * default is false, in which case the "fixed" pagename will be
437      * returned.)
438      *
439      * @return string Sanified or canonical page name.  Returns the
440      * empty string if there is no canonical version of the page name
441      * (in which case the page name should be considered completely
442      * illegal, and should not be treated as a page name at all.)
443      */
444     function CanonizePagename ($pagename, $fail_on_error=false) {
445         // Compress internal white-space to single space character.
446         $pagename = preg_replace('/[\s\xa0]+/', ' ', $orig = $pagename);
447         if ($pagename != $orig) {
448             trigger_error(sprintf(_("White space converted to single space in pagename '%s'"),
449                                   $pagename), E_USER_NOTICE);
450             if ($fail_on_error)
451                 return false;
452         }
453     
454         // Delete any control characters.
455         $pagename = preg_replace('/[\x00-\x1f\x7f\x80-\x9f]/', '', $orig = $pagename);
456         if ($pagename != $orig) {
457             trigger_error(sprintf(_("Pagename '%s': Control characters not allowed"),
458                                   $pagename), E_USER_NOTICE);
459             if ($fail_on_error)
460                 return false;
461         }
462
463         // Strip leading and trailing white-space.
464         $pagename = trim($pagename);
465         $orig = $pagename;
466         while ($pagename and $pagename[0] == SUBPAGE_SEPARATOR)
467             $pagename = substr($pagename, 1);
468         if ($pagename != $orig) {
469             trigger_error(sprintf(_("Leading %s not allowed in pagename '%s'"),
470                                   SUBPAGE_SEPARATOR, $orig), E_USER_NOTICE);
471             if ($fail_on_error)
472                 return false;
473         }
474
475         if (preg_match('/[:;]/', $pagename)) {
476             trigger_error(_("Semicolons and colons are deprecated within pagenames."),
477                           E_USER_NOTICE);
478         }
479     
480         if (strlen($pagename) > MAX_PAGENAME_LENGTH) {
481             $pagename = substr($orig = $pagename, 0, MAX_PAGENAME_LENGTH);
482             trigger_error(sprintf(_("Pagename '%s' is too long."), $orig),
483                           E_USER_NOTICE);
484             if ($fail_on_error)
485                 return false;
486         }
487     
488         return $pagename;
489     }
490 }
491
492 /**
493  * Convert old page markup to new-style markup.
494  *
495  * @param string $text Old-style wiki markup.
496  *
497  * @param string $markup_type
498  * One of: <dl>
499  * <dt><code>"block"</code>  <dd>Convert all markup.
500  * <dt><code>"inline"</code> <dd>Convert only inline markup.
501  * <dt><code>"links"</code>  <dd>Convert only link markup.
502  * </dl>
503  *
504  * @return string New-style wiki markup.
505  *
506  * @bugs Footnotes don't work quite as before (esp if there are
507  *   multiple references to the same footnote.  But close enough,
508  *   probably for now....
509  */
510 function ConvertOldMarkup ($text, $markup_type = "block") {
511
512     static $subs;
513     static $block_re;
514     
515     if (empty($subs)) {
516         /*****************************************************************
517          * Conversions for inline markup:
518          */
519
520         // escape tilde's
521         $orig[] = '/~/';
522         $repl[] = '~~';
523
524         // escape escaped brackets
525         $orig[] = '/\[\[/';
526         $repl[] = '~[';
527
528         // change ! escapes to ~'s.
529         global $AllowedProtocols, $WikiNameRegexp, $request;
530         include_once('lib/interwiki.php');
531         $map = InterWikiMap::GetMap($request);
532         $bang_esc[] = "(?:$AllowedProtocols):[^\s<>\[\]\"'()]*[^\s<>\[\]\"'(),.?]";
533         $bang_esc[] = $map->getRegexp() . ":[^\\s.,;?()]+"; // FIXME: is this really needed?
534         $bang_esc[] = $WikiNameRegexp;
535         $orig[] = '/!((?:' . join(')|(', $bang_esc) . '))/';
536         $repl[] = '~\\1';
537
538         $subs["links"] = array($orig, $repl);
539
540         // Escape '<'s
541         //$orig[] = '/<(?!\?plugin)|(?<!^)</m';
542         //$repl[] = '~<';
543         
544         // Convert footnote references.
545         $orig[] = '/(?<=.)(?<!~)\[\s*(\d+)\s*\]/m';
546         $repl[] = '#[|ftnt_ref_\\1]<sup>~[[\\1|#ftnt_\\1]~]</sup>';
547
548         // Convert old style emphases to HTML style emphasis.
549         $orig[] = '/__(.*?)__/';
550         $repl[] = '<strong>\\1</strong>';
551         $orig[] = "/''(.*?)''/";
552         $repl[] = '<em>\\1</em>';
553
554         // Escape nestled markup.
555         $orig[] = '/^(?<=^|\s)[=_](?=\S)|(?<=\S)[=_*](?=\s|$)/m';
556         $repl[] = '~\\0';
557         
558         // in old markup headings only allowed at beginning of line
559         $orig[] = '/!/';
560         $repl[] = '~!';
561
562         $subs["inline"] = array($orig, $repl);
563
564         /*****************************************************************
565          * Patterns which match block markup constructs which take
566          * special handling...
567          */
568
569         // Indented blocks
570         $blockpats[] = '[ \t]+\S(?:.*\s*\n[ \t]+\S)*';
571
572         // Tables
573         $blockpats[] = '\|(?:.*\n\|)*';
574
575         // List items
576         $blockpats[] = '[#*;]*(?:[*#]|;.*?:)';
577
578         // Footnote definitions
579         $blockpats[] = '\[\s*(\d+)\s*\]';
580
581         // Plugins
582         $blockpats[] = '<\?plugin(?:-form)?\b.*\?>\s*$';
583
584         // Section Title
585         $blockpats[] = '!{1,3}[^!]';
586
587         $block_re = ( '/\A((?:.|\n)*?)(^(?:'
588                       . join("|", $blockpats)
589                       . ').*$)\n?/m' );
590         
591     }
592     
593     if ($markup_type != "block") {
594         list ($orig, $repl) = $subs[$markup_type];
595         return preg_replace($orig, $repl, $text);
596     }
597     else {
598         list ($orig, $repl) = $subs['inline'];
599         $out = '';
600         while (preg_match($block_re, $text, $m)) {
601             $text = substr($text, strlen($m[0]));
602             list (,$leading_text, $block) = $m;
603             $suffix = "\n";
604             
605             if (strchr(" \t", $block[0])) {
606                 // Indented block
607                 $prefix = "<pre>\n";
608                 $suffix = "\n</pre>\n";
609             }
610             elseif ($block[0] == '|') {
611                 // Old-style table
612                 $prefix = "<?plugin OldStyleTable\n";
613                 $suffix = "\n?>\n";
614             }
615             elseif (strchr("#*;", $block[0])) {
616                 // Old-style list item
617                 preg_match('/^([#*;]*)([*#]|;.*?:) */', $block, $m);
618                 list (,$ind,$bullet) = $m;
619                 $block = substr($block, strlen($m[0]));
620                 
621                 $indent = str_repeat('     ', strlen($ind));
622                 if ($bullet[0] == ';') {
623                     //$term = ltrim(substr($bullet, 1));
624                     //return $indent . $term . "\n" . $indent . '     ';
625                     $prefix = $ind . $bullet;
626                 }
627                 else
628                     $prefix = $indent . $bullet . ' ';
629             }
630             elseif ($block[0] == '[') {
631                 // Footnote definition
632                 preg_match('/^\[\s*(\d+)\s*\]/', $block, $m);
633                 $footnum = $m[1];
634                 $block = substr($block, strlen($m[0]));
635                 $prefix = "#[|ftnt_${footnum}]~[[${footnum}|#ftnt_ref_${footnum}]~] ";
636             }
637             elseif ($block[0] == '<') {
638                 // Plugin.
639                 // HACK: no inline markup...
640                 $prefix = $block;
641                 $block = '';
642             }
643             elseif ($block[0] == '!') {
644                 // Section heading
645                 preg_match('/^!{1,3}/', $block, $m);
646                 $prefix = $m[0];
647                 $block = substr($block, strlen($m[0]));
648             }
649             else {
650                 // AAck!
651                 assert(0);
652             }
653
654             $out .= ( preg_replace($orig, $repl, $leading_text)
655                       . $prefix
656                       . preg_replace($orig, $repl, $block)
657                       . $suffix );
658         }
659         return $out . preg_replace($orig, $repl, $text);
660     }
661 }
662
663
664 /**
665  * Expand tabs in string.
666  *
667  * Converts all tabs to (the appropriate number of) spaces.
668  *
669  * @param string $str
670  * @param integer $tab_width
671  * @return string
672  */
673 function expand_tabs($str, $tab_width = 8) {
674     $split = split("\t", $str);
675     $tail = array_pop($split);
676     $expanded = "\n";
677     foreach ($split as $hunk) {
678         $expanded .= $hunk;
679         $pos = strlen(strrchr($expanded, "\n")) - 1;
680         $expanded .= str_repeat(" ", ($tab_width - $pos % $tab_width));
681     }
682     return substr($expanded, 1) . $tail;
683 }
684
685 /**
686  * Split WikiWords in page names.
687  *
688  * It has been deemed useful to split WikiWords (into "Wiki Words") in
689  * places like page titles. This is rumored to help search engines
690  * quite a bit.
691  *
692  * @param $page string The page name.
693  *
694  * @return string The split name.
695  */
696 function split_pagename ($page) {
697     
698     if (preg_match("/\s/", $page))
699         return $page;           // Already split --- don't split any more.
700     
701     // FIXME: this algorithm is Anglo-centric.
702     static $RE;
703     if (!isset($RE)) {
704         // This mess splits between a lower-case letter followed by
705         // either an upper-case or a numeral; except that it wont
706         // split the prefixes 'Mc', 'De', or 'Di' off of their tails.
707         $RE[] = '/([[:lower:]])((?<!Mc|De|Di)[[:upper:]]|\d)/';
708         // This the single-letter words 'I' and 'A' from any following
709         // capitalized words.
710         $sep = preg_quote(SUBPAGE_SEPARATOR, '/');
711         $RE[] = "/(?<= |${sep}|^)([AI])([[:upper:]][[:lower:]])/";
712         // Split numerals from following letters.
713         $RE[] = '/(\d)([[:alpha:]])/';
714         
715         foreach ($RE as $key => $val)
716             $RE[$key] = pcre_fix_posix_classes($val);
717     }
718
719     foreach ($RE as $regexp) {
720         $page = preg_replace($regexp, '\\1 \\2', $page);
721     }
722     return $page;
723 }
724
725 function NoSuchRevision (&$request, $page, $version) {
726     $html = HTML(HTML::h2(_("Revision Not Found")),
727                  HTML::p(fmt("I'm sorry.  Version %d of %s is not in the database.",
728                              $version, WikiLink($page, 'auto'))));
729     include_once('lib/Template.php');
730     GeneratePage($html, _("Bad Version"), $page->getCurrentRevision());
731     $request->finish();
732 }
733
734
735 /**
736  * Get time offset for local time zone.
737  *
738  * @param $time time_t Get offset for this time. Default: now.
739  * @param $no_colon boolean Don't put colon between hours and minutes.
740  * @return string Offset as a string in the format +HH:MM.
741  */
742 function TimezoneOffset ($time = false, $no_colon = false) {
743     if ($time === false)
744         $time = time();
745     $secs = date('Z', $time);
746
747     if ($secs < 0) {
748         $sign = '-';
749         $secs = -$secs;
750     }
751     else {
752         $sign = '+';
753     }
754     $colon = $no_colon ? '' : ':';
755     $mins = intval(($secs + 30) / 60);
756     return sprintf("%s%02d%s%02d",
757                    $sign, $mins / 60, $colon, $mins % 60);
758 }
759
760
761 /**
762  * Format time in ISO-8601 format.
763  *
764  * @param $time time_t Time.  Default: now.
765  * @return string Date and time in ISO-8601 format.
766  */
767 function Iso8601DateTime ($time = false) {
768     if ($time === false)
769         $time = time();
770     $tzoff = TimezoneOffset($time);
771     $date  = date('Y-m-d', $time);
772     $time  = date('H:i:s', $time);
773     return $date . 'T' . $time . $tzoff;
774 }
775
776 /**
777  * Format time in RFC-2822 format.
778  *
779  * @param $time time_t Time.  Default: now.
780  * @return string Date and time in RFC-2822 format.
781  */
782 function Rfc2822DateTime ($time = false) {
783     if ($time === false)
784         $time = time();
785     return date('D, j M Y H:i:s ', $time) . TimezoneOffset($time, 'no colon');
786 }
787
788 /**
789  * Format time in RFC-1123 format.
790  *
791  * @param $time time_t Time.  Default: now.
792  * @return string Date and time in RFC-1123 format.
793  */
794 function Rfc1123DateTime ($time = false) {
795     if ($time === false)
796         $time = time();
797     return gmdate('D, d M Y H:i:s \G\M\T', $time);
798 }
799
800 /** Parse date in RFC-1123 format.
801  *
802  * According to RFC 1123 we must accept dates in the following
803  * formats:
804  *
805  *   Sun, 06 Nov 1994 08:49:37 GMT  ; RFC 822, updated by RFC 1123
806  *   Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036
807  *   Sun Nov  6 08:49:37 1994       ; ANSI C's asctime() format
808  *
809  * (Though we're only allowed to generate dates in the first format.)
810  */
811 function ParseRfc1123DateTime ($timestr) {
812     $timestr = trim($timestr);
813     if (preg_match('/^ \w{3},\s* (\d{1,2}) \s* (\w{3}) \s* (\d{4}) \s*'
814                    .'(\d\d):(\d\d):(\d\d) \s* GMT $/ix',
815                    $timestr, $m)) {
816         list(, $mday, $mon, $year, $hh, $mm, $ss) = $m;
817     }
818     elseif (preg_match('/^ \w+,\s* (\d{1,2})-(\w{3})-(\d{2}|\d{4}) \s*'
819                        .'(\d\d):(\d\d):(\d\d) \s* GMT $/ix',
820                        $timestr, $m)) {
821         list(, $mday, $mon, $year, $hh, $mm, $ss) = $m;
822         if ($year < 70) $year += 2000;
823         elseif ($year < 100) $year += 1900;
824     }
825     elseif (preg_match('/^\w+\s* (\w{3}) \s* (\d{1,2}) \s*'
826                        .'(\d\d):(\d\d):(\d\d) \s* (\d{4})$/ix',
827                        $timestr, $m)) {
828         list(, $mon, $mday, $hh, $mm, $ss, $year) = $m;
829     }
830     else {
831         // Parse failed.
832         return false;
833     }
834
835     $time = strtotime("$mday $mon $year ${hh}:${mm}:${ss} GMT");
836     if ($time == -1)
837         return false;           // failed
838     return $time;
839 }
840
841 /**
842  * Format time to standard 'ctime' format.
843  *
844  * @param $time time_t Time.  Default: now.
845  * @return string Date and time.
846  */
847 function CTime ($time = false)
848 {
849     if ($time === false)
850         $time = time();
851     return date("D M j H:i:s Y", $time);
852 }
853
854
855
856 /**
857  * Internationalized printf.
858  *
859  * This is essentially the same as PHP's built-in printf
860  * with the following exceptions:
861  * <ol>
862  * <li> It passes the format string through gettext().
863  * <li> It supports the argument reordering extensions.
864  * </ol>
865  *
866  * Example:
867  *
868  * In php code, use:
869  * <pre>
870  *    __printf("Differences between versions %s and %s of %s",
871  *             $new_link, $old_link, $page_link);
872  * </pre>
873  *
874  * Then in locale/po/de.po, one can reorder the printf arguments:
875  *
876  * <pre>
877  *    msgid "Differences between %s and %s of %s."
878  *    msgstr "Der Unterschiedsergebnis von %3$s, zwischen %1$s und %2$s."
879  * </pre>
880  *
881  * (Note that while PHP tries to expand $vars within double-quotes,
882  * the values in msgstr undergo no such expansion, so the '$'s
883  * okay...)
884  *
885  * One shouldn't use reordered arguments in the default format string.
886  * Backslashes in the default string would be necessary to escape the
887  * '$'s, and they'll cause all kinds of trouble....
888  */ 
889 function __printf ($fmt) {
890     $args = func_get_args();
891     array_shift($args);
892     echo __vsprintf($fmt, $args);
893 }
894
895 /**
896  * Internationalized sprintf.
897  *
898  * This is essentially the same as PHP's built-in printf with the
899  * following exceptions:
900  *
901  * <ol>
902  * <li> It passes the format string through gettext().
903  * <li> It supports the argument reordering extensions.
904  * </ol>
905  *
906  * @see __printf
907  */ 
908 function __sprintf ($fmt) {
909     $args = func_get_args();
910     array_shift($args);
911     return __vsprintf($fmt, $args);
912 }
913
914 /**
915  * Internationalized vsprintf.
916  *
917  * This is essentially the same as PHP's built-in printf with the
918  * following exceptions:
919  *
920  * <ol>
921  * <li> It passes the format string through gettext().
922  * <li> It supports the argument reordering extensions.
923  * </ol>
924  *
925  * @see __printf
926  */ 
927 function __vsprintf ($fmt, $args) {
928     $fmt = gettext($fmt);
929     // PHP's sprintf doesn't support variable with specifiers,
930     // like sprintf("%*s", 10, "x"); --- so we won't either.
931     
932     if (preg_match_all('/(?<!%)%(\d+)\$/x', $fmt, $m)) {
933         // Format string has '%2$s' style argument reordering.
934         // PHP doesn't support this.
935         if (preg_match('/(?<!%)%[- ]?\d*[^- \d$]/x', $fmt))
936             // literal variable name substitution only to keep locale
937             // strings uncluttered
938             trigger_error(sprintf(_("Can't mix '%s' with '%s' type format strings"),
939                                   '%1\$s','%s'), E_USER_WARNING); //php+locale error
940         
941         $fmt = preg_replace('/(?<!%)%\d+\$/x', '%', $fmt);
942         $newargs = array();
943         
944         // Reorder arguments appropriately.
945         foreach($m[1] as $argnum) {
946             if ($argnum < 1 || $argnum > count($args))
947                 trigger_error(sprintf(_("%s: argument index out of range"), 
948                                       $argnum), E_USER_WARNING);
949             $newargs[] = $args[$argnum - 1];
950         }
951         $args = $newargs;
952     }
953     
954     // Not all PHP's have vsprintf, so...
955     array_unshift($args, $fmt);
956     return call_user_func_array('sprintf', $args);
957 }
958
959
960 class fileSet {
961     /**
962      * Build an array in $this->_fileList of files from $dirname.
963      * Subdirectories are not traversed.
964      *
965      * (This was a function LoadDir in lib/loadsave.php)
966      * See also http://www.php.net/manual/en/function.readdir.php
967      */
968     function getFiles() {
969         return $this->_fileList;
970     }
971
972     function _filenameSelector($filename) {
973         if (! $this->_pattern)
974             return true;
975         else {
976             return glob_match ($this->_pattern, $filename, $this->_case);
977         }
978     }
979
980     function fileSet($directory, $filepattern = false) {
981         $this->_fileList = array();
982         $this->_pattern = $filepattern;
983         $this->_case = !isWindows();
984         $this->_pathsep = '/';
985
986         if (empty($directory)) {
987             trigger_error(sprintf(_("%s is empty."), 'directoryname'),
988                           E_USER_NOTICE);
989             return; // early return
990         }
991
992         @ $dir_handle = opendir($dir=$directory);
993         if (empty($dir_handle)) {
994             trigger_error(sprintf(_("Unable to open directory '%s' for reading"),
995                                   $dir), E_USER_NOTICE);
996             return; // early return
997         }
998
999         while ($filename = readdir($dir_handle)) {
1000             if ($filename[0] == '.' || filetype($dir . $this->_pathsep . $filename) != 'file')
1001                 continue;
1002             if ($this->_filenameSelector($filename)) {
1003                 array_push($this->_fileList, "$filename");
1004                 //trigger_error(sprintf(_("found file %s"), $filename),
1005                 //                      E_USER_NOTICE); //debugging
1006             }
1007         }
1008         closedir($dir_handle);
1009     }
1010 };
1011
1012 // File globbing
1013
1014 // expands a list containing regex's to its matching entries
1015 class ListRegexExpand {
1016     var $match, $list, $index, $case_sensitive;
1017     function ListRegexExpand (&$list, $match, $case_sensitive = true) {
1018         $this->match = str_replace('/','\/',$match);
1019         $this->list = &$list;
1020         $this->case_sensitive = $case_sensitive;        
1021     }
1022     function listMatchCallback ($item, $key) {
1023         if (preg_match('/' . $this->match . ($this->case_sensitive ? '/' : '/i'), $item)) {
1024             unset($this->list[$this->index]);
1025             $this->list[] = $item;
1026         }
1027     }
1028     function expandRegex ($index, &$pages) {
1029         $this->index = $index;
1030         array_walk($pages, array($this, 'listMatchCallback'));
1031         return $this->list;
1032     }
1033 }
1034
1035 // convert fileglob to regex style
1036 function glob_to_pcre ($glob) {
1037     $re = preg_replace('/\./', '\\.', $glob);
1038     $re = preg_replace(array('/\*/','/\?/'), array('.*','.'), $glob);
1039     if (!preg_match('/^[\?\*]/',$glob))
1040         $re = '^' . $re;
1041     if (!preg_match('/[\?\*]$/',$glob))
1042         $re = $re . '$';
1043     return $re;
1044 }
1045
1046 function glob_match ($glob, $against, $case_sensitive = true) {
1047     return preg_match('/' . glob_to_pcre($glob) . ($case_sensitive ? '/' : '/i'), $against);
1048 }
1049
1050 function explodeList($input, $allnames, $glob_style = true, $case_sensitive = true) {
1051     $list = explode(',',$input);
1052     // expand wildcards from list of $allnames
1053     if (preg_match('/[\?\*]/',$input)) {
1054         for ($i = 0; $i < sizeof($list); $i++) {
1055             $f = $list[$i];
1056             if (preg_match('/[\?\*]/',$f)) {
1057                 reset($allnames);
1058                 $expand = new ListRegexExpand($list, $glob_style ? glob_to_pcre($f) : $f, $case_sensitive);
1059                 $expand->expandRegex($i, $allnames);
1060             }
1061         }
1062     }
1063     return $list;
1064 }
1065
1066 // echo implode(":",explodeList("Test*",array("xx","Test1","Test2")));
1067
1068 function explodePageList($input, $perm = false) {
1069     // expand wildcards from list of all pages
1070     if (preg_match('/[\?\*]/',$input)) {
1071         $dbi = $GLOBALS['request']->_dbi;
1072         $allPagehandles = $dbi->getAllPages($perm);
1073         while ($pagehandle = $allPagehandles->next()) {
1074             $allPages[] = $pagehandle->getName();
1075         }
1076         return explodeList($input, $allPages);
1077     } else {
1078         return explode(',',$input);
1079     }
1080 }
1081
1082 // Class introspections
1083
1084 /** Determine whether object is of a specified type.
1085  *
1086  * @param $object object An object.
1087  * @param $class string Class name.
1088  * @return bool True iff $object is a $class
1089  * or a sub-type of $class. 
1090  */
1091 function isa ($object, $class) 
1092 {
1093     $lclass = strtolower($class);
1094
1095     return is_object($object)
1096         && ( get_class($object) == strtolower($lclass)
1097              || is_subclass_of($object, $lclass) );
1098 }
1099
1100 /** Determine whether (possible) object has method.
1101  *
1102  * @param $object mixed Object
1103  * @param $method string Method name
1104  * @return bool True iff $object is an object with has method $method.
1105  */
1106 function can ($object, $method) 
1107 {
1108     return is_object($object) && method_exists($object, strtolower($method));
1109 }
1110
1111 /** Hash a value.
1112  *
1113  * This is used for generating ETags.
1114  */
1115 function hash ($x) {
1116     if (is_scalar($x)) {
1117         return $x;
1118     }
1119     elseif (is_array($x)) {            
1120         ksort($x);
1121         return md5(serialize($x));
1122     }
1123     elseif (is_object($x)) {
1124         return $x->hash();
1125     }
1126     trigger_error("Can't hash $x", E_USER_ERROR);
1127 }
1128
1129     
1130 /**
1131  * Seed the random number generator.
1132  *
1133  * better_srand() ensures the randomizer is seeded only once.
1134  * 
1135  * How random do you want it? See:
1136  * http://www.php.net/manual/en/function.srand.php
1137  * http://www.php.net/manual/en/function.mt-srand.php
1138  */
1139 function better_srand($seed = '') {
1140     static $wascalled = FALSE;
1141     if (!$wascalled) {
1142         $seed = $seed === '' ? (double) microtime() * 1000000 : $seed;
1143         srand($seed);
1144         $wascalled = TRUE;
1145         //trigger_error("new random seed", E_USER_NOTICE); //debugging
1146     }
1147 }
1148
1149 /**
1150  * Recursively count all non-empty elements 
1151  * in array of any dimension or mixed - i.e. 
1152  * array('1' => 2, '2' => array('1' => 3, '2' => 4))
1153  * See http://www.php.net/manual/en/function.count.php
1154  */
1155 function count_all($arg) {
1156     // skip if argument is empty
1157     if ($arg) {
1158         //print_r($arg); //debugging
1159         $count = 0;
1160         // not an array, return 1 (base case) 
1161         if(!is_array($arg))
1162             return 1;
1163         // else call recursively for all elements $arg
1164         foreach($arg as $key => $val)
1165             $count += count_all($val);
1166         return $count;
1167     }
1168 }
1169
1170 function isSubPage($pagename) {
1171     return (strstr($pagename, SUBPAGE_SEPARATOR));
1172 }
1173
1174 function subPageSlice($pagename, $pos) {
1175     $pages = explode(SUBPAGE_SEPARATOR,$pagename);
1176     $pages = array_slice($pages,$pos,1);
1177     return $pages[0];
1178 }
1179
1180 /**
1181  * Alert
1182  *
1183  * Class for "popping up" and alert box.  (Except that right now, it doesn't
1184  * pop up...)
1185  *
1186  * FIXME:
1187  * This is a hackish and needs to be refactored.  However it would be nice to
1188  * unify all the different methods we use for showing Alerts and Dialogs.
1189  * (E.g. "Page deleted", login form, ...)
1190  */
1191 class Alert {
1192     /** Constructor
1193      *
1194      * @param object $request
1195      * @param mixed $head  Header ("title") for alert box.
1196      * @param mixed $body  The text in the alert box.
1197      * @param hash $buttons  An array mapping button labels to URLs.
1198      *    The default is a single "Okay" button pointing to $request->getURLtoSelf().
1199      */
1200     function Alert($head, $body, $buttons=false) {
1201         if ($buttons === false)
1202             $buttons = array();
1203
1204         $this->_tokens = array('HEADER' => $head, 'CONTENT' => $body);
1205         $this->_buttons = $buttons;
1206     }
1207
1208     /**
1209      * Show the alert box.
1210      */
1211     function show(&$request) {
1212         global $request;
1213
1214         $tokens = $this->_tokens;
1215         $tokens['BUTTONS'] = $this->_getButtons();
1216         
1217         $request->discardOutput();
1218         $tmpl = new Template('dialog', $request, $tokens);
1219         $tmpl->printXML();
1220         $request->finish();
1221     }
1222
1223
1224     function _getButtons() {
1225         global $request;
1226
1227         $buttons = $this->_buttons;
1228         if (!$buttons)
1229             $buttons = array(_("Okay") => $request->getURLtoSelf());
1230         
1231         global $Theme;
1232         foreach ($buttons as $label => $url)
1233             print "$label $url\n";
1234             $out[] = $Theme->makeButton($label, $url, 'wikiaction');
1235         return new XmlContent($out);
1236     }
1237 }
1238
1239                       
1240         
1241 // $Log: not supported by cvs2svn $
1242 // Revision 1.141  2003/02/22 20:49:55  dairiki
1243 // Fixes for "Call-time pass by reference has been deprecated" errors.
1244 //
1245 // Revision 1.140  2003/02/21 23:33:29  dairiki
1246 // Set alt="" on the link icon image tags.
1247 // (See SF bug #675141.)
1248 //
1249 // Revision 1.139  2003/02/21 22:16:27  dairiki
1250 // Get rid of MakeWikiForm, and form-style MagicPhpWikiURLs.
1251 // These have been obsolete for quite awhile (I hope).
1252 //
1253 // Revision 1.138  2003/02/21 04:12:36  dairiki
1254 // WikiPageName: fixes for new cached links.
1255 //
1256 // Alert: new class for displaying alerts.
1257 //
1258 // ExtractWikiPageLinks and friends are now gone.
1259 //
1260 // LinkBracketLink moved to InlineParser.php
1261 //
1262 // Revision 1.137  2003/02/18 23:13:40  dairiki
1263 // Wups again.  Typo fix.
1264 //
1265 // Revision 1.136  2003/02/18 21:52:07  dairiki
1266 // Fix so that one can still link to wiki pages with # in their names.
1267 // (This was made difficult by the introduction of named tags, since
1268 // '[Page #1]' is now a link to anchor '1' in page 'Page'.
1269 //
1270 // Now the ~ escape for page names should work: [Page ~#1].
1271 //
1272 // Revision 1.135  2003/02/18 19:17:04  dairiki
1273 // split_pagename():
1274 //     Bug fix. 'ThisIsABug' was being split to 'This IsA Bug'.
1275 //     Cleanup up subpage splitting code.
1276 //
1277 // Revision 1.134  2003/02/16 19:44:20  dairiki
1278 // New function hash().  This is a helper, primarily for generating
1279 // HTTP ETags.
1280 //
1281 // Revision 1.133  2003/02/16 04:50:09  dairiki
1282 // New functions:
1283 // Rfc1123DateTime(), ParseRfc1123DateTime()
1284 // for converting unix timestamps to and from strings.
1285 //
1286 // These functions produce and grok the time strings
1287 // in the format specified by RFC 2616 for use in HTTP headers
1288 // (like Last-Modified).
1289 //
1290 // Revision 1.132  2003/01/04 22:19:43  carstenklapp
1291 // Bugfix UnfoldSubpages: "Undefined offset: 1" error when plugin invoked
1292 // on a page with no subpages (explodeList(): array 0-based, sizeof 1-based).
1293 //
1294
1295 // (c-file-style: "gnu")
1296 // Local Variables:
1297 // mode: php
1298 // tab-width: 8
1299 // c-basic-offset: 4
1300 // c-hanging-comment-ender-p: nil
1301 // indent-tabs-mode: nil
1302 // End:   
1303 ?>