]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/WikiTheme.php
Remove commented code
[SourceForge/phpwiki.git] / lib / WikiTheme.php
1 <?php
2 /* Copyright (C) 2002,2004,2005,2006,2008,2009,2010 $ThePhpWikiProgrammingTeam
3  *
4  * This file is part of PhpWiki.
5  *
6  * PhpWiki is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * PhpWiki is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License along
17  * with PhpWiki; if not, write to the Free Software Foundation, Inc.,
18  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19  */
20
21 /**
22  * Customize output by themes: templates, css, special links functions,
23  * and more formatting.
24  */
25
26 /**
27  * Make a link to a wiki page (in this wiki).
28  *
29  * This is a convenience function.
30  *
31  * @param mixed $page_or_rev
32  * Can be:<dl>
33  * <dt>A string</dt><dd>The page to link to.</dd>
34  * <dt>A WikiDB_Page object</dt><dd>The page to link to.</dd>
35  * <dt>A WikiDB_PageRevision object</dt><dd>A specific version of the page to link to.</dd>
36  * </dl>
37  *
38  * @param string $type
39  * One of:<dl>
40  * <dt>'unknown'</dt><dd>Make link appropriate for a non-existant page.</dd>
41  * <dt>'known'</dt><dd>Make link appropriate for an existing page.</dd>
42  * <dt>'auto'</dt><dd>Either 'unknown' or 'known' as appropriate.</dd>
43  * <dt>'button'</dt><dd>Make a button-style link.</dd>
44  * <dt>'if_known'</dt><dd>Only linkify if page exists.</dd>
45  * </dl>
46  * Unless $type of of the latter form, the link will be of class 'wiki', 'wikiunknown',
47  * 'named-wiki', or 'named-wikiunknown', as appropriate.
48  *
49  * @param mixed $label (string or XmlContent object)
50  * Label for the link.  If not given, defaults to the page name.
51  *
52  * @return XmlContent The link
53  */
54 function WikiLink($page_or_rev, $type = 'known', $label = false)
55 {
56     global $WikiTheme, $request;
57
58     if ($type == 'button') {
59         return $WikiTheme->makeLinkButton($page_or_rev, $label);
60     }
61
62     $version = false;
63
64     if (isa($page_or_rev, 'WikiDB_PageRevision')) {
65         $version = $page_or_rev->getVersion();
66         if ($page_or_rev->isCurrent())
67             $version = false;
68         $page = $page_or_rev->getPage();
69         $pagename = $page->getName();
70         $wikipage = $pagename;
71     } elseif (isa($page_or_rev, 'WikiDB_Page')) {
72         $page = $page_or_rev;
73         $pagename = $page->getName();
74         $wikipage = $pagename;
75     } elseif (isa($page_or_rev, 'WikiPageName')) {
76         $wikipage = $page_or_rev;
77         $pagename = $wikipage->name;
78         if (!$wikipage->isValid('strict'))
79             return $WikiTheme->linkBadWikiWord($wikipage, $label);
80     } else {
81         $wikipage = new WikiPageName($page_or_rev, $request->getPage());
82         $pagename = $wikipage->name;
83         if (!$wikipage->isValid('strict'))
84             return $WikiTheme->linkBadWikiWord($wikipage, $label);
85     }
86
87     if ($type == 'auto' or $type == 'if_known') {
88         if (isset($page)) {
89             $exists = $page->exists();
90         } else {
91             $dbi =& $request->_dbi;
92             $exists = $dbi->isWikiPage($wikipage->name);
93         }
94     } elseif ($type == 'unknown') {
95         $exists = false;
96     } else {
97         $exists = true;
98     }
99
100     // FIXME: this should be somewhere else, if really needed.
101     // WikiLink makes A link, not a string of fancy ones.
102     // (I think that the fancy split links are just confusing.)
103     // Todo: test external ImageLinks http://some/images/next.gif
104     if (isa($wikipage, 'WikiPageName') and
105         !$label and
106             strchr(substr($wikipage->shortName, 1), SUBPAGE_SEPARATOR)
107     ) {
108         $parts = explode(SUBPAGE_SEPARATOR, $wikipage->shortName);
109         $last_part = array_pop($parts);
110         $sep = '';
111         $link = HTML::span();
112         foreach ($parts as $part) {
113             $path[] = $part;
114             $parent = join(SUBPAGE_SEPARATOR, $path);
115             if ($WikiTheme->_autosplitWikiWords)
116                 $part = " " . $part;
117             if ($part)
118                 $link->pushContent($WikiTheme->linkExistingWikiWord($parent, $sep . $part));
119             $sep = $WikiTheme->_autosplitWikiWords
120                 ? ' ' . SUBPAGE_SEPARATOR : SUBPAGE_SEPARATOR;
121         }
122         if ($exists)
123             $link->pushContent($WikiTheme->linkExistingWikiWord($wikipage, $sep . $last_part,
124                 $version));
125         else
126             $link->pushContent($WikiTheme->linkUnknownWikiWord($wikipage, $sep . $last_part));
127         return $link;
128     }
129
130     if ($exists) {
131         return $WikiTheme->linkExistingWikiWord($wikipage, $label, $version);
132     } elseif ($type == 'if_known') {
133         if (!$label && isa($wikipage, 'WikiPageName'))
134             $label = $wikipage->shortName;
135         return HTML($label ? $label : $pagename);
136     } else {
137         return $WikiTheme->linkUnknownWikiWord($wikipage, $label);
138     }
139 }
140
141 /**
142  * Make a button.
143  *
144  * This is a convenience function.
145  *
146  * @param $action string
147  * One of <dl>
148  * <dt>[action]</dt><dd>Perform action (e.g. 'edit') on the selected page.</dd>
149  * <dt>[ActionPage]</dt><dd>Run the actionpage (e.g. 'BackLinks') on the selected page.</dd>
150  * <dt>'submit:'[name]</dt><dd>Make a form submission button with the given name.
151  *      ([name] can be blank for a nameless submit button.)</dd>
152  * <dt>a hash</dt><dd>Query args for the action. E.g.<pre>
153  *      array('action' => 'diff', 'previous' => 'author')
154  * </pre></dd>
155  * </dl>
156  *
157  * @param $label string
158  * A label for the button.  If ommited, a suitable default (based on the valued of $action)
159  * will be picked.
160  *
161  * @param $page_or_rev mixed
162  * Which page (& version) to perform the action on.
163  * Can be one of:<dl>
164  * <dt>A string</dt><dd>The pagename.</dd>
165  * <dt>A WikiDB_Page object</dt><dd>The page.</dd>
166  * <dt>A WikiDB_PageRevision object</dt><dd>A specific version of the page.</dd>
167  * </dl>
168  * ($Page_or_rev is ignored for submit buttons.)
169  */
170 function Button($action, $label = false, $page_or_rev = false, $options = false)
171 {
172     global $WikiTheme;
173
174     if (!is_array($action) && preg_match('/^submit:(.*)/', $action, $m))
175         return $WikiTheme->makeSubmitButton($label, $m[1], $page_or_rev, $options);
176     else
177         return $WikiTheme->makeActionButton($action, $label, $page_or_rev, $options);
178 }
179
180 class WikiTheme
181 {
182     var $HTML_DUMP_SUFFIX = '';
183     var $DUMP_MODE = false, $dumped_images, $dumped_css;
184
185     /**
186      * noinit: Do not initialize unnecessary items in default_theme fallback twice.
187      */
188     function WikiTheme($theme_name = 'default', $noinit = false)
189     {
190         $this->_name = $theme_name;
191         $this->_themes_dir = NormalizeLocalFileName("themes");
192         $this->_path = defined('PHPWIKI_DIR') ? NormalizeLocalFileName("") : "";
193         $this->_theme = "themes/$theme_name";
194         $this->_parents = array();
195
196         if ($theme_name != 'default') {
197             $parent = $this;
198             /* derived classes should search all parent classes */
199             while ($parent = get_parent_class($parent)) {
200                 if (strtolower($parent) == 'wikitheme') {
201                     $this->_default_theme = new WikiTheme('default', true);
202                     $this->_parents[] = $this->_default_theme;
203                 } elseif ($parent) {
204                     $this->_parents[] = new WikiTheme
205                     (preg_replace("/^WikiTheme_/i", "", $parent), true);
206                 }
207             }
208         }
209         if ($noinit) return;
210         $this->_css = array();
211
212         // on derived classes do not add headers twice
213         if (count($this->_parents) > 1) {
214             return;
215         }
216         $this->addMoreHeaders(JavaScript('', array('src' => $this->_findData("wikicommon.js"))));
217         if (!(defined('FUSIONFORGE') and FUSIONFORGE)) {
218             // FusionForge already loads this
219             $this->addMoreHeaders(JavaScript('', array('src' => $this->_findData("sortable.js"))));
220         }
221         // by pixels
222         if ((is_object($GLOBALS['request']) // guard against unittests
223             and $GLOBALS['request']->getPref('doubleClickEdit'))
224             or ENABLE_DOUBLECLICKEDIT
225         )
226             $this->initDoubleClickEdit();
227
228         // will be replaced by acDropDown
229         if (ENABLE_LIVESEARCH) { // by bitflux.ch
230             $this->initLiveSearch();
231         }
232         // replaces external LiveSearch
233         // enable ENABLE_AJAX for DynamicIncludePage
234         if (ENABLE_ACDROPDOWN or ENABLE_AJAX) {
235             $this->initMoAcDropDown();
236             if (ENABLE_AJAX and DEBUG) // minified all together
237                 $this->addMoreHeaders(JavaScript('', array('src' => $this->_findData("ajax.js"))));
238         }
239     }
240
241     function file($file)
242     {
243         return $this->_path . "$this->_theme/$file";
244     }
245
246     function _findFile($file, $missing_okay = false)
247     {
248         if (file_exists($this->file($file)))
249             return "$this->_theme/$file";
250
251         // FIXME: this is a short-term hack.  Delete this after all files
252         // get moved into themes/...
253         // Needed for button paths in parent themes
254         if (file_exists($this->_path . $file))
255             return $file;
256
257         /* Derived classes should search all parent classes */
258         foreach ($this->_parents as $parent) {
259             $path = $parent->_findFile($file, 1);
260             if ($path) {
261                 return $path;
262             } elseif (0 and DEBUG & (_DEBUG_VERBOSE + _DEBUG_REMOTE)) {
263                 trigger_error("$parent->_theme/$file: not found", E_USER_NOTICE);
264             }
265         }
266         if (isset($this->_default_theme)) {
267             return $this->_default_theme->_findFile($file, $missing_okay);
268         } elseif (!$missing_okay) {
269             trigger_error("$this->_theme/$file: not found", E_USER_NOTICE);
270             if (DEBUG & _DEBUG_TRACE) {
271                 echo "<pre>";
272                 printSimpleTrace(debug_backtrace());
273                 echo "</pre>\n";
274             }
275         }
276         return false;
277     }
278
279     function _findData($file, $missing_okay = false)
280     {
281         if (!string_starts_with($file, "themes")) { // common case
282             $path = $this->_findFile($file, $missing_okay);
283         } else {
284             // _findButton only
285             if (file_exists($file)) {
286                 $path = $file;
287             } elseif (defined('DATA_PATH')
288                 and file_exists(DATA_PATH . "/$file")
289             ) {
290                 $path = $file;
291             } else { // fallback for buttons in parent themes
292                 $path = $this->_findFile($file, $missing_okay);
293             }
294         }
295         if (!$path)
296             return false;
297         if (!DEBUG) {
298             $min = preg_replace("/\.(css|js)$/", "-min.\\1", $file);
299             if ($min and ($x = $this->_findFile($min, true))) $path = $x;
300         }
301
302         if (defined('DATA_PATH'))
303             return DATA_PATH . "/$path";
304         return $path;
305     }
306
307     ////////////////////////////////////////////////////////////////
308     //
309     // Date and Time formatting
310     //
311     ////////////////////////////////////////////////////////////////
312
313     // Note:  Windows' implementation of strftime does not include certain
314     // format specifiers, such as %e (for date without leading zeros).  In
315     // general, see:
316     // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vclib/html/_crt_strftime.2c_.wcsftime.asp
317     // As a result, we have to use %d, and strip out leading zeros ourselves.
318
319     var $_dateFormat = "%B %d, %Y";
320     var $_timeFormat = "%I:%M %p";
321
322     var $_showModTime = true;
323
324     /**
325      * Set format string used for dates.
326      *
327      * @param $fs string Format string for dates.
328      *
329      * @param $show_mod_time bool If true (default) then times
330      * are included in the messages generated by getLastModifiedMessage(),
331      * otherwise, only the date of last modification will be shown.
332      */
333     function setDateFormat($fs, $show_mod_time = true)
334     {
335         $this->_dateFormat = $fs;
336         $this->_showModTime = $show_mod_time;
337     }
338
339     /**
340      * Set format string used for times.
341      *
342      * @param $fs string Format string for times.
343      */
344     function setTimeFormat($fs)
345     {
346         $this->_timeFormat = $fs;
347     }
348
349     /**
350      * Format a date.
351      *
352      * Any time zone offset specified in the users preferences is
353      * taken into account by this method.
354      *
355      * @param $time_t integer Unix-style time.
356      *
357      * @return string The date.
358      */
359     function formatDate($time_t)
360     {
361         global $request;
362
363         $offset_time = $time_t + 3600 * $request->getPref('timeOffset');
364         // strip leading zeros from date elements (ie space followed by zero
365         // or leading 0 as in French "09 mai 2009")
366         return preg_replace('/ 0/', ' ', preg_replace('/^0/', ' ',
367             strftime($this->_dateFormat, $offset_time)));
368     }
369
370     /**
371      * Format a date.
372      *
373      * Any time zone offset specified in the users preferences is
374      * taken into account by this method.
375      *
376      * @param $time_t integer Unix-style time.
377      *
378      * @return string The time.
379      */
380     function formatTime($time_t)
381     {
382         //FIXME: make 24-hour mode configurable?
383         global $request;
384         $offset_time = $time_t + 3600 * $request->getPref('timeOffset');
385         return preg_replace('/^0/', ' ',
386             strtolower(strftime($this->_timeFormat, $offset_time)));
387     }
388
389     /**
390      * Format a date and time.
391      *
392      * Any time zone offset specified in the users preferences is
393      * taken into account by this method.
394      *
395      * @param $time_t integer Unix-style time.
396      *
397      * @return string The date and time.
398      */
399     function formatDateTime($time_t)
400     {
401         if ($time_t == 0) {
402             // Do not display "01 January 1970 1:00" for nonexistent pages
403             return "";
404         } else {
405             return $this->formatDate($time_t) . ' ' . $this->formatTime($time_t);
406         }
407     }
408
409     /**
410      * Format a (possibly relative) date.
411      *
412      * If enabled in the users preferences, this method might
413      * return a relative day (e.g. 'Today', 'Yesterday').
414      *
415      * Any time zone offset specified in the users preferences is
416      * taken into account by this method.
417      *
418      * @param $time_t integer Unix-style time.
419      *
420      * @return string The day.
421      */
422     function getDay($time_t)
423     {
424         global $request;
425
426         if ($request->getPref('relativeDates') && ($date = $this->_relativeDay($time_t))) {
427             return ucfirst($date);
428         }
429         return $this->formatDate($time_t);
430     }
431
432     /**
433      * Format the "last modified" message for a page revision.
434      *
435      * @param $revision object A WikiDB_PageRevision object.
436      *
437      * @param $show_version bool Should the page version number
438      * be included in the message.  (If this argument is omitted,
439      * then the version number will be shown only iff the revision
440      * is not the current one.
441      *
442      * @return string The "last modified" message.
443      */
444     function getLastModifiedMessage($revision, $show_version = 'auto')
445     {
446         global $request;
447         if (!$revision) return '';
448
449         // dates >= this are considered invalid.
450         if (!defined('EPOCH'))
451             define('EPOCH', 0); // seconds since ~ January 1 1970
452
453         $mtime = $revision->get('mtime');
454         if ($mtime <= EPOCH)
455             return _("Never edited");
456
457         if ($show_version == 'auto')
458             $show_version = !$revision->isCurrent();
459
460         if ($request->getPref('relativeDates') && ($date = $this->_relativeDay($mtime))) {
461             if ($this->_showModTime)
462                 $date = sprintf(_("%s at %s"),
463                     $date, $this->formatTime($mtime));
464
465             if ($show_version)
466                 return fmt("Version %s, saved on %s", $revision->getVersion(), $date);
467             else
468                 return fmt("Last edited on %s", $date);
469         }
470
471         if ($this->_showModTime)
472             $date = $this->formatDateTime($mtime);
473         else
474             $date = $this->formatDate($mtime);
475
476         if ($show_version)
477             return fmt("Version %s, saved on %s", $revision->getVersion(), $date);
478         else
479             return fmt("Last edited on %s", $date);
480     }
481
482     function _relativeDay($time_t)
483     {
484         global $request;
485
486         if (is_numeric($request->getPref('timeOffset')))
487             $offset = 3600 * $request->getPref('timeOffset');
488         else
489             $offset = 0;
490
491         $now = time() + $offset;
492         $today = localtime($now, true);
493         $time = localtime($time_t + $offset, true);
494
495         if ($time['tm_yday'] == $today['tm_yday'] && $time['tm_year'] == $today['tm_year'])
496             return _("today");
497
498         // Note that due to daylight savings chages (and leap seconds), $now minus
499         // 24 hours is not guaranteed to be yesterday.
500         $yesterday = localtime($now - (12 + $today['tm_hour']) * 3600, true);
501         if ($time['tm_yday'] == $yesterday['tm_yday']
502             and $time['tm_year'] == $yesterday['tm_year']
503         )
504             return _("yesterday");
505
506         return false;
507     }
508
509     /**
510      * Format the "Author" and "Owner" messages for a page revision.
511      */
512     function getOwnerMessage($page)
513     {
514         if (!ENABLE_PAGEPERM or !class_exists("PagePermission"))
515             return '';
516         $dbi =& $GLOBALS['request']->_dbi;
517         $owner = $page->getOwner();
518         if ($owner) {
519             /*
520             if ( mayAccessPage('change',$page->getName()) )
521                 return fmt("Owner: %s", $this->makeActionButton(array('action'=>_("chown"),
522                                                                       's' => $page->getName()),
523                                                                 $owner, $page));
524             */
525             if ($dbi->isWikiPage($owner))
526                 return fmt("Owner: %s", WikiLink($owner));
527             else
528                 return fmt("Owner: %s", '"' . $owner . '"');
529         }
530         return '';
531     }
532
533     /* New behaviour: (by Matt Brown)
534        Prefer author (name) over internal author_id (IP) */
535     function getAuthorMessage($revision)
536     {
537         if (!$revision) return '';
538         $dbi =& $GLOBALS['request']->_dbi;
539         $author = $revision->get('author');
540         if (!$author) $author = $revision->get('author_id');
541         if (!$author) return '';
542         if ($dbi->isWikiPage($author)) {
543             return fmt("by %s", WikiLink($author));
544         } else {
545             return fmt("by %s", '"' . $author . '"');
546         }
547     }
548
549     ////////////////////////////////////////////////////////////////
550     //
551     // Hooks for other formatting
552     //
553     ////////////////////////////////////////////////////////////////
554
555     //FIXME: PHP 4.1 Warnings
556     //lib/WikiTheme.php:84: Notice[8]: The call_user_method() function is deprecated,
557     //use the call_user_func variety with the array(&$obj, "method") syntax instead
558
559     function getFormatter($type, $format)
560     {
561         $method = strtolower("get${type}Formatter");
562         if (method_exists($this, $method))
563             return $this->{$method}($format);
564         return false;
565     }
566
567     ////////////////////////////////////////////////////////////////
568     //
569     // Links
570     //
571     ////////////////////////////////////////////////////////////////
572
573     var $_autosplitWikiWords = false;
574
575     function setAutosplitWikiWords($autosplit = true)
576     {
577         $this->_autosplitWikiWords = $autosplit ? true : false;
578     }
579
580     function maybeSplitWikiWord($wikiword)
581     {
582         if ($this->_autosplitWikiWords)
583             return SplitPagename($wikiword);
584         else
585             return $wikiword;
586     }
587
588     var $_anonEditUnknownLinks = true;
589
590     function setAnonEditUnknownLinks($anonedit = true)
591     {
592         $this->_anonEditUnknownLinks = $anonedit ? true : false;
593     }
594
595     function linkExistingWikiWord($wikiword, $linktext = '', $version = false)
596     {
597         global $request;
598
599         if ($version !== false and !$this->HTML_DUMP_SUFFIX)
600             $url = WikiURL($wikiword, array('version' => $version));
601         else
602             $url = WikiURL($wikiword);
603
604         // Extra steps for dumping page to an html file.
605         if ($this->HTML_DUMP_SUFFIX) {
606             $url = preg_replace('/^\./', '%2e', $url); // dot pages
607         }
608
609         $link = HTML::a(array('href' => $url));
610
611         if (isa($wikiword, 'WikiPageName'))
612             $default_text = $wikiword->shortName;
613         else
614             $default_text = $wikiword;
615
616         if (!empty($linktext)) {
617             $link->pushContent($linktext);
618             $link->setAttr('class', 'named-wiki');
619             $link->setAttr('title', $this->maybeSplitWikiWord($default_text));
620         } else {
621             $link->pushContent($this->maybeSplitWikiWord($default_text));
622             $link->setAttr('class', 'wiki');
623         }
624         if ($request->getArg('frame'))
625             $link->setAttr('target', '_top');
626         return $link;
627     }
628
629     function linkUnknownWikiWord($wikiword, $linktext = '')
630     {
631         global $request;
632
633         // Get rid of anchors on unknown wikiwords
634         if (isa($wikiword, 'WikiPageName')) {
635             $default_text = $wikiword->shortName;
636             $wikiword = $wikiword->name;
637         } else {
638             $default_text = $wikiword;
639         }
640
641         if ($this->DUMP_MODE) { // HTML, PDF or XML
642             $link = HTML::span(empty($linktext) ? $wikiword : $linktext);
643             $link->setAttr('style', 'text-decoration: underline');
644             $link->addTooltip(sprintf(_("Empty link to: %s"), $wikiword));
645             $link->setAttr('class', empty($linktext) ? 'wikiunknown' : 'named-wikiunknown');
646             return $link;
647         } else {
648             // if AnonEditUnknownLinks show "?" only users which are allowed to edit this page
649             if (!$this->_anonEditUnknownLinks and
650                 (!$request->_user->isSignedIn()
651                     or !mayAccessPage('edit', $request->getArg('pagename')))
652             ) {
653                 $text = HTML::span(empty($linktext) ? $wikiword : $linktext);
654                 $text->setAttr('class', empty($linktext) ? 'wikiunknown' : 'named-wikiunknown');
655                 return $text;
656             } else {
657                 $url = WikiURL($wikiword, array('action' => 'create'));
658                 $button = $this->makeButton('?', $url);
659                 $button->addTooltip(sprintf(_("Create: %s"), $wikiword));
660             }
661         }
662
663         $link = HTML::span();
664         if (!empty($linktext)) {
665             $link->pushContent(HTML::span($linktext));
666             $link->setAttr('style', 'text-decoration: underline');
667             $link->setAttr('class', 'named-wikiunknown');
668         } else {
669             $link->pushContent(HTML::span($this->maybeSplitWikiWord($default_text)));
670             $link->setAttr('style', 'text-decoration: underline');
671             $link->setAttr('class', 'wikiunknown');
672         }
673         if (!isa($button, "ImageButton"))
674             $button->setAttr('rel', 'nofollow');
675         $link->pushContent($button);
676         if ($request->getPref('googleLink')) {
677             $gbutton = $this->makeButton('G', "http://www.google.com/search?q="
678                 . urlencode($wikiword));
679             $gbutton->addTooltip(sprintf(_("Google:%s"), $wikiword));
680             $link->pushContent($gbutton);
681         }
682         if ($request->getArg('frame'))
683             $link->setAttr('target', '_top');
684
685         return $link;
686     }
687
688     function linkBadWikiWord($wikiword, $linktext = '')
689     {
690         global $ErrorManager;
691
692         if ($linktext) {
693             $text = $linktext;
694         } elseif (isa($wikiword, 'WikiPageName')) {
695             $text = $wikiword->shortName;
696         } else {
697             $text = $wikiword;
698         }
699
700         if (isa($wikiword, 'WikiPageName'))
701             $message = $wikiword->getWarnings();
702         else
703             $message = sprintf(_("“%s”: Bad page name"), $wikiword);
704         $ErrorManager->warning($message);
705
706         return HTML::span(array('class' => 'badwikiword'), $text);
707     }
708
709     ////////////////////////////////////////////////////////////////
710     //
711     // Images and Icons
712     //
713     ////////////////////////////////////////////////////////////////
714     var $_imageAliases = array();
715
716     /**
717      *
718      * (To disable an image, alias the image to <code>false</code>.
719      */
720     function addImageAlias($alias, $image_name)
721     {
722         // fall back to the PhpWiki-supplied image if not found
723         if ((empty($this->_imageAliases[$alias])
724             and $this->_findFile("images/$image_name", true))
725             or $image_name === false
726         )
727             $this->_imageAliases[$alias] = $image_name;
728     }
729
730     function getImageURL($image)
731     {
732         $aliases = &$this->_imageAliases;
733
734         if (isset($aliases[$image])) {
735             $image = $aliases[$image];
736             if (!$image)
737                 return false;
738         }
739
740         // If not extension, default to .png.
741         if (!preg_match('/\.\w+$/', $image))
742             $image .= '.png';
743
744         // FIXME: this should probably be made to fall back
745         //        automatically to .gif, .jpg.
746         //        Also try .gif before .png if browser doesn't like png.
747
748         $path = $this->_findData("images/$image", 'missing okay');
749         if (!$path) // search explicit images/ or button/ links also
750             $path = $this->_findData("$image", 'missing okay');
751
752         if ($this->DUMP_MODE) {
753             if (empty($this->dumped_images)) $this->dumped_images = array();
754             $path = "images/" . basename($path);
755             if (!in_array($path, $this->dumped_images))
756                 $this->dumped_images[] = $path;
757         }
758         return $path;
759     }
760
761     function setLinkIcon($proto, $image = false)
762     {
763         if (!$image)
764             $image = $proto;
765
766         $this->_linkIcons[$proto] = $image;
767     }
768
769     function getLinkIconURL($proto)
770     {
771         $icons = &$this->_linkIcons;
772         if (!empty($icons[$proto]))
773             return $this->getImageURL($icons[$proto]);
774         elseif (!empty($icons['*']))
775             return $this->getImageURL($icons['*']);
776         return false;
777     }
778
779     var $_linkIcon = 'front'; // or 'after' or 'no'.
780     // maybe also 'spanall': there is a scheme currently in effect with front, which
781     // spans the icon only to the first, to let the next words wrap on line breaks
782     // see stdlib.php:PossiblyGlueIconToText()
783     function getLinkIconAttr()
784     {
785         return $this->_linkIcon;
786     }
787
788     function setLinkIconAttr($where)
789     {
790         $this->_linkIcon = $where;
791     }
792
793     function addButtonAlias($text, $alias = false)
794     {
795         $aliases = &$this->_buttonAliases;
796
797         if (is_array($text))
798             $aliases = array_merge($aliases, $text);
799         elseif ($alias === false)
800             unset($aliases[$text]); else
801             $aliases[$text] = $alias;
802     }
803
804     function getButtonURL($text)
805     {
806         $aliases = &$this->_buttonAliases;
807         if (isset($aliases[$text]))
808             $text = $aliases[$text];
809
810         $qtext = urlencode($text);
811         $url = $this->_findButton("$qtext.png");
812         if ($url && strstr($url, '%')) {
813             $url = preg_replace('|([^/]+)$|e', 'urlencode("\\1")', $url);
814         }
815         if (!$url) { // Jeff complained about png not supported everywhere.
816             // This was not PC until 2005.
817             $url = $this->_findButton("$qtext.gif");
818             if ($url && strstr($url, '%')) {
819                 $url = preg_replace('|([^/]+)$|e', 'urlencode("\\1")', $url);
820             }
821         }
822         if ($url and $this->DUMP_MODE) {
823             if (empty($this->dumped_buttons)) $this->dumped_buttons = array();
824             $file = $url;
825             if (defined('DATA_PATH'))
826                 $file = substr($url, strlen(DATA_PATH) + 1);
827             $url = "images/buttons/" . basename($file);
828             if (!array_key_exists($text, $this->dumped_buttons))
829                 $this->dumped_buttons[$text] = $file;
830         }
831         return $url;
832     }
833
834     function _findButton($button_file)
835     {
836         if (empty($this->_button_path))
837             $this->_button_path = $this->_getButtonPath();
838
839         foreach ($this->_button_path as $dir) {
840             if ($path = $this->_findData("$dir/$button_file", 1))
841                 return $path;
842         }
843         return false;
844     }
845
846     function _getButtonPath()
847     {
848         $button_dir = $this->_findFile("buttons");
849         $path_dir = $this->_path . $button_dir;
850         if (!file_exists($path_dir) || !is_dir($path_dir))
851             return array();
852         $path = array($button_dir);
853
854         $dir = dir($path_dir);
855         while (($subdir = $dir->read()) !== false) {
856             if ($subdir[0] == '.')
857                 continue;
858             if ($subdir == 'CVS')
859                 continue;
860             if (is_dir("$path_dir/$subdir"))
861                 $path[] = "$button_dir/$subdir";
862         }
863         $dir->close();
864         // add default buttons
865         $path[] = "themes/default/buttons";
866         $path_dir = $this->_path . "themes/default/buttons";
867         $dir = dir($path_dir);
868         while (($subdir = $dir->read()) !== false) {
869             if ($subdir[0] == '.')
870                 continue;
871             if ($subdir == 'CVS')
872                 continue;
873             if (is_dir("$path_dir/$subdir"))
874                 $path[] = "themes/default/buttons/$subdir";
875         }
876         $dir->close();
877
878         return $path;
879     }
880
881     ////////////////////////////////////////////////////////////////
882     //
883     // Button style
884     //
885     ////////////////////////////////////////////////////////////////
886
887     function makeButton($text, $url, $class = false, $options = false)
888     {
889         // FIXME: don't always try for image button?
890
891         // Special case: URLs like 'submit:preview' generate form
892         // submission buttons.
893         if (preg_match('/^submit:(.*)$/', $url, $m))
894             return $this->makeSubmitButton($text, $m[1], $class, $options);
895
896         if (is_string($text))
897             $imgurl = $this->getButtonURL($text);
898         else
899             $imgurl = $text;
900         if ($imgurl)
901             return new ImageButton($text, $url,
902                 in_array($class, array("wikiaction", "wikiadmin")) ? "wikibutton" : $class,
903                 $imgurl, $options);
904         else
905             return new Button($this->maybeSplitWikiWord($text), $url,
906                 $class, $options);
907     }
908
909     function makeSubmitButton($text, $name, $class = false, $options = false)
910     {
911         $imgurl = $this->getButtonURL($text);
912
913         if ($imgurl)
914             return new SubmitImageButton($text, $name, !$class ? "wikibutton" : $class, $imgurl, $options);
915         else
916             return new SubmitButton($text, $name, $class, $options);
917     }
918
919     /**
920      * Make button to perform action.
921      *
922      * This constructs a button which performs an action on the
923      * currently selected version of the current page.
924      * (Or anotherpage or version, if you want...)
925      *
926      * @param $action string The action to perform (e.g. 'edit', 'lock').
927      * This can also be the name of an "action page" like 'LikePages'.
928      * Alternatively you can give a hash of query args to be applied
929      * to the page.
930      *
931      * @param $label string Textual label for the button.  If left empty,
932      * a suitable name will be guessed.
933      *
934      * @param $page_or_rev mixed  The page to link to.  This can be
935      * given as a string (the page name), a WikiDB_Page object, or as
936      * WikiDB_PageRevision object.  If given as a WikiDB_PageRevision
937      * object, the button will link to a specific version of the
938      * designated page, otherwise the button links to the most recent
939      * version of the page.
940      *
941      * @return object A Button object.
942      */
943     function makeActionButton($action, $label = false,
944                               $page_or_rev = false, $options = false)
945     {
946         extract($this->_get_name_and_rev($page_or_rev));
947
948         if (is_array($action)) {
949             $attr = $action;
950             $action = isset($attr['action']) ? $attr['action'] : 'browse';
951         } else
952             $attr['action'] = $action;
953
954         $class = is_safe_action($action) ? 'wikiaction' : 'wikiadmin';
955         if (!$label)
956             $label = $this->_labelForAction($action);
957
958         if ($version)
959             $attr['version'] = $version;
960
961         if ($action == 'browse')
962             unset($attr['action']);
963
964         $options = $this->fixAccesskey($options);
965
966         return $this->makeButton($label, WikiURL($pagename, $attr), $class, $options);
967     }
968
969     function tooltipAccessKeyPrefix()
970     {
971         static $tooltipAccessKeyPrefix = null;
972         if ($tooltipAccessKeyPrefix) return $tooltipAccessKeyPrefix;
973
974         $tooltipAccessKeyPrefix = 'alt';
975         if (isBrowserOpera()) $tooltipAccessKeyPrefix = 'shift-esc';
976         elseif (isBrowserSafari() or browserDetect("Mac") or isBrowserKonqueror())
977             $tooltipAccessKeyPrefix = 'ctrl'; // ff2 win and x11 only
978         elseif ((browserDetect("firefox/2") or browserDetect("minefield/3") or browserDetect("SeaMonkey/1.1"))
979             and ((browserDetect("windows") or browserDetect("x11")))
980         )
981             $tooltipAccessKeyPrefix = 'alt-shift';
982         return $tooltipAccessKeyPrefix;
983     }
984
985     /** Define the accesskey in the title only, with ending [p] or [alt-p].
986      *  This fixes the prefix in the title and sets the accesskey.
987      */
988     function fixAccesskey($attrs)
989     {
990         if (!empty($attrs['title']) and preg_match("/\[(alt-)?(.)\]$/", $attrs['title'], $m)) {
991             if (empty($attrs['accesskey'])) $attrs['accesskey'] = $m[2];
992             // firefox 'alt-shift', MSIE: 'alt', ... see wikibits.js
993             $attrs['title'] = preg_replace("/\[(alt-)?(.)\]$/", "[" . $this->tooltipAccessKeyPrefix() . "-\\2]", $attrs['title']);
994         }
995         return $attrs;
996     }
997
998     /**
999      * Make a "button" which links to a wiki-page.
1000      *
1001      * These are really just regular WikiLinks, possibly
1002      * disguised (e.g. behind an image button) by the theme.
1003      *
1004      * This method should probably only be used for links
1005      * which appear in page navigation bars, or similar places.
1006      *
1007      * Use linkExistingWikiWord, or LinkWikiWord for normal links.
1008      *
1009      * @param $page_or_rev mixed The page to link to.  This can be
1010      * given as a string (the page name), a WikiDB_Page object, or as
1011      * WikiDB_PageRevision object.  If given as a WikiDB_PageRevision
1012      * object, the button will link to a specific version of the
1013      * designated page, otherwise the button links to the most recent
1014      * version of the page.
1015      *
1016      * @return object A Button object.
1017      */
1018     function makeLinkButton($page_or_rev, $label = false, $action = false)
1019     {
1020         extract($this->_get_name_and_rev($page_or_rev));
1021
1022         $args = $version ? array('version' => $version) : false;
1023         if ($action) $args['action'] = $action;
1024
1025         return $this->makeButton($label ? $label : $pagename,
1026             WikiURL($pagename, $args), 'wiki');
1027     }
1028
1029     function _get_name_and_rev($page_or_rev)
1030     {
1031         $version = false;
1032
1033         if (empty($page_or_rev)) {
1034             global $request;
1035             $pagename = $request->getArg("pagename");
1036             $version = $request->getArg("version");
1037         } elseif (is_object($page_or_rev)) {
1038             if (isa($page_or_rev, 'WikiDB_PageRevision')) {
1039                 $rev = $page_or_rev;
1040                 $page = $rev->getPage();
1041                 if (!$rev->isCurrent()) $version = $rev->getVersion();
1042             } else {
1043                 $page = $page_or_rev;
1044             }
1045             $pagename = $page->getName();
1046         } else {
1047             $pagename = (string)$page_or_rev;
1048         }
1049         return compact('pagename', 'version');
1050     }
1051
1052     function _labelForAction($action)
1053     {
1054         switch ($action) {
1055             case 'edit':
1056                 return _("Edit");
1057             case 'diff':
1058                 return _("Diff");
1059             case 'logout':
1060                 return _("Sign Out");
1061             case 'login':
1062                 return _("Sign In");
1063             case 'rename':
1064                 return _("Rename Page");
1065             case 'lock':
1066                 return _("Lock Page");
1067             case 'unlock':
1068                 return _("Unlock Page");
1069             case 'remove':
1070                 return _("Remove Page");
1071             case 'purge':
1072                 return _("Purge Page");
1073             default:
1074                 // I don't think the rest of these actually get used.
1075                 // 'setprefs'
1076                 // 'upload' 'dumpserial' 'loadfile' 'zip'
1077                 // 'save' 'browse'
1078                 return gettext(ucfirst($action));
1079         }
1080     }
1081
1082     //----------------------------------------------------------------
1083     var $_buttonSeparator = "\n | ";
1084
1085     function setButtonSeparator($separator)
1086     {
1087         $this->_buttonSeparator = $separator;
1088     }
1089
1090     function getButtonSeparator()
1091     {
1092         return $this->_buttonSeparator;
1093     }
1094
1095     ////////////////////////////////////////////////////////////////
1096     //
1097     // CSS
1098     //
1099     // Notes:
1100     //
1101     // Based on testing with Galeon 1.2.7 (Mozilla 1.2):
1102     // Automatic media-based style selection (via <link> tags) only
1103     // seems to work for the default style, not for alternate styles.
1104     //
1105     // Doing
1106     //
1107     //  <link rel="stylesheet" type="text/css" href="phpwiki.css" />
1108     //  <link rel="stylesheet" type="text/css" href="phpwiki-printer.css" media="print" />
1109     //
1110     // works to make it so that the printer style sheet get used
1111     // automatically when printing (or print-previewing) a page
1112     // (but when only when the default style is selected.)
1113     //
1114     // Attempts like:
1115     //
1116     //  <link rel="alternate stylesheet" title="Modern"
1117     //        type="text/css" href="phpwiki-modern.css" />
1118     //  <link rel="alternate stylesheet" title="Modern"
1119     //        type="text/css" href="phpwiki-printer.css" media="print" />
1120     //
1121     // Result in two "Modern" choices when trying to select alternate style.
1122     // If one selects the first of those choices, one gets phpwiki-modern
1123     // both when browsing and printing.  If one selects the second "Modern",
1124     // one gets no CSS when browsing, and phpwiki-printer when printing.
1125     //
1126     // The Real Fix?
1127     // =============
1128     //
1129     // We should probably move to doing the media based style
1130     // switching in the CSS files themselves using, e.g.:
1131     //
1132     //  @import url(print.css) print;
1133     //
1134     ////////////////////////////////////////////////////////////////
1135
1136     function _CSSlink($title, $css_file, $media, $is_alt = false)
1137     {
1138         // Don't set title on default style.  This makes it clear to
1139         // the user which is the default (i.e. most supported) style.
1140         if ($is_alt and isBrowserKonqueror())
1141             return HTML();
1142         $link = HTML::link(array('rel' => $is_alt ? 'alternate stylesheet' : 'stylesheet',
1143             'type' => 'text/css',
1144             'charset' => $GLOBALS['charset'],
1145             'href' => $this->_findData($css_file)));
1146         if ($is_alt)
1147             $link->setAttr('title', $title);
1148
1149         if ($media)
1150             $link->setAttr('media', $media);
1151         if ($this->DUMP_MODE) {
1152             if (empty($this->dumped_css)) $this->dumped_css = array();
1153             if (!in_array($css_file, $this->dumped_css)) $this->dumped_css[] = $css_file;
1154             $link->setAttr('href', basename($link->getAttr('href')));
1155         }
1156
1157         return $link;
1158     }
1159
1160     /** Set default CSS source for this theme.
1161      *
1162      * To set styles to be used for different media, pass a
1163      * hash for the second argument, e.g.
1164      *
1165      * $theme->setDefaultCSS('default', array('' => 'normal.css',
1166      *                                        'print' => 'printer.css'));
1167      *
1168      * If you call this more than once, the last one called takes
1169      * precedence as the default style.
1170      *
1171      * @param string $title Name of style (currently ignored, unless
1172      * you call this more than once, in which case, some of the style
1173      * will become alternate (rather than default) styles, and then their
1174      * titles will be used.
1175      *
1176      * @param mixed $css_files Name of CSS file, or hash containing a mapping
1177      * between media types and CSS file names.  Use a key of '' (the empty string)
1178      * to set the default CSS for non-specified media.  (See above for an example.)
1179      */
1180     function setDefaultCSS($title, $css_files)
1181     {
1182         if (!is_array($css_files))
1183             $css_files = array('' => $css_files);
1184         // Add to the front of $this->_css
1185         unset($this->_css[$title]);
1186         $this->_css = array_merge(array($title => $css_files), $this->_css);
1187     }
1188
1189     /** Set alternate CSS source for this theme.
1190      *
1191      * @param string $title     Name of style.
1192      * @param string $css_files Name of CSS file.
1193      */
1194     function addAlternateCSS($title, $css_files)
1195     {
1196         if (!is_array($css_files))
1197             $css_files = array('' => $css_files);
1198         $this->_css[$title] = $css_files;
1199     }
1200
1201     /**
1202      * @return string HTML for CSS.
1203      */
1204     function getCSS()
1205     {
1206         $css = array();
1207         $is_alt = false;
1208         foreach ($this->_css as $title => $css_files) {
1209             ksort($css_files); // move $css_files[''] to front.
1210             foreach ($css_files as $media => $css_file) {
1211                 if (!empty($this->DUMP_MODE)) {
1212                     if ($media == 'print')
1213                         $css[] = $this->_CSSlink($title, $css_file, '', $is_alt);
1214                 } else {
1215                     $css[] = $this->_CSSlink($title, $css_file, $media, $is_alt);
1216                 }
1217                 if ($is_alt) break;
1218             }
1219             $is_alt = true;
1220         }
1221         return HTML($css);
1222     }
1223
1224     function findTemplate($name)
1225     {
1226         if ($tmp = $this->_findFile("templates/$name.tmpl", 1))
1227             return $this->_path . $tmp;
1228         else {
1229             $f1 = $this->file("templates/$name.tmpl");
1230             foreach ($this->_parents as $parent) {
1231                 if ($tmp = $parent->_findFile("templates/$name.tmpl", 1))
1232                     return $this->_path . $tmp;
1233             }
1234             trigger_error("$f1 not found", E_USER_ERROR);
1235             return false;
1236         }
1237     }
1238
1239     /**
1240      * Add a random header element to head
1241      * TODO: first css, then js. Maybe seperate it into addJSHeaders/addCSSHeaders
1242      * or use an optional type argument, and seperate it within _MoreHeaders[]
1243      */
1244     //$GLOBALS['request']->_MoreHeaders = array();
1245     function addMoreHeaders($element)
1246     {
1247         $GLOBALS['request']->_MoreHeaders[] = $element;
1248         if (!empty($this->_headers_printed) and $this->_headers_printed) {
1249             trigger_error(_("Some action(page) wanted to add more headers, but they were already printed.")
1250                     . "\n" . $element->asXML(),
1251                 E_USER_NOTICE);
1252         }
1253     }
1254
1255     /**
1256      * Singleton. Only called once, by the head template. See the warning above.
1257      */
1258     function getMoreHeaders()
1259     {
1260         global $request;
1261         // actionpages cannot add headers, because recursive template expansion
1262         // already expanded the head template before.
1263         $this->_headers_printed = 1;
1264         if (empty($request->_MoreHeaders))
1265             return '';
1266         $out = '';
1267         if (false and ($file = $this->_findData('delayed.js'))) {
1268             $request->_MoreHeaders[] = JavaScript('
1269 // Add a script element as a child of the body
1270 function downloadJSAtOnload() {
1271 var element = document.createElement("script");
1272 element.src = "' . $file . '";
1273 document.body.appendChild(element);
1274 }
1275 // Check for browser support of event handling capability
1276 if (window.addEventListener)
1277 window.addEventListener("load", downloadJSAtOnload, false);
1278 else if (window.attachEvent)
1279 window.attachEvent("onload", downloadJSAtOnload);
1280 else window.onload = downloadJSAtOnload;');
1281         }
1282         //$out = "<!-- More Headers -->\n";
1283         foreach ($request->_MoreHeaders as $h) {
1284             if (is_object($h))
1285                 $out .= $h->printXML();
1286             else
1287                 $out .= "$h\n";
1288         }
1289         return $out;
1290     }
1291
1292     //$GLOBALS['request']->_MoreAttr = array();
1293     // new arg: named elements to be able to remove them. such as DoubleClickEdit for htmldumps
1294     function addMoreAttr($tag, $name, $element)
1295     {
1296         global $request;
1297         // protect from duplicate attr (body jscript: themes, prefs, ...)
1298         static $_attr_cache = array();
1299         $hash = md5($tag . "/" . $element);
1300         if (!empty($_attr_cache[$hash])) return;
1301         $_attr_cache[$hash] = 1;
1302
1303         if (empty($request->_MoreAttr) or !is_array($request->_MoreAttr[$tag]))
1304             $request->_MoreAttr[$tag] = array($name => $element);
1305         else
1306             $request->_MoreAttr[$tag][$name] = $element;
1307     }
1308
1309     function getMoreAttr($tag)
1310     {
1311         global $request;
1312         if (empty($request->_MoreAttr[$tag]))
1313             return '';
1314         $out = '';
1315         foreach ($request->_MoreAttr[$tag] as $name => $element) {
1316             if (is_object($element))
1317                 $out .= $element->printXML();
1318             else
1319                 $out .= "$element";
1320         }
1321         return $out;
1322     }
1323
1324     /**
1325      * Common Initialisations
1326      */
1327
1328     /**
1329      * The ->load() method replaces the formerly global code in themeinfo.php.
1330      * This is run only once for the selected theme, and not for the parent themes.
1331      * Without this you would not be able to derive from other themes.
1332      */
1333     function load()
1334     {
1335
1336         $this->initGlobals();
1337
1338         // CSS file defines fonts, colors and background images for this
1339         // style.  The companion '*-heavy.css' file isn't defined, it's just
1340         // expected to be in the same directory that the base style is in.
1341
1342         // This should result in phpwiki-printer.css being used when
1343         // printing or print-previewing with style "PhpWiki" or "MacOSX" selected.
1344         $this->setDefaultCSS('PhpWiki',
1345             array('' => 'phpwiki.css',
1346                 'print' => 'phpwiki-printer.css'));
1347
1348         // This allows one to manually select "Printer" style (when browsing page)
1349         // to see what the printer style looks like.
1350         $this->addAlternateCSS(_("Printer"), 'phpwiki-printer.css', 'print, screen');
1351         $this->addAlternateCSS(_("Top & bottom toolbars"), 'phpwiki-topbottombars.css');
1352         $this->addAlternateCSS(_("Modern"), 'phpwiki-modern.css');
1353
1354         if (isBrowserIE()) {
1355             $this->addMoreHeaders($this->_CSSlink(0,
1356                 $this->_findFile('IEFixes.css'), 'all'));
1357             $this->addMoreHeaders("\n");
1358         }
1359
1360         /**
1361          * The logo image appears on every page and links to the HomePage.
1362          */
1363         $this->addImageAlias('logo', WIKI_NAME . 'Logo.png');
1364
1365         $this->addImageAlias('search', 'search.png');
1366
1367         /**
1368          * The Signature image is shown after saving an edited page. If this
1369          * is set to false then the "Thank you for editing..." screen will
1370          * be omitted.
1371          */
1372
1373         $this->addImageAlias('signature', WIKI_NAME . "Signature.png");
1374         // Uncomment this next line to disable the signature.
1375         //$this->addImageAlias('signature', false);
1376
1377         /*
1378          * Link icons.
1379          */
1380         $this->setLinkIcon('http');
1381         $this->setLinkIcon('https');
1382         $this->setLinkIcon('ftp');
1383         $this->setLinkIcon('mailto');
1384         $this->setLinkIcon('interwiki');
1385         $this->setLinkIcon('wikiuser');
1386         $this->setLinkIcon('*', 'url');
1387
1388         $this->setButtonSeparator("\n | ");
1389
1390         /**
1391          * WikiWords can automatically be split by inserting spaces between
1392          * the words. The default is to leave WordsSmashedTogetherLikeSo.
1393          */
1394         $this->setAutosplitWikiWords(false);
1395
1396         /**
1397          * Layout improvement with dangling links for mostly closed wiki's:
1398          * If false, only users with edit permissions will be presented the
1399          * special wikiunknown class with "?" and Tooltip.
1400          * If true (default), any user will see the ?, but will be presented
1401          * the PrintLoginForm on a click.
1402          */
1403         //$this->setAnonEditUnknownLinks(false);
1404
1405         /*
1406          * You may adjust the formats used for formatting dates and times
1407          * below.  (These examples give the default formats.)
1408          * Formats are given as format strings to PHP strftime() function See
1409          * http://www.php.net/manual/en/function.strftime.php for details.
1410          * Do not include the server's zone (%Z), times are converted to the
1411          * user's time zone.
1412          *
1413          * Suggestion for french:
1414          *   $this->setDateFormat("%A %e %B %Y");
1415          *   $this->setTimeFormat("%H:%M:%S");
1416          * Suggestion for capable php versions, using the server locale:
1417          *   $this->setDateFormat("%x");
1418          *   $this->setTimeFormat("%X");
1419          */
1420         //$this->setDateFormat("%B %d, %Y");
1421         //$this->setTimeFormat("%I:%M %p");
1422
1423         /*
1424          * To suppress times in the "Last edited on" messages, give a
1425          * give a second argument of false:
1426          */
1427         //$this->setDateFormat("%B %d, %Y", false);
1428
1429         /**
1430          * Custom UserPreferences:
1431          * A list of name => _UserPreference class pairs.
1432          * Rationale: Certain themes should be able to extend the predefined list
1433          * of preferences. Display/editing is done in the theme specific userprefs.tmpl
1434          * but storage/sanification/update/... must be extended to the Get/SetPreferences methods.
1435          * See themes/wikilens/themeinfo.php
1436          */
1437         //$this->customUserPreference();
1438
1439         /**
1440          * Register custom PageList type and define custom PageList classes.
1441          * Rationale: Certain themes should be able to extend the predefined list
1442          * of pagelist types. E.g. certain plugins, like MostPopular might use
1443          * info=pagename,hits,rating
1444          * which displays the rating column whenever the wikilens theme is active.
1445          * See themes/wikilens/themeinfo.php
1446          */
1447         //$this->addPageListColumn();
1448
1449     } // end of load
1450
1451     /**
1452      * Custom UserPreferences:
1453      * A list of name => _UserPreference class pairs.
1454      * Rationale: Certain themes should be able to extend the predefined list
1455      * of preferences. Display/editing is done in the theme specific userprefs.tmpl
1456      * but storage/sanification/update/... must be extended to the Get/SetPreferences methods.
1457      * These values are just ignored if another theme is used.
1458      */
1459     function customUserPreferences($array)
1460     {
1461         global $customUserPreferenceColumns; // FIXME: really a global?
1462         if (empty($customUserPreferenceColumns)) $customUserPreferenceColumns = array();
1463         //array('wikilens' => new _UserPreference_wikilens());
1464         foreach ($array as $field => $prefobj) {
1465             $customUserPreferenceColumns[$field] = $prefobj;
1466         }
1467     }
1468
1469     /** addPageListColumn(array('rating' => new _PageList_Column_rating('rating', _("Rate"))))
1470      *  Register custom PageList types for special themes, like
1471      *  'rating' for wikilens
1472      */
1473     function addPageListColumn($array)
1474     {
1475         global $customPageListColumns;
1476         if (empty($customPageListColumns)) $customPageListColumns = array();
1477         foreach ($array as $column => $obj) {
1478             $customPageListColumns[$column] = $obj;
1479         }
1480     }
1481
1482     function initGlobals()
1483     {
1484         global $request;
1485         static $already = 0;
1486         if (!$already) {
1487             $script_url = deduce_script_name();
1488             if ((DEBUG & _DEBUG_REMOTE) and isset($_GET['start_debug']))
1489                 $script_url .= ("?start_debug=" . $_GET['start_debug']);
1490             $folderArrowPath = dirname($this->_findData('images/folderArrowLoading.gif'));
1491             $pagename = $request->getArg('pagename');
1492             $js = "var data_path = '" . javascript_quote_string(DATA_PATH) . "';\n"
1493                 // XSS warning with pagename
1494                 . "var pagename  = '" . javascript_quote_string($pagename) . "';\n"
1495                 . "var script_url= '" . javascript_quote_string($script_url) . "';\n"
1496                 . "var stylepath = data_path+'/" . javascript_quote_string($this->_theme) . "/';\n"
1497                 . "var folderArrowPath = '" . javascript_quote_string($folderArrowPath) . "';\n"
1498                 . "var use_path_info = " . (USE_PATH_INFO ? "true" : "false") . ";\n";
1499             $this->addMoreHeaders(JavaScript($js));
1500             $already = 1;
1501         }
1502     }
1503
1504     // Works only on action=browse. Patch #970004 by pixels
1505     // Usage: call $WikiTheme->initDoubleClickEdit() from theme init or
1506     // define ENABLE_DOUBLECLICKEDIT
1507     function initDoubleClickEdit()
1508     {
1509         if (!$this->HTML_DUMP_SUFFIX)
1510             $this->addMoreAttr('body', 'DoubleClickEdit', HTML::Raw(" ondblclick=\"url = document.URL; url2 = url; if (url.indexOf('?') != -1) url2 = url.slice(0, url.indexOf('?')); if ((url.indexOf('action') == -1) || (url.indexOf('action=browse') != -1)) document.location = url2 + '?action=edit';\""));
1511     }
1512
1513     // Immediate title search results via XMLHTML(HttpRequest)
1514     // by Bitflux GmbH, bitflux.ch. You need to install the livesearch.js seperately.
1515     // Google's or acdropdown is better.
1516     function initLiveSearch()
1517     {
1518         //subclasses of Sidebar will init this twice
1519         static $already = 0;
1520         if (!$this->HTML_DUMP_SUFFIX and !$already) {
1521             $this->addMoreAttr('body', 'LiveSearch',
1522                 HTML::Raw(" onload=\"liveSearchInit()"));
1523             $this->addMoreHeaders(JavaScript('var liveSearchURI="'
1524                 . WikiURL(_("TitleSearch"), false, true) . '";'));
1525             $this->addMoreHeaders(JavaScript('', array
1526             ('src' => $this->_findData('livesearch.js'))));
1527             $already = 1;
1528         }
1529     }
1530
1531     // Immediate title search results via XMLHttpRequest
1532     // using the shipped moacdropdown js-lib
1533     function initMoAcDropDown()
1534     {
1535         //subclasses of Sidebar will init this twice
1536         static $already = 0;
1537         if (!$this->HTML_DUMP_SUFFIX and !$already) {
1538             $dir = $this->_findData('moacdropdown');
1539             if (!DEBUG and ($css = $this->_findFile('moacdropdown/css/dropdown.css'))) {
1540                 $this->addMoreHeaders($this->_CSSlink(0, $css, 'all'));
1541             } else {
1542                 $this->addMoreHeaders(HTML::style(array('type' => 'text/css'), "  @import url( $dir/css/dropdown.css );\n"));
1543             }
1544             $already = 1;
1545         }
1546     }
1547
1548     function calendarLink($date = false)
1549     {
1550         return $this->calendarBase() . SUBPAGE_SEPARATOR .
1551             strftime("%Y-%m-%d", $date ? $date : time());
1552     }
1553
1554     function calendarBase()
1555     {
1556         static $UserCalPageTitle = false;
1557         global $request;
1558
1559         if (!$UserCalPageTitle)
1560             $UserCalPageTitle = $request->_user->getId() .
1561                 SUBPAGE_SEPARATOR . _("Calendar");
1562         if (!$UserCalPageTitle)
1563             $UserCalPageTitle = (BLOG_EMPTY_DEFAULT_PREFIX ? ''
1564                 : ($request->_user->getId() . SUBPAGE_SEPARATOR)) . "Blog";
1565         return $UserCalPageTitle;
1566     }
1567
1568     function calendarInit($force = false)
1569     {
1570         $dbi = $GLOBALS['request']->getDbh();
1571         // display flat calender dhtml in the sidebar
1572         if ($force or $dbi->isWikiPage($this->calendarBase())) {
1573             $jslang = @$GLOBALS['LANG'];
1574             $this->addMoreHeaders
1575             (
1576                 $this->_CSSlink(0,
1577                     $this->_findFile('jscalendar/calendar-phpwiki.css'), 'all'));
1578             $this->addMoreHeaders
1579             (JavaScript('',
1580                 array('src' => $this->_findData('jscalendar/calendar' . (DEBUG ? '' : '_stripped') . '.js'))));
1581             if (!($langfile = $this->_findData("jscalendar/lang/calendar-$jslang.js")))
1582                 $langfile = $this->_findData("jscalendar/lang/calendar-en.js");
1583             $this->addMoreHeaders(JavaScript('', array('src' => $langfile)));
1584             $this->addMoreHeaders
1585             (JavaScript('',
1586                 array('src' =>
1587                 $this->_findData('jscalendar/calendar-setup' . (DEBUG ? '' : '_stripped') . '.js'))));
1588
1589             // Get existing date entries for the current user
1590             require_once 'lib/TextSearchQuery.php';
1591             $iter = $dbi->titleSearch(new TextSearchQuery("^" . $this->calendarBase() . SUBPAGE_SEPARATOR, true, "auto"));
1592             $existing = array();
1593             while ($page = $iter->next()) {
1594                 if ($page->exists())
1595                     $existing[] = basename($page->_pagename);
1596             }
1597             if (!empty($existing)) {
1598                 $js_exist = '{"' . join('":1,"', $existing) . '":1}';
1599                 //var SPECIAL_DAYS = {"2004-05-11":1,"2004-05-12":1,"2004-06-01":1}
1600                 $this->addMoreHeaders(JavaScript('
1601 /* This table holds the existing calender entries for the current user
1602  *  calculated from the database
1603  */
1604
1605 var SPECIAL_DAYS = ' . javascript_quote_string($js_exist) . ';
1606
1607 /* This function returns true if the date exists in SPECIAL_DAYS */
1608 function dateExists(date, y, m, d) {
1609     var year = date.getFullYear();
1610     m = m + 1;
1611     m = m < 10 ? "0" + m : m;  // integer, 0..11
1612     d = d < 10 ? "0" + d : d;  // integer, 1..31
1613     var date = year+"-"+m+"-"+d;
1614     var exists = SPECIAL_DAYS[date];
1615     if (!exists) return false;
1616     else return true;
1617 }
1618 // This is the actual date status handler.
1619 // Note that it receives the date object as well as separate
1620 // values of year, month and date.
1621 function dateStatusFunc(date, y, m, d) {
1622     if (dateExists(date, y, m, d)) return "existing";
1623     else return false;
1624 }
1625 '));
1626             } else {
1627                 $this->addMoreHeaders(JavaScript('
1628 function dateStatusFunc(date, y, m, d) { return false;}'));
1629             }
1630         }
1631     }
1632
1633     ////////////////////////////////////////////////////////////////
1634     //
1635     // Events
1636     //
1637     ////////////////////////////////////////////////////////////////
1638
1639     /**  CbUserLogin (&$request, $userid)
1640      * Callback when a user logs in
1641      */
1642     function CbUserLogin(&$request, $userid)
1643     {
1644         ; // do nothing
1645     }
1646
1647     /** CbNewUserEdit (&$request, $userid)
1648      * Callback when a new user creates or edits a page
1649      */
1650     function CbNewUserEdit(&$request, $userid)
1651     {
1652         ; // i.e. create homepage with Template/UserPage
1653     }
1654
1655     /** CbNewUserLogin (&$request, $userid)
1656      * Callback when a "new user" logs in.
1657      *  What is new? We only record changes, not logins.
1658      *  Should we track user actions?
1659      *  Let's say a new user is a user without homepage.
1660      */
1661     function CbNewUserLogin(&$request, $userid)
1662     {
1663         ; // do nothing
1664     }
1665
1666     /** CbUserLogout (&$request, $userid)
1667      * Callback when a user logs out
1668      */
1669     function CbUserLogout(&$request, $userid)
1670     {
1671         ; // do nothing
1672     }
1673
1674 }
1675
1676 /**
1677  * A class representing a clickable "button".
1678  *
1679  * In it's simplest (default) form, a "button" is just a link associated
1680  * with some sort of wiki-action.
1681  */
1682 class Button extends HtmlElement
1683 {
1684     /** Constructor
1685      *
1686      * @param $text string The text for the button.
1687      * @param $url string The url (href) for the button.
1688      * @param $class string The CSS class for the button.
1689      * @param $options array Additional attributes for the &lt;input&gt; tag.
1690      */
1691     function Button($text, $url, $class = false, $options = false)
1692     {
1693         global $request;
1694         $this->_init('a', array('href' => $url));
1695         if ($class)
1696             $this->setAttr('class', $class);
1697         if ($request->getArg('frame'))
1698             $this->setAttr('target', '_top');
1699         if (!empty($options) and is_array($options)) {
1700             foreach ($options as $key => $val)
1701                 $this->setAttr($key, $val);
1702         }
1703         // Google honors this
1704         if (in_array(strtolower($text), array('edit', 'create', 'diff', 'pdf'))
1705             and !$request->_user->isAuthenticated()
1706         )
1707             $this->setAttr('rel', 'nofollow');
1708         $this->pushContent($GLOBALS['WikiTheme']->maybeSplitWikiWord($text));
1709     }
1710
1711 }
1712
1713 /**
1714  * A clickable image button.
1715  */
1716 class ImageButton extends Button
1717 {
1718     /** Constructor
1719      *
1720      * @param $text string The text for the button.
1721      * @param $url string The url (href) for the button.
1722      * @param $class string The CSS class for the button.
1723      * @param $img_url string URL for button's image.
1724      * @param $img_attr array Additional attributes for the &lt;img&gt; tag.
1725      */
1726     function ImageButton($text, $url, $class, $img_url, $img_attr = false)
1727     {
1728         $this->__construct('a', array('href' => $url));
1729         if ($class)
1730             $this->setAttr('class', $class);
1731         // Google honors this
1732         if (in_array(strtolower($text), array('edit', 'create', 'diff', 'pdf'))
1733             and !$GLOBALS['request']->_user->isAuthenticated()
1734         )
1735             $this->setAttr('rel', 'nofollow');
1736
1737         if (!is_array($img_attr))
1738             $img_attr = array();
1739         $img_attr['src'] = $img_url;
1740         $img_attr['alt'] = $text;
1741         $img_attr['class'] = 'wiki-button';
1742         $this->pushContent(HTML::img($img_attr));
1743     }
1744 }
1745
1746 /**
1747  * A class representing a form <samp>submit</samp> button.
1748  */
1749 class SubmitButton extends HtmlElement
1750 {
1751     /** Constructor
1752      *
1753      * @param $text string The text for the button.
1754      * @param $name string The name of the form field.
1755      * @param $class string The CSS class for the button.
1756      * @param $options array Additional attributes for the &lt;input&gt; tag.
1757      */
1758     function SubmitButton($text, $name = false, $class = false, $options = false)
1759     {
1760         $this->__construct('input', array('type' => 'submit',
1761             'value' => $text));
1762         if ($name)
1763             $this->setAttr('name', $name);
1764         if ($class)
1765             $this->setAttr('class', $class);
1766         if (!empty($options)) {
1767             foreach ($options as $key => $val)
1768                 $this->setAttr($key, $val);
1769         }
1770     }
1771
1772 }
1773
1774 /**
1775  * A class representing an image form <samp>submit</samp> button.
1776  */
1777 class SubmitImageButton extends SubmitButton
1778 {
1779     /** Constructor
1780      *
1781      * @param $text string The text for the button.
1782      * @param $name string The name of the form field.
1783      * @param $class string The CSS class for the button.
1784      * @param $img_url string URL for button's image.
1785      * @param $img_attr array Additional attributes for the &lt;img&gt; tag.
1786      */
1787     function SubmitImageButton($text, $name = false, $class = false, $img_url, $img_attr = false)
1788     {
1789         $this->__construct('input', array('type' => 'image',
1790             'src' => $img_url,
1791             'value' => $text,
1792             'alt' => $text));
1793         if ($name)
1794             $this->setAttr('name', $name);
1795         if ($class)
1796             $this->setAttr('class', $class);
1797         if (!empty($img_attr)) {
1798             foreach ($img_attr as $key => $val)
1799                 $this->setAttr($key, $val);
1800         }
1801     }
1802
1803 }
1804
1805 /**
1806  * A sidebar box with title and body, narrow fixed-width.
1807  * To represent abbrevated content of plugins, links or forms,
1808  * like "Getting Started", "Search", "Sarch Pagename",
1809  * "Login", "Menu", "Recent Changes", "Last comments", "Last Blogs"
1810  * "Calendar"
1811  * ... See http://tikiwiki.org/
1812  *
1813  * Usage:
1814  * sidebar.tmpl:
1815  *   $menu = SidebarBox("Menu",HTML::dl(HTML::dt(...))); $menu->format();
1816  *   $menu = PluginSidebarBox("RecentChanges",array('limit'=>10)); $menu->format();
1817  */
1818 class SidebarBox
1819 {
1820
1821     function SidebarBox($title, $body)
1822     {
1823         require_once 'lib/WikiPlugin.php';
1824         $this->title = $title;
1825         $this->body = $body;
1826     }
1827
1828     function format()
1829     {
1830         return WikiPlugin::makeBox($this->title, $this->body);
1831     }
1832 }
1833
1834 /**
1835  * A sidebar box for plugins.
1836  * Any plugin may provide a box($args=false, $request=false, $basepage=false)
1837  * method, with the help of WikiPlugin::makeBox()
1838  */
1839 class PluginSidebarBox extends SidebarBox
1840 {
1841
1842     var $_plugin, $_args = false, $_basepage = false;
1843
1844     function PluginSidebarBox($name, $args = false, $basepage = false)
1845     {
1846         require_once 'lib/WikiPlugin.php';
1847
1848         $loader = new WikiPluginLoader();
1849         $plugin = $loader->getPlugin($name);
1850         if (!$plugin) {
1851             return $loader->_error(sprintf(_("Plugin %s: undefined"),
1852                 $name));
1853         }
1854         /*
1855                 if (!method_exists($plugin, 'box')) {
1856                     return $loader->_error(sprintf(_("%s: has no box method"),
1857                                                    get_class($plugin)));
1858                 }*/
1859         $this->_plugin =& $plugin;
1860         $this->_args = $args ? $args : array();
1861         $this->_basepage = $basepage;
1862     }
1863
1864     function format($args = false)
1865     {
1866         return $this->_plugin->box($args ? array_merge($this->_args, $args) : $this->_args,
1867             $GLOBALS['request'],
1868             $this->_basepage);
1869     }
1870 }
1871
1872 // Various boxes which are no plugins
1873 class RelatedLinksBox extends SidebarBox
1874 {
1875     function RelatedLinksBox($title = false, $body = '', $limit = 20)
1876     {
1877         global $request;
1878         $this->title = $title ? $title : _("Related Links");
1879         $this->body = HTML($body);
1880         $page = $request->getPage($request->getArg('pagename'));
1881         $revision = $page->getCurrentRevision();
1882         $page_content = $revision->getTransformedContent();
1883         //$cache = &$page->_wikidb->_cache;
1884         $counter = 0;
1885         $sp = HTML::Raw('&middot; ');
1886         foreach ($page_content->getWikiPageLinks() as $link) {
1887             $linkto = $link['linkto'];
1888             if (!$request->_dbi->isWikiPage($linkto)) continue;
1889             $this->body->pushContent($sp, WikiLink($linkto), HTML::br());
1890             $counter++;
1891             if ($limit and $counter > $limit) continue;
1892         }
1893     }
1894 }
1895
1896 class RelatedExternalLinksBox extends SidebarBox
1897 {
1898     function RelatedExternalLinksBox($title = false, $body = '', $limit = 20)
1899     {
1900         global $request;
1901         $this->title = $title ? $title : _("External Links");
1902         $this->body = HTML($body);
1903         $page = $request->getPage($request->getArg('pagename'));
1904         $cache = &$page->_wikidb->_cache;
1905         $counter = 0;
1906         $sp = HTML::Raw('&middot; ');
1907         foreach ($cache->getWikiPageLinks() as $link) {
1908             $linkto = $link['linkto'];
1909             if ($linkto) {
1910                 $this->body->pushContent($sp, WikiLink($linkto), HTML::br());
1911                 $counter++;
1912                 if ($limit and $counter > $limit) continue;
1913             }
1914         }
1915     }
1916 }
1917
1918 function listAvailableThemes()
1919 {
1920     $available_themes = array();
1921     $dir_root = 'themes';
1922     if (defined('PHPWIKI_DIR'))
1923         $dir_root = PHPWIKI_DIR . "/$dir_root";
1924     $dir = dir($dir_root);
1925     if ($dir) {
1926         while ($entry = $dir->read()) {
1927             if (is_dir($dir_root . '/' . $entry)
1928                 and file_exists($dir_root . '/' . $entry . '/themeinfo.php')
1929             ) {
1930                 array_push($available_themes, $entry);
1931             }
1932         }
1933         $dir->close();
1934     }
1935     return $available_themes;
1936 }
1937
1938 function listAvailableLanguages()
1939 {
1940     $available_languages = array('en');
1941     $dir_root = 'locale';
1942     if (defined('PHPWIKI_DIR'))
1943         $dir_root = PHPWIKI_DIR . "/$dir_root";
1944     if ($dir = dir($dir_root)) {
1945         while ($entry = $dir->read()) {
1946             if (is_dir($dir_root . "/" . $entry) and is_dir($dir_root . '/' . $entry . '/LC_MESSAGES')) {
1947                 array_push($available_languages, $entry);
1948             }
1949         }
1950         $dir->close();
1951     }
1952     return $available_languages;
1953 }
1954
1955 // Local Variables:
1956 // mode: php
1957 // tab-width: 8
1958 // c-basic-offset: 4
1959 // c-hanging-comment-ender-p: nil
1960 // indent-tabs-mode: nil
1961 // End: