]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/stdlib.php
Fix from Martin Geisler for overly aggresive
[SourceForge/phpwiki.git] / lib / stdlib.php
1 <?php //rcs_id('$Id: stdlib.php,v 1.131 2002-11-22 21:41:09 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     MakeWikiForm ($pagename, $args, $class, $button_text)
11     SplitQueryArgs ($query_args)
12     LinkPhpwikiURL($url, $text)
13     LinkBracketLink($bracketlink)
14     ExtractWikiPageLinks($content)
15     ConvertOldMarkup($content)
16     
17     class Stack { push($item), pop(), cnt(), top() }
18
19     split_pagename ($page)
20     NoSuchRevision ($request, $page, $version)
21     TimezoneOffset ($time, $no_colon)
22     Iso8601DateTime ($time)
23     Rfc2822DateTime ($time)
24     CTime ($time)
25     __printf ($fmt)
26     __sprintf ($fmt)
27     __vsprintf ($fmt, $args)
28     better_srand($seed = '')
29     count_all($arg)
30     isSubPage($pagename)
31     subPageSlice($pagename, $pos)
32     explodePageList($input, $perm = false)
33
34   function: LinkInterWikiLink($link, $linktext)
35   moved to: lib/interwiki.php
36   function: linkExistingWikiWord($wikiword, $linktext, $version)
37   moved to: lib/Theme.php
38   function: LinkUnknownWikiWord($wikiword, $linktext)
39   moved to: lib/Theme.php
40   function: UpdateRecentChanges($dbi, $pagename, $isnewpage) 
41   gone see: lib/plugin/RecentChanges.php
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->fullPagename;
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' => $proto, '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
267 function MakeWikiForm ($pagename, $args, $class, $button_text = '') {
268     // HACK: so as to not completely break old PhpWikiAdministration pages.
269     trigger_error("MagicPhpWikiURL forms are no longer supported.  "
270                   . "Use the WikiFormPlugin instead.", E_USER_NOTICE);
271
272     global $request;
273     $loader = new WikiPluginLoader;
274     @$action = (string)$args['action'];
275     return $loader->expandPI("<?plugin WikiForm action=$action ?>", $request);
276 }
277
278 function SplitQueryArgs ($query_args = '') 
279 {
280     $split_args = split('&', $query_args);
281     $args = array();
282     while (list($key, $val) = each($split_args))
283         if (preg_match('/^ ([^=]+) =? (.*) /x', $val, $m))
284             $args[$m[1]] = $m[2];
285     return $args;
286 }
287
288 function LinkPhpwikiURL($url, $text = '') {
289     $args = array();
290     
291     if (!preg_match('/^ phpwiki: ([^?]*) [?]? (.*) $/x', $url, $m)) {
292         return HTML::strong(array('class' => 'rawurl'),
293                             HTML::u(array('class' => 'baduri'),
294                                     _("BAD phpwiki: URL")));
295     }
296
297     if ($m[1])
298         $pagename = urldecode($m[1]);
299     $qargs = $m[2];
300     
301     if (empty($pagename) &&
302         preg_match('/^(diff|edit|links|info)=([^&]+)$/', $qargs, $m)) {
303         // Convert old style links (to not break diff links in
304         // RecentChanges).
305         $pagename = urldecode($m[2]);
306         $args = array("action" => $m[1]);
307     }
308     else {
309         $args = SplitQueryArgs($qargs);
310     }
311
312     if (empty($pagename))
313         $pagename = $GLOBALS['request']->getArg('pagename');
314
315     if (isset($args['action']) && $args['action'] == 'browse')
316         unset($args['action']);
317     
318     /*FIXME:
319       if (empty($args['action']))
320       $class = 'wikilink';
321       else if (is_safe_action($args['action']))
322       $class = 'wikiaction';
323     */
324     if (empty($args['action']) || is_safe_action($args['action']))
325         $class = 'wikiaction';
326     else {
327         // Don't allow administrative links on unlocked pages.
328         $page = $GLOBALS['request']->getPage();
329         if (!$page->get('locked'))
330             return HTML::span(array('class' => 'wikiunsafe'),
331                               HTML::u(_("Lock page to enable link")));
332         $class = 'wikiadmin';
333     }
334     
335     // FIXME: ug, don't like this
336     if (preg_match('/=\d*\(/', $qargs))
337         return MakeWikiForm($pagename, $args, $class, $text);
338     if (!$text)
339         $text = HTML::span(array('class' => 'rawurl'), $url);
340
341     return HTML::a(array('href'  => WikiURL($pagename, $args),
342                          'class' => $class),
343                    $text);
344 }
345
346 /**
347  * A class to assist in parsing wiki pagenames.
348  *
349  * Now with subpages and anchors, parsing and passing around
350  * pagenames is more complicated.  This should help.
351  */
352 class WikiPagename
353 {
354     /** Short name for page.
355      *
356      * This is the value of $name passed to the constructor.
357      */
358     var $shortName;
359
360     /** The full page name.
361      *
362      * This is the full name of the page (without anchor).
363      */
364     var $fullPagename;
365     
366     /** The anchor.
367      *
368      * This is the referenced anchor within the page, or the empty string.
369      */
370     var $anchor;
371     
372     /** Constructor
373      *
374      * @param WikiRequest $request
375      * @param string $name Page name.
376      * This can be a relative subpage name (like '/SubPage'), and can also
377      * include an anchor (e.g. 'SandBox#anchorname' or just '#anchor').
378      */
379     function WikiPageName($request, $name) {
380         $this->shortName = $name;
381
382         if ($name[0] == SUBPAGE_SEPARATOR or $name[0] == '#')
383             $name = $request->getArg('pagename') . $name;
384
385         if (strstr($name, '#')) {
386             list($this->fullPagename, $this->anchor) = split('#', $name, 2);
387         }
388         else {
389             $this->fullPagename = $name;
390             $this->anchor = '';
391         }
392         
393
394         $dbi = $request->getDbh();
395         $this->_exists = $dbi->isWikiPage($this->fullPagename);
396     }
397
398     /**
399      * Determine whether page 'exists'.
400      *
401      * @return boolean True if page exists with non-default content.
402      */
403     function exists() {
404         return $this->_exists;
405     }
406 }
407
408 function LinkBracketLink($bracketlink) {
409     global $request, $AllowedProtocols, $InlineImages;
410
411     include_once("lib/interwiki.php");
412     $intermap = InterWikiMap::GetMap($request);
413     
414     // $bracketlink will start and end with brackets; in between will
415     // be either a page name, a URL or both separated by a pipe.
416     
417     // strip brackets and leading space
418     preg_match('/(\#?) \[\s* (?: ([^|]*?) \s* (\|) )? \s* (.+?) \s*\]/x', $bracketlink, $matches);
419     list (, $hash, $label, $bar, $link) = $matches;
420
421     // if label looks like a url to an image, we want an image link.
422     if (preg_match("/\\.($InlineImages)$/i", $label)) {
423         $imgurl = $label;
424         if (! preg_match("#^($AllowedProtocols):#", $imgurl)) {
425             // linkname like 'images/next.gif'.
426             global $Theme;
427             $imgurl = $Theme->getImageURL($linkname);
428         }
429         $label = LinkImage($imgurl, $link);
430     }
431
432     if ($hash) {
433         // It's an anchor, not a link...
434         $id = MangleXmlIdentifier($link);
435         return HTML::a(array('name' => $id, 'id' => $id),
436                        $bar ? $label : $link);
437     }
438
439     $wikipage = new WikiPageName($request, $link);
440     if ($wikipage->exists()) {
441         return WikiLink($wikipage, 'known', $label);
442     }
443     elseif (preg_match("#^($AllowedProtocols):#", $link)) {
444         // if it's an image, embed it; otherwise, it's a regular link
445         if (preg_match("/\\.($InlineImages)$/i", $link))
446             // no image link, just the src. see [img|link] above
447             return LinkImage($link, $label);
448         else
449             return LinkURL($link, $label);
450     }
451     elseif (preg_match("/^phpwiki:/", $link))
452         return LinkPhpwikiURL($link, $label);
453     elseif (preg_match("/^" . $intermap->getRegexp() . ":/", $link))
454         return $intermap->link($link, $label);
455     else {
456         return WikiLink($wikipage, 'unknown', $label);
457     }
458 }
459
460 /**
461  * Extract internal links from wiki page.
462  *
463  * @param mixed $content The raw wiki-text, either as
464  * an array of lines or as one big string.
465  *
466  * @return array List of the names of pages linked to.
467  */
468 function ExtractWikiPageLinks($content) {
469     list ($wikilinks,) = ExtractLinks($content);
470     return $wikilinks;
471 }      
472
473 /**
474  * Extract external links from a wiki page.
475  *
476  * @param mixed $content The raw wiki-text, either as
477  * an array of lines or as one big string.
478  *
479  * @return array List of the names of pages linked to.
480  */
481 function ExtractExternalLinks($content) {
482     list (, $urls) = ExtractLinks($content);
483     return $urls;
484 }      
485
486 /**
487  * Extract links from wiki page.
488  *
489  * FIXME: this should be done by the transform code.
490  *
491  * @param mixed $content The raw wiki-text, either as
492  * an array of lines or as one big string.
493  *
494  * @return array List of two arrays.  The first contains
495  * the internal links (names of pages linked to), the second
496  * contains external URLs linked to.
497  */
498 function ExtractLinks($content) {
499     include_once('lib/interwiki.php');
500     global $request, $WikiNameRegexp, $AllowedProtocols;
501     
502     if (is_string($content))
503         $content = explode("\n", $content);
504     
505     $wikilinks = array();
506     $urls = array();
507     
508     foreach ($content as $line) {
509         // remove plugin code
510         $line = preg_replace('/<\?plugin\s+\w.*?\?>/', '', $line);
511         // remove escaped '['
512         $line = str_replace('[[', ' ', $line);
513         // remove footnotes
514         $line = preg_replace('/\[\d+\]/', ' ', $line);
515         
516         // bracket links (only type wiki-* is of interest)
517         $numBracketLinks = preg_match_all("/\[\s*([^\]|]+\|)?\s*(\S.*?)\s*\]/",
518                                           $line, $brktlinks);
519         for ($i = 0; $i < $numBracketLinks; $i++) {
520             $link = LinkBracketLink($brktlinks[0][$i]);
521             $class = $link->getAttr('class');
522             if (preg_match('/^(named-)?wiki(unknown)?$/', $class)) {
523                 if ($brktlinks[2][$i][0] == SUBPAGE_SEPARATOR) {
524                     $wikilinks[$request->getArg('pagename') . $brktlinks[2][$i]] = 1;
525                 } else {
526                     $wikilinks[$brktlinks[2][$i]] = 1;
527                 }
528             }
529             elseif (preg_match('/^(namedurl|rawurl|(named-)?interwiki)$/', $class)) {
530                 $urls[$brktlinks[2][$i]] = 1;
531             }
532             $line = str_replace($brktlinks[0][$i], '', $line);
533         }
534         
535         // Raw URLs
536         preg_match_all("/!?\b($AllowedProtocols):[^\s<>\[\]\"'()]*[^\s<>\[\]\"'(),.?]/",
537                        $line, $link);
538         foreach ($link[0] as $url) {
539             if ($url[0] <> '!') {
540                 $urls[$url] = 1;
541             }
542             $line = str_replace($url, '', $line);
543         }
544
545         // Interwiki links
546         $map = InterWikiMap::GetMap($request);
547         $regexp = pcre_fix_posix_classes("!?(?<![[:alnum:]])") 
548             . $map->getRegexp() . ":[^\\s.,;?()]+";
549         preg_match_all("/$regexp/", $line, $link);
550         foreach ($link[0] as $interlink) {
551             if ($interlink[0] <> '!') {
552                 $link = $map->link($interlink);
553                 $urls[$link->getAttr('href')] = 1;
554             }
555             $line = str_replace($interlink, '', $line);
556         }
557
558         // BumpyText old-style wiki links
559         if (preg_match_all("/!?$WikiNameRegexp/", $line, $link)) {
560             for ($i = 0; isset($link[0][$i]); $i++) {
561                 if($link[0][$i][0] <> '!') {
562                     if ($link[0][$i][0] == SUBPAGE_SEPARATOR) {
563                         $wikilinks[$request->getArg('pagename') . $link[0][$i]] = 1;
564                     } else {
565                         $wikilinks[$link[0][$i]] = 1;
566                     }
567                 }
568             }
569         }
570     }
571     return array(array_keys($wikilinks), array_keys($urls));
572 }      
573
574
575 /**
576  * Convert old page markup to new-style markup.
577  *
578  * @param string $text Old-style wiki markup.
579  *
580  * @param string $markup_type
581  * One of: <dl>
582  * <dt><code>"block"</code>  <dd>Convert all markup.
583  * <dt><code>"inline"</code> <dd>Convert only inline markup.
584  * <dt><code>"links"</code>  <dd>Convert only link markup.
585  * </dl>
586  *
587  * @return string New-style wiki markup.
588  *
589  * @bugs Footnotes don't work quite as before (esp if there are
590  *   multiple references to the same footnote.  But close enough,
591  *   probably for now....
592  */
593 function ConvertOldMarkup ($text, $markup_type = "block") {
594
595     static $subs;
596     static $block_re;
597     
598     if (empty($subs)) {
599         /*****************************************************************
600          * Conversions for inline markup:
601          */
602
603         // escape tilde's
604         $orig[] = '/~/';
605         $repl[] = '~~';
606
607         // escape escaped brackets
608         $orig[] = '/\[\[/';
609         $repl[] = '~[';
610
611         // change ! escapes to ~'s.
612         global $AllowedProtocols, $WikiNameRegexp, $request;
613         include_once('lib/interwiki.php');
614         $map = InterWikiMap::GetMap($request);
615         $bang_esc[] = "(?:$AllowedProtocols):[^\s<>\[\]\"'()]*[^\s<>\[\]\"'(),.?]";
616         $bang_esc[] = $map->getRegexp() . ":[^\\s.,;?()]+"; // FIXME: is this really needed?
617         $bang_esc[] = $WikiNameRegexp;
618         $orig[] = '/!((?:' . join(')|(', $bang_esc) . '))/';
619         $repl[] = '~\\1';
620
621         $subs["links"] = array($orig, $repl);
622
623         // Escape '<'s
624         //$orig[] = '/<(?!\?plugin)|(?<!^)</m';
625         //$repl[] = '~<';
626         
627         // Convert footnote references.
628         $orig[] = '/(?<=.)(?<!~)\[\s*(\d+)\s*\]/m';
629         $repl[] = '#[|ftnt_ref_\\1]<sup>~[[\\1|#ftnt_\\1]~]</sup>';
630
631         // Convert old style emphases to HTML style emphasis.
632         $orig[] = '/__(.*?)__/';
633         $repl[] = '<strong>\\1</strong>';
634         $orig[] = "/''(.*?)''/";
635         $repl[] = '<em>\\1</em>';
636
637         // Escape nestled markup.
638         $orig[] = '/^(?<=^|\s)[=_](?=\S)|(?<=\S)[=_*](?=\s|$)/m';
639         $repl[] = '~\\0';
640         
641         // in old markup headings only allowed at beginning of line
642         $orig[] = '/!/';
643         $repl[] = '~!';
644
645         $subs["inline"] = array($orig, $repl);
646
647         /*****************************************************************
648          * Patterns which match block markup constructs which take
649          * special handling...
650          */
651
652         // Indented blocks
653         $blockpats[] = '[ \t]+\S(?:.*\s*\n[ \t]+\S)*';
654
655         // Tables
656         $blockpats[] = '\|(?:.*\n\|)*';
657
658         // List items
659         $blockpats[] = '[#*;]*(?:[*#]|;.*?:)';
660
661         // Footnote definitions
662         $blockpats[] = '\[\s*(\d+)\s*\]';
663
664         // Plugins
665         $blockpats[] = '<\?plugin(?:-form)?\b.*\?>\s*$';
666
667         // Section Title
668         $blockpats[] = '!{1,3}[^!]';
669
670         $block_re = ( '/\A((?:.|\n)*?)(^(?:'
671                       . join("|", $blockpats)
672                       . ').*$)\n?/m' );
673         
674     }
675     
676     if ($markup_type != "block") {
677         list ($orig, $repl) = $subs[$markup_type];
678         return preg_replace($orig, $repl, $text);
679     }
680     else {
681         list ($orig, $repl) = $subs['inline'];
682         $out = '';
683         while (preg_match($block_re, $text, $m)) {
684             $text = substr($text, strlen($m[0]));
685             list (,$leading_text, $block) = $m;
686             $suffix = "\n";
687             
688             if (strchr(" \t", $block[0])) {
689                 // Indented block
690                 $prefix = "<pre>\n";
691                 $suffix = "\n</pre>\n";
692             }
693             elseif ($block[0] == '|') {
694                 // Old-style table
695                 $prefix = "<?plugin OldStyleTable\n";
696                 $suffix = "\n?>\n";
697             }
698             elseif (strchr("#*;", $block[0])) {
699                 // Old-style list item
700                 preg_match('/^([#*;]*)([*#]|;.*?:) */', $block, $m);
701                 list (,$ind,$bullet) = $m;
702                 $block = substr($block, strlen($m[0]));
703                 
704                 $indent = str_repeat('     ', strlen($ind));
705                 if ($bullet[0] == ';') {
706                     //$term = ltrim(substr($bullet, 1));
707                     //return $indent . $term . "\n" . $indent . '     ';
708                     $prefix = $ind . $bullet;
709                 }
710                 else
711                     $prefix = $indent . $bullet . ' ';
712             }
713             elseif ($block[0] == '[') {
714                 // Footnote definition
715                 preg_match('/^\[\s*(\d+)\s*\]/', $block, $m);
716                 $footnum = $m[1];
717                 $block = substr($block, strlen($m[0]));
718                 $prefix = "#[|ftnt_${footnum}]~[[${footnum}|#ftnt_ref_${footnum}]~] ";
719             }
720             elseif ($block[0] == '<') {
721                 // Plugin.
722                 // HACK: no inline markup...
723                 $prefix = $block;
724                 $block = '';
725             }
726             elseif ($block[0] == '!') {
727                 // Section heading
728                 preg_match('/^!{1,3}/', $block, $m);
729                 $prefix = $m[0];
730                 $block = substr($block, strlen($m[0]));
731             }
732             else {
733                 // AAck!
734                 assert(0);
735             }
736
737             $out .= ( preg_replace($orig, $repl, $leading_text)
738                       . $prefix
739                       . preg_replace($orig, $repl, $block)
740                       . $suffix );
741         }
742         return $out . preg_replace($orig, $repl, $text);
743     }
744 }
745
746
747 /**
748  * Expand tabs in string.
749  *
750  * Converts all tabs to (the appropriate number of) spaces.
751  *
752  * @param string $str
753  * @param integer $tab_width
754  * @return string
755  */
756 function expand_tabs($str, $tab_width = 8) {
757     $split = split("\t", $str);
758     $tail = array_pop($split);
759     $expanded = "\n";
760     foreach ($split as $hunk) {
761         $expanded .= $hunk;
762         $pos = strlen(strrchr($expanded, "\n")) - 1;
763         $expanded .= str_repeat(" ", ($tab_width - $pos % $tab_width));
764     }
765     return substr($expanded, 1) . $tail;
766 }
767
768 /**
769  * Split WikiWords in page names.
770  *
771  * It has been deemed useful to split WikiWords (into "Wiki Words") in
772  * places like page titles. This is rumored to help search engines
773  * quite a bit.
774  *
775  * @param $page string The page name.
776  *
777  * @return string The split name.
778  */
779 function split_pagename ($page) {
780     
781     if (preg_match("/\s/", $page))
782         return $page;           // Already split --- don't split any more.
783     
784     // FIXME: this algorithm is Anglo-centric.
785     static $RE;
786     if (!isset($RE)) {
787         // This mess splits between a lower-case letter followed by
788         // either an upper-case or a numeral; except that it wont
789         // split the prefixes 'Mc', 'De', or 'Di' off of their tails.
790         $RE[] = '/([[:lower:]])((?<!Mc|De|Di)[[:upper:]]|\d)/';
791         // This the single-letter words 'I' and 'A' from any following
792         // capitalized words.
793         $RE[] = '/(?: |^)([AI])([[:upper:]][[:lower:]])/';
794         // Split numerals from following letters.
795         $RE[] = '/(\d)([[:alpha:]])/';
796         
797         foreach ($RE as $key => $val)
798             $RE[$key] = pcre_fix_posix_classes($val);
799     }
800     if (isSubPage($page)) {
801         // FIXME: is this needed?
802         $pages = explode(SUBPAGE_SEPARATOR,$page);
803         $new_page = $pages[0] ? split_pagename($pages[0]) : '';
804         for ($i=1; $i < sizeof($pages); $i++) {
805             $new_page .=  (SUBPAGE_SEPARATOR . ($pages[$i] ? split_pagename($pages[$i]) : ''));
806         }
807         return $new_page;
808     } else {
809         foreach ($RE as $regexp) {
810             $page = preg_replace($regexp, '\\1 \\2', $page);
811         }
812         return $page;
813     }
814 }
815
816 function NoSuchRevision (&$request, $page, $version) {
817     $html = HTML(HTML::h2(_("Revision Not Found")),
818                  HTML::p(fmt("I'm sorry.  Version %d of %s is not in the database.",
819                              $version, WikiLink($page, 'auto'))));
820     include_once('lib/Template.php');
821     GeneratePage($html, _("Bad Version"), $page->getCurrentRevision());
822     $request->finish();
823 }
824
825
826 /**
827  * Get time offset for local time zone.
828  *
829  * @param $time time_t Get offset for this time. Default: now.
830  * @param $no_colon boolean Don't put colon between hours and minutes.
831  * @return string Offset as a string in the format +HH:MM.
832  */
833 function TimezoneOffset ($time = false, $no_colon = false) {
834     if ($time === false)
835         $time = time();
836     $secs = date('Z', $time);
837
838     if ($secs < 0) {
839         $sign = '-';
840         $secs = -$secs;
841     }
842     else {
843         $sign = '+';
844     }
845     $colon = $no_colon ? '' : ':';
846     $mins = intval(($secs + 30) / 60);
847     return sprintf("%s%02d%s%02d",
848                    $sign, $mins / 60, $colon, $mins % 60);
849 }
850
851
852 /**
853  * Format time in ISO-8601 format.
854  *
855  * @param $time time_t Time.  Default: now.
856  * @return string Date and time in ISO-8601 format.
857  */
858 function Iso8601DateTime ($time = false) {
859     if ($time === false)
860         $time = time();
861     $tzoff = TimezoneOffset($time);
862     $date  = date('Y-m-d', $time);
863     $time  = date('H:i:s', $time);
864     return $date . 'T' . $time . $tzoff;
865 }
866
867 /**
868  * Format time in RFC-2822 format.
869  *
870  * @param $time time_t Time.  Default: now.
871  * @return string Date and time in RFC-2822 format.
872  */
873 function Rfc2822DateTime ($time = false) {
874     if ($time === false)
875         $time = time();
876     return date('D, j M Y H:i:s ', $time) . TimezoneOffset($time, 'no colon');
877 }
878
879 /**
880  * Format time to standard 'ctime' format.
881  *
882  * @param $time time_t Time.  Default: now.
883  * @return string Date and time.
884  */
885 function CTime ($time = false)
886 {
887     if ($time === false)
888         $time = time();
889     return date("D M j H:i:s Y", $time);
890 }
891
892
893
894 /**
895  * Internationalized printf.
896  *
897  * This is essentially the same as PHP's built-in printf
898  * with the following exceptions:
899  * <ol>
900  * <li> It passes the format string through gettext().
901  * <li> It supports the argument reordering extensions.
902  * </ol>
903  *
904  * Example:
905  *
906  * In php code, use:
907  * <pre>
908  *    __printf("Differences between versions %s and %s of %s",
909  *             $new_link, $old_link, $page_link);
910  * </pre>
911  *
912  * Then in locale/po/de.po, one can reorder the printf arguments:
913  *
914  * <pre>
915  *    msgid "Differences between %s and %s of %s."
916  *    msgstr "Der Unterschiedsergebnis von %3$s, zwischen %1$s und %2$s."
917  * </pre>
918  *
919  * (Note that while PHP tries to expand $vars within double-quotes,
920  * the values in msgstr undergo no such expansion, so the '$'s
921  * okay...)
922  *
923  * One shouldn't use reordered arguments in the default format string.
924  * Backslashes in the default string would be necessary to escape the
925  * '$'s, and they'll cause all kinds of trouble....
926  */ 
927 function __printf ($fmt) {
928     $args = func_get_args();
929     array_shift($args);
930     echo __vsprintf($fmt, $args);
931 }
932
933 /**
934  * Internationalized sprintf.
935  *
936  * This is essentially the same as PHP's built-in printf with the
937  * following exceptions:
938  *
939  * <ol>
940  * <li> It passes the format string through gettext().
941  * <li> It supports the argument reordering extensions.
942  * </ol>
943  *
944  * @see __printf
945  */ 
946 function __sprintf ($fmt) {
947     $args = func_get_args();
948     array_shift($args);
949     return __vsprintf($fmt, $args);
950 }
951
952 /**
953  * Internationalized vsprintf.
954  *
955  * This is essentially the same as PHP's built-in printf with the
956  * following exceptions:
957  *
958  * <ol>
959  * <li> It passes the format string through gettext().
960  * <li> It supports the argument reordering extensions.
961  * </ol>
962  *
963  * @see __printf
964  */ 
965 function __vsprintf ($fmt, $args) {
966     $fmt = gettext($fmt);
967     // PHP's sprintf doesn't support variable with specifiers,
968     // like sprintf("%*s", 10, "x"); --- so we won't either.
969     
970     if (preg_match_all('/(?<!%)%(\d+)\$/x', $fmt, $m)) {
971         // Format string has '%2$s' style argument reordering.
972         // PHP doesn't support this.
973         if (preg_match('/(?<!%)%[- ]?\d*[^- \d$]/x', $fmt))
974             // literal variable name substitution only to keep locale
975             // strings uncluttered
976             trigger_error(sprintf(_("Can't mix '%s' with '%s' type format strings"),
977                                   '%1\$s','%s'), E_USER_WARNING); //php+locale error
978         
979         $fmt = preg_replace('/(?<!%)%\d+\$/x', '%', $fmt);
980         $newargs = array();
981         
982         // Reorder arguments appropriately.
983         foreach($m[1] as $argnum) {
984             if ($argnum < 1 || $argnum > count($args))
985                 trigger_error(sprintf(_("%s: argument index out of range"), 
986                                       $argnum), E_USER_WARNING);
987             $newargs[] = $args[$argnum - 1];
988         }
989         $args = $newargs;
990     }
991     
992     // Not all PHP's have vsprintf, so...
993     array_unshift($args, $fmt);
994     return call_user_func_array('sprintf', $args);
995 }
996
997
998 class fileSet {
999     /**
1000      * Build an array in $this->_fileList of files from $dirname.
1001      * Subdirectories are not traversed.
1002      *
1003      * (This was a function LoadDir in lib/loadsave.php)
1004      * See also http://www.php.net/manual/en/function.readdir.php
1005      */
1006     function getFiles() {
1007         return $this->_fileList;
1008     }
1009
1010     function _filenameSelector($filename) {
1011         if (! $this->_pattern)
1012             return true;
1013         else {
1014             return glob_match ($this->_pattern, $filename, $this->_case);
1015         }
1016     }
1017
1018     function fileSet($directory, $filepattern = false) {
1019         $this->_fileList = array();
1020         $this->_pattern = $filepattern;
1021         $this->_case = !isWindows();
1022         $this->_pathsep = '/';
1023
1024         if (empty($directory)) {
1025             trigger_error(sprintf(_("%s is empty."), 'directoryname'),
1026                           E_USER_NOTICE);
1027             return; // early return
1028         }
1029
1030         @ $dir_handle = opendir($dir=$directory);
1031         if (empty($dir_handle)) {
1032             trigger_error(sprintf(_("Unable to open directory '%s' for reading"),
1033                                   $dir), E_USER_NOTICE);
1034             return; // early return
1035         }
1036
1037         while ($filename = readdir($dir_handle)) {
1038             if ($filename[0] == '.' || filetype($dir . $this->_pathsep . $filename) != 'file')
1039                 continue;
1040             if ($this->_filenameSelector($filename)) {
1041                 array_push($this->_fileList, "$filename");
1042                 //trigger_error(sprintf(_("found file %s"), $filename),
1043                 //                      E_USER_NOTICE); //debugging
1044             }
1045         }
1046         closedir($dir_handle);
1047     }
1048 };
1049
1050 // File globbing
1051
1052 // expands a list containing regex's to its matching entries
1053 class ListRegexExpand {
1054     var $match, $list, $index, $case_sensitive;
1055     function ListRegexExpand (&$list, $match, $case_sensitive = true) {
1056         $this->match = str_replace('/','\/',$match);
1057         $this->list = &$list;
1058         $this->case_sensitive = $case_sensitive;        
1059     }
1060     function listMatchCallback ($item, $key) {
1061         if (preg_match('/' . $this->match . ($this->case_sensitive ? '/' : '/i'), $item)) {
1062             unset($this->list[$this->index]);
1063             $this->list[] = $item;
1064         }
1065     }
1066     function expandRegex ($index, &$pages) {
1067         $this->index = $index;
1068         array_walk($pages, array($this, 'listMatchCallback'));
1069         return $this->list;
1070     }
1071 }
1072
1073 // convert fileglob to regex style
1074 function glob_to_pcre ($glob) {
1075     $re = preg_replace('/\./', '\\.', $glob);
1076     $re = preg_replace(array('/\*/','/\?/'), array('.*','.'), $glob);
1077     if (!preg_match('/^[\?\*]/',$glob))
1078         $re = '^' . $re;
1079     if (!preg_match('/[\?\*]$/',$glob))
1080         $re = $re . '$';
1081     return $re;
1082 }
1083
1084 function glob_match ($glob, $against, $case_sensitive = true) {
1085     return preg_match('/' . glob_to_pcre($glob) . ($case_sensitive ? '/' : '/i'), $against);
1086 }
1087
1088 function explodeList($input, $allnames, $glob_style = true, $case_sensitive = true) {
1089     $list = explode(',',$input);
1090     // expand wildcards from list of $allnames
1091     if (preg_match('/[\?\*]/',$input)) {
1092         for ($i = 0; $i <= sizeof($list); $i++) {
1093             $f = $list[$i];
1094             if (preg_match('/[\?\*]/',$f)) {
1095                 reset($allnames);
1096                 $expand = new ListRegexExpand(&$list, $glob_style ? glob_to_pcre($f) : $f, $case_sensitive);
1097                 $expand->expandRegex($i, &$allnames);
1098             }
1099         }
1100     }
1101     return $list;
1102 }
1103
1104 // echo implode(":",explodeList("Test*",array("xx","Test1","Test2")));
1105
1106 function explodePageList($input, $perm = false) {
1107     // expand wildcards from list of all pages
1108     if (preg_match('/[\?\*]/',$input)) {
1109         $dbi = $GLOBALS['request']->_dbi;
1110         $allPagehandles = $dbi->getAllPages($perm);
1111         while ($pagehandle = $allPagehandles->next()) {
1112             $allPages[] = $pagehandle->getName();
1113         }
1114         return explodeList($input, &$allPages);
1115     } else {
1116         return explode(',',$input);
1117     }
1118 }
1119
1120 // Class introspections
1121
1122 /** Determine whether object is of a specified type.
1123  *
1124  * @param $object object An object.
1125  * @param $class string Class name.
1126  * @return bool True iff $object is a $class
1127  * or a sub-type of $class. 
1128  */
1129 function isa ($object, $class) 
1130 {
1131     $lclass = strtolower($class);
1132
1133     return is_object($object)
1134         && ( get_class($object) == strtolower($lclass)
1135              || is_subclass_of($object, $lclass) );
1136 }
1137
1138 /** Determine whether (possible) object has method.
1139  *
1140  * @param $object mixed Object
1141  * @param $method string Method name
1142  * @return bool True iff $object is an object with has method $method.
1143  */
1144 function can ($object, $method) 
1145 {
1146     return is_object($object) && method_exists($object, strtolower($method));
1147 }
1148
1149 /**
1150  * Seed the random number generator.
1151  *
1152  * better_srand() ensures the randomizer is seeded only once.
1153  * 
1154  * How random do you want it? See:
1155  * http://www.php.net/manual/en/function.srand.php
1156  * http://www.php.net/manual/en/function.mt-srand.php
1157  */
1158 function better_srand($seed = '') {
1159     static $wascalled = FALSE;
1160     if (!$wascalled) {
1161         $seed = $seed === '' ? (double) microtime() * 1000000 : $seed;
1162         srand($seed);
1163         $wascalled = TRUE;
1164         //trigger_error("new random seed", E_USER_NOTICE); //debugging
1165     }
1166 }
1167
1168 /**
1169  * Recursively count all non-empty elements 
1170  * in array of any dimension or mixed - i.e. 
1171  * array('1' => 2, '2' => array('1' => 3, '2' => 4))
1172  * See http://www.php.net/manual/en/function.count.php
1173  */
1174 function count_all($arg) {
1175     // skip if argument is empty
1176     if ($arg) {
1177         //print_r($arg); //debugging
1178         $count = 0;
1179         // not an array, return 1 (base case) 
1180         if(!is_array($arg))
1181             return 1;
1182         // else call recursively for all elements $arg
1183         foreach($arg as $key => $val)
1184             $count += count_all($val);
1185         return $count;
1186     }
1187 }
1188
1189 function isSubPage($pagename) {
1190     return (strstr($pagename, SUBPAGE_SEPARATOR));
1191 }
1192
1193 function subPageSlice($pagename, $pos) {
1194     $pages = explode(SUBPAGE_SEPARATOR,$pagename);
1195     $pages = array_slice($pages,$pos,1);
1196     return $pages[0];
1197 }
1198
1199
1200 // (c-file-style: "gnu")
1201 // Local Variables:
1202 // mode: php
1203 // tab-width: 8
1204 // c-basic-offset: 4
1205 // c-hanging-comment-ender-p: nil
1206 // indent-tabs-mode: nil
1207 // End:   
1208 ?>