]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/stdlib.php
New functions:
[SourceForge/phpwiki.git] / lib / stdlib.php
1 <?php //rcs_id('$Id: stdlib.php,v 1.133 2003-02-16 04:50: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 in RFC-1123 format.
881  *
882  * @param $time time_t Time.  Default: now.
883  * @return string Date and time in RFC-1123 format.
884  */
885 function Rfc1123DateTime ($time = false) {
886     if ($time === false)
887         $time = time();
888     return gmdate('D, d M Y H:i:s \G\M\T', $time);
889 }
890
891 /** Parse date in RFC-1123 format.
892  *
893  * According to RFC 1123 we must accept dates in the following
894  * formats:
895  *
896  *   Sun, 06 Nov 1994 08:49:37 GMT  ; RFC 822, updated by RFC 1123
897  *   Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036
898  *   Sun Nov  6 08:49:37 1994       ; ANSI C's asctime() format
899  *
900  * (Though we're only allowed to generate dates in the first format.)
901  */
902 function ParseRfc1123DateTime ($timestr) {
903     $timestr = trim($timestr);
904     if (preg_match('/^ \w{3},\s* (\d{1,2}) \s* (\w{3}) \s* (\d{4}) \s*'
905                    .'(\d\d):(\d\d):(\d\d) \s* GMT $/ix',
906                    $timestr, $m)) {
907         list(, $mday, $mon, $year, $hh, $mm, $ss) = $m;
908     }
909     elseif (preg_match('/^ \w+,\s* (\d{1,2})-(\w{3})-(\d{2}|\d{4}) \s*'
910                        .'(\d\d):(\d\d):(\d\d) \s* GMT $/ix',
911                        $timestr, $m)) {
912         list(, $mday, $mon, $year, $hh, $mm, $ss) = $m;
913         if ($year < 70) $year += 2000;
914         elseif ($year < 100) $year += 1900;
915     }
916     elseif (preg_match('/^\w+\s* (\w{3}) \s* (\d{1,2}) \s*'
917                        .'(\d\d):(\d\d):(\d\d) \s* (\d{4})$/ix',
918                        $timestr, $m)) {
919         list(, $mon, $mday, $hh, $mm, $ss, $year) = $m;
920     }
921     else {
922         // Parse failed.
923         return false;
924     }
925
926     $time = strtotime("$mday $mon $year ${hh}:${mm}:${ss} GMT");
927     if ($time == -1)
928         return false;           // failed
929     return $time;
930 }
931
932 /**
933  * Format time to standard 'ctime' format.
934  *
935  * @param $time time_t Time.  Default: now.
936  * @return string Date and time.
937  */
938 function CTime ($time = false)
939 {
940     if ($time === false)
941         $time = time();
942     return date("D M j H:i:s Y", $time);
943 }
944
945
946
947 /**
948  * Internationalized printf.
949  *
950  * This is essentially the same as PHP's built-in printf
951  * with the following exceptions:
952  * <ol>
953  * <li> It passes the format string through gettext().
954  * <li> It supports the argument reordering extensions.
955  * </ol>
956  *
957  * Example:
958  *
959  * In php code, use:
960  * <pre>
961  *    __printf("Differences between versions %s and %s of %s",
962  *             $new_link, $old_link, $page_link);
963  * </pre>
964  *
965  * Then in locale/po/de.po, one can reorder the printf arguments:
966  *
967  * <pre>
968  *    msgid "Differences between %s and %s of %s."
969  *    msgstr "Der Unterschiedsergebnis von %3$s, zwischen %1$s und %2$s."
970  * </pre>
971  *
972  * (Note that while PHP tries to expand $vars within double-quotes,
973  * the values in msgstr undergo no such expansion, so the '$'s
974  * okay...)
975  *
976  * One shouldn't use reordered arguments in the default format string.
977  * Backslashes in the default string would be necessary to escape the
978  * '$'s, and they'll cause all kinds of trouble....
979  */ 
980 function __printf ($fmt) {
981     $args = func_get_args();
982     array_shift($args);
983     echo __vsprintf($fmt, $args);
984 }
985
986 /**
987  * Internationalized sprintf.
988  *
989  * This is essentially the same as PHP's built-in printf with the
990  * following exceptions:
991  *
992  * <ol>
993  * <li> It passes the format string through gettext().
994  * <li> It supports the argument reordering extensions.
995  * </ol>
996  *
997  * @see __printf
998  */ 
999 function __sprintf ($fmt) {
1000     $args = func_get_args();
1001     array_shift($args);
1002     return __vsprintf($fmt, $args);
1003 }
1004
1005 /**
1006  * Internationalized vsprintf.
1007  *
1008  * This is essentially the same as PHP's built-in printf with the
1009  * following exceptions:
1010  *
1011  * <ol>
1012  * <li> It passes the format string through gettext().
1013  * <li> It supports the argument reordering extensions.
1014  * </ol>
1015  *
1016  * @see __printf
1017  */ 
1018 function __vsprintf ($fmt, $args) {
1019     $fmt = gettext($fmt);
1020     // PHP's sprintf doesn't support variable with specifiers,
1021     // like sprintf("%*s", 10, "x"); --- so we won't either.
1022     
1023     if (preg_match_all('/(?<!%)%(\d+)\$/x', $fmt, $m)) {
1024         // Format string has '%2$s' style argument reordering.
1025         // PHP doesn't support this.
1026         if (preg_match('/(?<!%)%[- ]?\d*[^- \d$]/x', $fmt))
1027             // literal variable name substitution only to keep locale
1028             // strings uncluttered
1029             trigger_error(sprintf(_("Can't mix '%s' with '%s' type format strings"),
1030                                   '%1\$s','%s'), E_USER_WARNING); //php+locale error
1031         
1032         $fmt = preg_replace('/(?<!%)%\d+\$/x', '%', $fmt);
1033         $newargs = array();
1034         
1035         // Reorder arguments appropriately.
1036         foreach($m[1] as $argnum) {
1037             if ($argnum < 1 || $argnum > count($args))
1038                 trigger_error(sprintf(_("%s: argument index out of range"), 
1039                                       $argnum), E_USER_WARNING);
1040             $newargs[] = $args[$argnum - 1];
1041         }
1042         $args = $newargs;
1043     }
1044     
1045     // Not all PHP's have vsprintf, so...
1046     array_unshift($args, $fmt);
1047     return call_user_func_array('sprintf', $args);
1048 }
1049
1050
1051 class fileSet {
1052     /**
1053      * Build an array in $this->_fileList of files from $dirname.
1054      * Subdirectories are not traversed.
1055      *
1056      * (This was a function LoadDir in lib/loadsave.php)
1057      * See also http://www.php.net/manual/en/function.readdir.php
1058      */
1059     function getFiles() {
1060         return $this->_fileList;
1061     }
1062
1063     function _filenameSelector($filename) {
1064         if (! $this->_pattern)
1065             return true;
1066         else {
1067             return glob_match ($this->_pattern, $filename, $this->_case);
1068         }
1069     }
1070
1071     function fileSet($directory, $filepattern = false) {
1072         $this->_fileList = array();
1073         $this->_pattern = $filepattern;
1074         $this->_case = !isWindows();
1075         $this->_pathsep = '/';
1076
1077         if (empty($directory)) {
1078             trigger_error(sprintf(_("%s is empty."), 'directoryname'),
1079                           E_USER_NOTICE);
1080             return; // early return
1081         }
1082
1083         @ $dir_handle = opendir($dir=$directory);
1084         if (empty($dir_handle)) {
1085             trigger_error(sprintf(_("Unable to open directory '%s' for reading"),
1086                                   $dir), E_USER_NOTICE);
1087             return; // early return
1088         }
1089
1090         while ($filename = readdir($dir_handle)) {
1091             if ($filename[0] == '.' || filetype($dir . $this->_pathsep . $filename) != 'file')
1092                 continue;
1093             if ($this->_filenameSelector($filename)) {
1094                 array_push($this->_fileList, "$filename");
1095                 //trigger_error(sprintf(_("found file %s"), $filename),
1096                 //                      E_USER_NOTICE); //debugging
1097             }
1098         }
1099         closedir($dir_handle);
1100     }
1101 };
1102
1103 // File globbing
1104
1105 // expands a list containing regex's to its matching entries
1106 class ListRegexExpand {
1107     var $match, $list, $index, $case_sensitive;
1108     function ListRegexExpand (&$list, $match, $case_sensitive = true) {
1109         $this->match = str_replace('/','\/',$match);
1110         $this->list = &$list;
1111         $this->case_sensitive = $case_sensitive;        
1112     }
1113     function listMatchCallback ($item, $key) {
1114         if (preg_match('/' . $this->match . ($this->case_sensitive ? '/' : '/i'), $item)) {
1115             unset($this->list[$this->index]);
1116             $this->list[] = $item;
1117         }
1118     }
1119     function expandRegex ($index, &$pages) {
1120         $this->index = $index;
1121         array_walk($pages, array($this, 'listMatchCallback'));
1122         return $this->list;
1123     }
1124 }
1125
1126 // convert fileglob to regex style
1127 function glob_to_pcre ($glob) {
1128     $re = preg_replace('/\./', '\\.', $glob);
1129     $re = preg_replace(array('/\*/','/\?/'), array('.*','.'), $glob);
1130     if (!preg_match('/^[\?\*]/',$glob))
1131         $re = '^' . $re;
1132     if (!preg_match('/[\?\*]$/',$glob))
1133         $re = $re . '$';
1134     return $re;
1135 }
1136
1137 function glob_match ($glob, $against, $case_sensitive = true) {
1138     return preg_match('/' . glob_to_pcre($glob) . ($case_sensitive ? '/' : '/i'), $against);
1139 }
1140
1141 function explodeList($input, $allnames, $glob_style = true, $case_sensitive = true) {
1142     $list = explode(',',$input);
1143     // expand wildcards from list of $allnames
1144     if (preg_match('/[\?\*]/',$input)) {
1145         for ($i = 0; $i < sizeof($list); $i++) {
1146             $f = $list[$i];
1147             if (preg_match('/[\?\*]/',$f)) {
1148                 reset($allnames);
1149                 $expand = new ListRegexExpand(&$list, $glob_style ? glob_to_pcre($f) : $f, $case_sensitive);
1150                 $expand->expandRegex($i, &$allnames);
1151             }
1152         }
1153     }
1154     return $list;
1155 }
1156
1157 // echo implode(":",explodeList("Test*",array("xx","Test1","Test2")));
1158
1159 function explodePageList($input, $perm = false) {
1160     // expand wildcards from list of all pages
1161     if (preg_match('/[\?\*]/',$input)) {
1162         $dbi = $GLOBALS['request']->_dbi;
1163         $allPagehandles = $dbi->getAllPages($perm);
1164         while ($pagehandle = $allPagehandles->next()) {
1165             $allPages[] = $pagehandle->getName();
1166         }
1167         return explodeList($input, &$allPages);
1168     } else {
1169         return explode(',',$input);
1170     }
1171 }
1172
1173 // Class introspections
1174
1175 /** Determine whether object is of a specified type.
1176  *
1177  * @param $object object An object.
1178  * @param $class string Class name.
1179  * @return bool True iff $object is a $class
1180  * or a sub-type of $class. 
1181  */
1182 function isa ($object, $class) 
1183 {
1184     $lclass = strtolower($class);
1185
1186     return is_object($object)
1187         && ( get_class($object) == strtolower($lclass)
1188              || is_subclass_of($object, $lclass) );
1189 }
1190
1191 /** Determine whether (possible) object has method.
1192  *
1193  * @param $object mixed Object
1194  * @param $method string Method name
1195  * @return bool True iff $object is an object with has method $method.
1196  */
1197 function can ($object, $method) 
1198 {
1199     return is_object($object) && method_exists($object, strtolower($method));
1200 }
1201
1202 /**
1203  * Seed the random number generator.
1204  *
1205  * better_srand() ensures the randomizer is seeded only once.
1206  * 
1207  * How random do you want it? See:
1208  * http://www.php.net/manual/en/function.srand.php
1209  * http://www.php.net/manual/en/function.mt-srand.php
1210  */
1211 function better_srand($seed = '') {
1212     static $wascalled = FALSE;
1213     if (!$wascalled) {
1214         $seed = $seed === '' ? (double) microtime() * 1000000 : $seed;
1215         srand($seed);
1216         $wascalled = TRUE;
1217         //trigger_error("new random seed", E_USER_NOTICE); //debugging
1218     }
1219 }
1220
1221 /**
1222  * Recursively count all non-empty elements 
1223  * in array of any dimension or mixed - i.e. 
1224  * array('1' => 2, '2' => array('1' => 3, '2' => 4))
1225  * See http://www.php.net/manual/en/function.count.php
1226  */
1227 function count_all($arg) {
1228     // skip if argument is empty
1229     if ($arg) {
1230         //print_r($arg); //debugging
1231         $count = 0;
1232         // not an array, return 1 (base case) 
1233         if(!is_array($arg))
1234             return 1;
1235         // else call recursively for all elements $arg
1236         foreach($arg as $key => $val)
1237             $count += count_all($val);
1238         return $count;
1239     }
1240 }
1241
1242 function isSubPage($pagename) {
1243     return (strstr($pagename, SUBPAGE_SEPARATOR));
1244 }
1245
1246 function subPageSlice($pagename, $pos) {
1247     $pages = explode(SUBPAGE_SEPARATOR,$pagename);
1248     $pages = array_slice($pages,$pos,1);
1249     return $pages[0];
1250 }
1251
1252 // $Log: not supported by cvs2svn $
1253 // Revision 1.132  2003/01/04 22:19:43  carstenklapp
1254 // Bugfix UnfoldSubpages: "Undefined offset: 1" error when plugin invoked
1255 // on a page with no subpages (explodeList(): array 0-based, sizeof 1-based).
1256 //
1257
1258 // (c-file-style: "gnu")
1259 // Local Variables:
1260 // mode: php
1261 // tab-width: 8
1262 // c-basic-offset: 4
1263 // c-hanging-comment-ender-p: nil
1264 // indent-tabs-mode: nil
1265 // End:   
1266 ?>