]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/WikiTheme.php
var --> public
[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     public $HTML_DUMP_SUFFIX = '';
183     public $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     public $_dateFormat = "%B %d, %Y";
320     public $_timeFormat = "%I:%M %p";
321
322     public $_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     function getFormatter($type, $format)
556     {
557         $method = strtolower("get${type}Formatter");
558         if (method_exists($this, $method))
559             return $this->{$method}($format);
560         return false;
561     }
562
563     ////////////////////////////////////////////////////////////////
564     //
565     // Links
566     //
567     ////////////////////////////////////////////////////////////////
568
569     public $_autosplitWikiWords = false;
570
571     function setAutosplitWikiWords($autosplit = true)
572     {
573         $this->_autosplitWikiWords = $autosplit ? true : false;
574     }
575
576     function maybeSplitWikiWord($wikiword)
577     {
578         if ($this->_autosplitWikiWords)
579             return SplitPagename($wikiword);
580         else
581             return $wikiword;
582     }
583
584     public $_anonEditUnknownLinks = true;
585
586     function setAnonEditUnknownLinks($anonedit = true)
587     {
588         $this->_anonEditUnknownLinks = $anonedit ? true : false;
589     }
590
591     function linkExistingWikiWord($wikiword, $linktext = '', $version = false)
592     {
593         global $request;
594
595         if ($version !== false and !$this->HTML_DUMP_SUFFIX)
596             $url = WikiURL($wikiword, array('version' => $version));
597         else
598             $url = WikiURL($wikiword);
599
600         // Extra steps for dumping page to an html file.
601         if ($this->HTML_DUMP_SUFFIX) {
602             $url = preg_replace('/^\./', '%2e', $url); // dot pages
603         }
604
605         $link = HTML::a(array('href' => $url));
606
607         if (isa($wikiword, 'WikiPageName'))
608             $default_text = $wikiword->shortName;
609         else
610             $default_text = $wikiword;
611
612         if (!empty($linktext)) {
613             $link->pushContent($linktext);
614             $link->setAttr('class', 'named-wiki');
615             $link->setAttr('title', $this->maybeSplitWikiWord($default_text));
616         } else {
617             $link->pushContent($this->maybeSplitWikiWord($default_text));
618             $link->setAttr('class', 'wiki');
619         }
620         if ($request->getArg('frame'))
621             $link->setAttr('target', '_top');
622         return $link;
623     }
624
625     function linkUnknownWikiWord($wikiword, $linktext = '')
626     {
627         global $request;
628
629         // Get rid of anchors on unknown wikiwords
630         if (isa($wikiword, 'WikiPageName')) {
631             $default_text = $wikiword->shortName;
632             $wikiword = $wikiword->name;
633         } else {
634             $default_text = $wikiword;
635         }
636
637         if ($this->DUMP_MODE) { // HTML, PDF or XML
638             $link = HTML::span(empty($linktext) ? $wikiword : $linktext);
639             $link->setAttr('style', 'text-decoration: underline');
640             $link->addTooltip(sprintf(_("Empty link to: %s"), $wikiword));
641             $link->setAttr('class', empty($linktext) ? 'wikiunknown' : 'named-wikiunknown');
642             return $link;
643         } else {
644             // if AnonEditUnknownLinks show "?" only users which are allowed to edit this page
645             if (!$this->_anonEditUnknownLinks and
646                 (!$request->_user->isSignedIn()
647                     or !mayAccessPage('edit', $request->getArg('pagename')))
648             ) {
649                 $text = HTML::span(empty($linktext) ? $wikiword : $linktext);
650                 $text->setAttr('class', empty($linktext) ? 'wikiunknown' : 'named-wikiunknown');
651                 return $text;
652             } else {
653                 $url = WikiURL($wikiword, array('action' => 'create'));
654                 $button = $this->makeButton('?', $url);
655                 $button->addTooltip(sprintf(_("Create: %s"), $wikiword));
656             }
657         }
658
659         $link = HTML::span();
660         if (!empty($linktext)) {
661             $link->pushContent(HTML::span($linktext));
662             $link->setAttr('style', 'text-decoration: underline');
663             $link->setAttr('class', 'named-wikiunknown');
664         } else {
665             $link->pushContent(HTML::span($this->maybeSplitWikiWord($default_text)));
666             $link->setAttr('style', 'text-decoration: underline');
667             $link->setAttr('class', 'wikiunknown');
668         }
669         if (!isa($button, "ImageButton"))
670             $button->setAttr('rel', 'nofollow');
671         $link->pushContent($button);
672         if ($request->getPref('googleLink')) {
673             $gbutton = $this->makeButton('G', "http://www.google.com/search?q="
674                 . urlencode($wikiword));
675             $gbutton->addTooltip(sprintf(_("Google:%s"), $wikiword));
676             $link->pushContent($gbutton);
677         }
678         if ($request->getArg('frame'))
679             $link->setAttr('target', '_top');
680
681         return $link;
682     }
683
684     function linkBadWikiWord($wikiword, $linktext = '')
685     {
686         global $ErrorManager;
687
688         if ($linktext) {
689             $text = $linktext;
690         } elseif (isa($wikiword, 'WikiPageName')) {
691             $text = $wikiword->shortName;
692         } else {
693             $text = $wikiword;
694         }
695
696         if (isa($wikiword, 'WikiPageName'))
697             $message = $wikiword->getWarnings();
698         else
699             $message = sprintf(_("“%s”: Bad page name"), $wikiword);
700         $ErrorManager->warning($message);
701
702         return HTML::span(array('class' => 'badwikiword'), $text);
703     }
704
705     ////////////////////////////////////////////////////////////////
706     //
707     // Images and Icons
708     //
709     ////////////////////////////////////////////////////////////////
710     public $_imageAliases = array();
711
712     /**
713      *
714      * (To disable an image, alias the image to <code>false</code>.
715      */
716     function addImageAlias($alias, $image_name)
717     {
718         // fall back to the PhpWiki-supplied image if not found
719         if ((empty($this->_imageAliases[$alias])
720             and $this->_findFile("images/$image_name", true))
721             or $image_name === false
722         )
723             $this->_imageAliases[$alias] = $image_name;
724     }
725
726     function getImageURL($image)
727     {
728         $aliases = &$this->_imageAliases;
729
730         if (isset($aliases[$image])) {
731             $image = $aliases[$image];
732             if (!$image)
733                 return false;
734         }
735
736         // If not extension, default to .png.
737         if (!preg_match('/\.\w+$/', $image))
738             $image .= '.png';
739
740         // FIXME: this should probably be made to fall back
741         //        automatically to .gif, .jpg.
742         //        Also try .gif before .png if browser doesn't like png.
743
744         $path = $this->_findData("images/$image", 'missing okay');
745         if (!$path) // search explicit images/ or button/ links also
746             $path = $this->_findData("$image", 'missing okay');
747
748         if ($this->DUMP_MODE) {
749             if (empty($this->dumped_images)) $this->dumped_images = array();
750             $path = "images/" . basename($path);
751             if (!in_array($path, $this->dumped_images))
752                 $this->dumped_images[] = $path;
753         }
754         return $path;
755     }
756
757     function setLinkIcon($proto, $image = false)
758     {
759         if (!$image)
760             $image = $proto;
761
762         $this->_linkIcons[$proto] = $image;
763     }
764
765     function getLinkIconURL($proto)
766     {
767         $icons = &$this->_linkIcons;
768         if (!empty($icons[$proto]))
769             return $this->getImageURL($icons[$proto]);
770         elseif (!empty($icons['*']))
771             return $this->getImageURL($icons['*']);
772         return false;
773     }
774
775     public $_linkIcon = 'front'; // or 'after' or 'no'.
776     // maybe also 'spanall': there is a scheme currently in effect with front, which
777     // spans the icon only to the first, to let the next words wrap on line breaks
778     // see stdlib.php:PossiblyGlueIconToText()
779     function getLinkIconAttr()
780     {
781         return $this->_linkIcon;
782     }
783
784     function setLinkIconAttr($where)
785     {
786         $this->_linkIcon = $where;
787     }
788
789     function addButtonAlias($text, $alias = false)
790     {
791         $aliases = &$this->_buttonAliases;
792
793         if (is_array($text))
794             $aliases = array_merge($aliases, $text);
795         elseif ($alias === false)
796             unset($aliases[$text]); else
797             $aliases[$text] = $alias;
798     }
799
800     function getButtonURL($text)
801     {
802         $aliases = &$this->_buttonAliases;
803         if (isset($aliases[$text]))
804             $text = $aliases[$text];
805
806         $qtext = urlencode($text);
807         $url = $this->_findButton("$qtext.png");
808         if ($url && strstr($url, '%')) {
809             $url = preg_replace('|([^/]+)$|e', 'urlencode("\\1")', $url);
810         }
811         if (!$url) { // Jeff complained about png not supported everywhere.
812             // This was not PC until 2005.
813             $url = $this->_findButton("$qtext.gif");
814             if ($url && strstr($url, '%')) {
815                 $url = preg_replace('|([^/]+)$|e', 'urlencode("\\1")', $url);
816             }
817         }
818         if ($url and $this->DUMP_MODE) {
819             if (empty($this->dumped_buttons)) $this->dumped_buttons = array();
820             $file = $url;
821             if (defined('DATA_PATH'))
822                 $file = substr($url, strlen(DATA_PATH) + 1);
823             $url = "images/buttons/" . basename($file);
824             if (!array_key_exists($text, $this->dumped_buttons))
825                 $this->dumped_buttons[$text] = $file;
826         }
827         return $url;
828     }
829
830     function _findButton($button_file)
831     {
832         if (empty($this->_button_path))
833             $this->_button_path = $this->_getButtonPath();
834
835         foreach ($this->_button_path as $dir) {
836             if ($path = $this->_findData("$dir/$button_file", 1))
837                 return $path;
838         }
839         return false;
840     }
841
842     function _getButtonPath()
843     {
844         $button_dir = $this->_findFile("buttons");
845         $path_dir = $this->_path . $button_dir;
846         if (!file_exists($path_dir) || !is_dir($path_dir))
847             return array();
848         $path = array($button_dir);
849
850         $dir = dir($path_dir);
851         while (($subdir = $dir->read()) !== false) {
852             if ($subdir[0] == '.')
853                 continue;
854             if ($subdir == 'CVS')
855                 continue;
856             if (is_dir("$path_dir/$subdir"))
857                 $path[] = "$button_dir/$subdir";
858         }
859         $dir->close();
860         // add default buttons
861         $path[] = "themes/default/buttons";
862         $path_dir = $this->_path . "themes/default/buttons";
863         $dir = dir($path_dir);
864         while (($subdir = $dir->read()) !== false) {
865             if ($subdir[0] == '.')
866                 continue;
867             if ($subdir == 'CVS')
868                 continue;
869             if (is_dir("$path_dir/$subdir"))
870                 $path[] = "themes/default/buttons/$subdir";
871         }
872         $dir->close();
873
874         return $path;
875     }
876
877     ////////////////////////////////////////////////////////////////
878     //
879     // Button style
880     //
881     ////////////////////////////////////////////////////////////////
882
883     function makeButton($text, $url, $class = false, $options = false)
884     {
885         // FIXME: don't always try for image button?
886
887         // Special case: URLs like 'submit:preview' generate form
888         // submission buttons.
889         if (preg_match('/^submit:(.*)$/', $url, $m))
890             return $this->makeSubmitButton($text, $m[1], $class, $options);
891
892         if (is_string($text))
893             $imgurl = $this->getButtonURL($text);
894         else
895             $imgurl = $text;
896         if ($imgurl)
897             return new ImageButton($text, $url,
898                 in_array($class, array("wikiaction", "wikiadmin")) ? "wikibutton" : $class,
899                 $imgurl, $options);
900         else
901             return new Button($this->maybeSplitWikiWord($text), $url,
902                 $class, $options);
903     }
904
905     function makeSubmitButton($text, $name, $class = false, $options = false)
906     {
907         $imgurl = $this->getButtonURL($text);
908
909         if ($imgurl)
910             return new SubmitImageButton($text, $name, !$class ? "wikibutton" : $class, $imgurl, $options);
911         else
912             return new SubmitButton($text, $name, $class, $options);
913     }
914
915     /**
916      * Make button to perform action.
917      *
918      * This constructs a button which performs an action on the
919      * currently selected version of the current page.
920      * (Or anotherpage or version, if you want...)
921      *
922      * @param $action string The action to perform (e.g. 'edit', 'lock').
923      * This can also be the name of an "action page" like 'LikePages'.
924      * Alternatively you can give a hash of query args to be applied
925      * to the page.
926      *
927      * @param $label string Textual label for the button.  If left empty,
928      * a suitable name will be guessed.
929      *
930      * @param $page_or_rev mixed  The page to link to.  This can be
931      * given as a string (the page name), a WikiDB_Page object, or as
932      * WikiDB_PageRevision object.  If given as a WikiDB_PageRevision
933      * object, the button will link to a specific version of the
934      * designated page, otherwise the button links to the most recent
935      * version of the page.
936      *
937      * @return object A Button object.
938      */
939     function makeActionButton($action, $label = false,
940                               $page_or_rev = false, $options = false)
941     {
942         extract($this->_get_name_and_rev($page_or_rev));
943
944         if (is_array($action)) {
945             $attr = $action;
946             $action = isset($attr['action']) ? $attr['action'] : 'browse';
947         } else
948             $attr['action'] = $action;
949
950         $class = is_safe_action($action) ? 'wikiaction' : 'wikiadmin';
951         if (!$label)
952             $label = $this->_labelForAction($action);
953
954         if ($version)
955             $attr['version'] = $version;
956
957         if ($action == 'browse')
958             unset($attr['action']);
959
960         $options = $this->fixAccesskey($options);
961
962         return $this->makeButton($label, WikiURL($pagename, $attr), $class, $options);
963     }
964
965     function tooltipAccessKeyPrefix()
966     {
967         static $tooltipAccessKeyPrefix = null;
968         if ($tooltipAccessKeyPrefix) return $tooltipAccessKeyPrefix;
969
970         $tooltipAccessKeyPrefix = 'alt';
971         if (isBrowserOpera()) $tooltipAccessKeyPrefix = 'shift-esc';
972         elseif (isBrowserSafari() or browserDetect("Mac") or isBrowserKonqueror())
973             $tooltipAccessKeyPrefix = 'ctrl'; // ff2 win and x11 only
974         elseif ((browserDetect("firefox/2") or browserDetect("minefield/3") or browserDetect("SeaMonkey/1.1"))
975             and ((browserDetect("windows") or browserDetect("x11")))
976         )
977             $tooltipAccessKeyPrefix = 'alt-shift';
978         return $tooltipAccessKeyPrefix;
979     }
980
981     /** Define the accesskey in the title only, with ending [p] or [alt-p].
982      *  This fixes the prefix in the title and sets the accesskey.
983      */
984     function fixAccesskey($attrs)
985     {
986         if (!empty($attrs['title']) and preg_match("/\[(alt-)?(.)\]$/", $attrs['title'], $m)) {
987             if (empty($attrs['accesskey'])) $attrs['accesskey'] = $m[2];
988             // firefox 'alt-shift', MSIE: 'alt', ... see wikibits.js
989             $attrs['title'] = preg_replace("/\[(alt-)?(.)\]$/", "[" . $this->tooltipAccessKeyPrefix() . "-\\2]", $attrs['title']);
990         }
991         return $attrs;
992     }
993
994     /**
995      * Make a "button" which links to a wiki-page.
996      *
997      * These are really just regular WikiLinks, possibly
998      * disguised (e.g. behind an image button) by the theme.
999      *
1000      * This method should probably only be used for links
1001      * which appear in page navigation bars, or similar places.
1002      *
1003      * Use linkExistingWikiWord, or LinkWikiWord for normal links.
1004      *
1005      * @param $page_or_rev mixed The page to link to.  This can be
1006      * given as a string (the page name), a WikiDB_Page object, or as
1007      * WikiDB_PageRevision object.  If given as a WikiDB_PageRevision
1008      * object, the button will link to a specific version of the
1009      * designated page, otherwise the button links to the most recent
1010      * version of the page.
1011      *
1012      * @return object A Button object.
1013      */
1014     function makeLinkButton($page_or_rev, $label = false, $action = false)
1015     {
1016         extract($this->_get_name_and_rev($page_or_rev));
1017
1018         $args = $version ? array('version' => $version) : false;
1019         if ($action) $args['action'] = $action;
1020
1021         return $this->makeButton($label ? $label : $pagename,
1022             WikiURL($pagename, $args), 'wiki');
1023     }
1024
1025     function _get_name_and_rev($page_or_rev)
1026     {
1027         $version = false;
1028
1029         if (empty($page_or_rev)) {
1030             global $request;
1031             $pagename = $request->getArg("pagename");
1032             $version = $request->getArg("version");
1033         } elseif (is_object($page_or_rev)) {
1034             if (isa($page_or_rev, 'WikiDB_PageRevision')) {
1035                 $rev = $page_or_rev;
1036                 $page = $rev->getPage();
1037                 if (!$rev->isCurrent()) $version = $rev->getVersion();
1038             } else {
1039                 $page = $page_or_rev;
1040             }
1041             $pagename = $page->getName();
1042         } else {
1043             $pagename = (string)$page_or_rev;
1044         }
1045         return compact('pagename', 'version');
1046     }
1047
1048     function _labelForAction($action)
1049     {
1050         switch ($action) {
1051             case 'edit':
1052                 return _("Edit");
1053             case 'diff':
1054                 return _("Diff");
1055             case 'logout':
1056                 return _("Sign Out");
1057             case 'login':
1058                 return _("Sign In");
1059             case 'rename':
1060                 return _("Rename Page");
1061             case 'lock':
1062                 return _("Lock Page");
1063             case 'unlock':
1064                 return _("Unlock Page");
1065             case 'remove':
1066                 return _("Remove Page");
1067             case 'purge':
1068                 return _("Purge Page");
1069             default:
1070                 // I don't think the rest of these actually get used.
1071                 // 'setprefs'
1072                 // 'upload' 'dumpserial' 'loadfile' 'zip'
1073                 // 'save' 'browse'
1074                 return gettext(ucfirst($action));
1075         }
1076     }
1077
1078     //----------------------------------------------------------------
1079     public $_buttonSeparator = "\n | ";
1080
1081     function setButtonSeparator($separator)
1082     {
1083         $this->_buttonSeparator = $separator;
1084     }
1085
1086     function getButtonSeparator()
1087     {
1088         return $this->_buttonSeparator;
1089     }
1090
1091     ////////////////////////////////////////////////////////////////
1092     //
1093     // CSS
1094     //
1095     // Notes:
1096     //
1097     // Based on testing with Galeon 1.2.7 (Mozilla 1.2):
1098     // Automatic media-based style selection (via <link> tags) only
1099     // seems to work for the default style, not for alternate styles.
1100     //
1101     // Doing
1102     //
1103     //  <link rel="stylesheet" type="text/css" href="phpwiki.css" />
1104     //  <link rel="stylesheet" type="text/css" href="phpwiki-printer.css" media="print" />
1105     //
1106     // works to make it so that the printer style sheet get used
1107     // automatically when printing (or print-previewing) a page
1108     // (but when only when the default style is selected.)
1109     //
1110     // Attempts like:
1111     //
1112     //  <link rel="alternate stylesheet" title="Modern"
1113     //        type="text/css" href="phpwiki-modern.css" />
1114     //  <link rel="alternate stylesheet" title="Modern"
1115     //        type="text/css" href="phpwiki-printer.css" media="print" />
1116     //
1117     // Result in two "Modern" choices when trying to select alternate style.
1118     // If one selects the first of those choices, one gets phpwiki-modern
1119     // both when browsing and printing.  If one selects the second "Modern",
1120     // one gets no CSS when browsing, and phpwiki-printer when printing.
1121     //
1122     // The Real Fix?
1123     // =============
1124     //
1125     // We should probably move to doing the media based style
1126     // switching in the CSS files themselves using, e.g.:
1127     //
1128     //  @import url(print.css) print;
1129     //
1130     ////////////////////////////////////////////////////////////////
1131
1132     function _CSSlink($title, $css_file, $media, $is_alt = false)
1133     {
1134         // Don't set title on default style.  This makes it clear to
1135         // the user which is the default (i.e. most supported) style.
1136         if ($is_alt and isBrowserKonqueror())
1137             return HTML();
1138         $link = HTML::link(array('rel' => $is_alt ? 'alternate stylesheet' : 'stylesheet',
1139             'type' => 'text/css',
1140             'href' => $this->_findData($css_file)));
1141         if ($is_alt)
1142             $link->setAttr('title', $title);
1143
1144         if ($media)
1145             $link->setAttr('media', $media);
1146         if ($this->DUMP_MODE) {
1147             if (empty($this->dumped_css)) $this->dumped_css = array();
1148             if (!in_array($css_file, $this->dumped_css)) $this->dumped_css[] = $css_file;
1149             $link->setAttr('href', basename($link->getAttr('href')));
1150         }
1151
1152         return $link;
1153     }
1154
1155     /** Set default CSS source for this theme.
1156      *
1157      * To set styles to be used for different media, pass a
1158      * hash for the second argument, e.g.
1159      *
1160      * $theme->setDefaultCSS('default', array('' => 'normal.css',
1161      *                                        'print' => 'printer.css'));
1162      *
1163      * If you call this more than once, the last one called takes
1164      * precedence as the default style.
1165      *
1166      * @param string $title Name of style (currently ignored, unless
1167      * you call this more than once, in which case, some of the style
1168      * will become alternate (rather than default) styles, and then their
1169      * titles will be used.
1170      *
1171      * @param mixed $css_files Name of CSS file, or hash containing a mapping
1172      * between media types and CSS file names.  Use a key of '' (the empty string)
1173      * to set the default CSS for non-specified media.  (See above for an example.)
1174      */
1175     function setDefaultCSS($title, $css_files)
1176     {
1177         if (!is_array($css_files))
1178             $css_files = array('' => $css_files);
1179         // Add to the front of $this->_css
1180         unset($this->_css[$title]);
1181         $this->_css = array_merge(array($title => $css_files), $this->_css);
1182     }
1183
1184     /** Set alternate CSS source for this theme.
1185      *
1186      * @param string $title     Name of style.
1187      * @param string $css_files Name of CSS file.
1188      */
1189     function addAlternateCSS($title, $css_files)
1190     {
1191         if (!is_array($css_files))
1192             $css_files = array('' => $css_files);
1193         $this->_css[$title] = $css_files;
1194     }
1195
1196     /**
1197      * @return string HTML for CSS.
1198      */
1199     function getCSS()
1200     {
1201         $css = array();
1202         $is_alt = false;
1203         foreach ($this->_css as $title => $css_files) {
1204             ksort($css_files); // move $css_files[''] to front.
1205             foreach ($css_files as $media => $css_file) {
1206                 if (!empty($this->DUMP_MODE)) {
1207                     if ($media == 'print')
1208                         $css[] = $this->_CSSlink($title, $css_file, '', $is_alt);
1209                 } else {
1210                     $css[] = $this->_CSSlink($title, $css_file, $media, $is_alt);
1211                 }
1212                 if ($is_alt) break;
1213             }
1214             $is_alt = true;
1215         }
1216         return HTML($css);
1217     }
1218
1219     function findTemplate($name)
1220     {
1221         if ($tmp = $this->_findFile("templates/$name.tmpl", 1))
1222             return $this->_path . $tmp;
1223         else {
1224             $f1 = $this->file("templates/$name.tmpl");
1225             foreach ($this->_parents as $parent) {
1226                 if ($tmp = $parent->_findFile("templates/$name.tmpl", 1))
1227                     return $this->_path . $tmp;
1228             }
1229             trigger_error("$f1 not found", E_USER_ERROR);
1230             return false;
1231         }
1232     }
1233
1234     /**
1235      * Add a random header element to head
1236      * TODO: first css, then js. Maybe separate it into addJSHeaders/addCSSHeaders
1237      * or use an optional type argument, and separate it within _MoreHeaders[]
1238      */
1239     //$GLOBALS['request']->_MoreHeaders = array();
1240     function addMoreHeaders($element)
1241     {
1242         $GLOBALS['request']->_MoreHeaders[] = $element;
1243         if (!empty($this->_headers_printed) and $this->_headers_printed) {
1244             trigger_error(_("Some action(page) wanted to add more headers, but they were already printed.")
1245                     . "\n" . $element->asXML(),
1246                 E_USER_NOTICE);
1247         }
1248     }
1249
1250     /**
1251      * Singleton. Only called once, by the head template. See the warning above.
1252      */
1253     function getMoreHeaders()
1254     {
1255         global $request;
1256         // actionpages cannot add headers, because recursive template expansion
1257         // already expanded the head template before.
1258         $this->_headers_printed = 1;
1259         if (empty($request->_MoreHeaders))
1260             return '';
1261         $out = '';
1262         if (false and ($file = $this->_findData('delayed.js'))) {
1263             $request->_MoreHeaders[] = JavaScript('
1264 // Add a script element as a child of the body
1265 function downloadJSAtOnload() {
1266 var element = document.createElement("script");
1267 element.src = "' . $file . '";
1268 document.body.appendChild(element);
1269 }
1270 // Check for browser support of event handling capability
1271 if (window.addEventListener)
1272 window.addEventListener("load", downloadJSAtOnload, false);
1273 else if (window.attachEvent)
1274 window.attachEvent("onload", downloadJSAtOnload);
1275 else window.onload = downloadJSAtOnload;');
1276         }
1277         //$out = "<!-- More Headers -->\n";
1278         foreach ($request->_MoreHeaders as $h) {
1279             if (is_object($h))
1280                 $out .= $h->printXML();
1281             else
1282                 $out .= "$h\n";
1283         }
1284         return $out;
1285     }
1286
1287     //$GLOBALS['request']->_MoreAttr = array();
1288     // new arg: named elements to be able to remove them. such as DoubleClickEdit for htmldumps
1289     function addMoreAttr($tag, $name, $element)
1290     {
1291         global $request;
1292         // protect from duplicate attr (body jscript: themes, prefs, ...)
1293         static $_attr_cache = array();
1294         $hash = md5($tag . "/" . $element);
1295         if (!empty($_attr_cache[$hash])) return;
1296         $_attr_cache[$hash] = 1;
1297
1298         if (empty($request->_MoreAttr) or !is_array($request->_MoreAttr[$tag]))
1299             $request->_MoreAttr[$tag] = array($name => $element);
1300         else
1301             $request->_MoreAttr[$tag][$name] = $element;
1302     }
1303
1304     function getMoreAttr($tag)
1305     {
1306         global $request;
1307         if (empty($request->_MoreAttr[$tag]))
1308             return '';
1309         $out = '';
1310         foreach ($request->_MoreAttr[$tag] as $name => $element) {
1311             if (is_object($element))
1312                 $out .= $element->printXML();
1313             else
1314                 $out .= "$element";
1315         }
1316         return $out;
1317     }
1318
1319     /**
1320      * Common Initialisations
1321      */
1322
1323     /**
1324      * The ->load() method replaces the formerly global code in themeinfo.php.
1325      * This is run only once for the selected theme, and not for the parent themes.
1326      * Without this you would not be able to derive from other themes.
1327      */
1328     function load()
1329     {
1330
1331         $this->initGlobals();
1332
1333         // CSS file defines fonts, colors and background images for this
1334         // style.  The companion '*-heavy.css' file isn't defined, it's just
1335         // expected to be in the same directory that the base style is in.
1336
1337         // This should result in phpwiki-printer.css being used when
1338         // printing or print-previewing with style "PhpWiki" or "MacOSX" selected.
1339         $this->setDefaultCSS('PhpWiki',
1340             array('' => 'phpwiki.css',
1341                 'print' => 'phpwiki-printer.css'));
1342
1343         // This allows one to manually select "Printer" style (when browsing page)
1344         // to see what the printer style looks like.
1345         $this->addAlternateCSS(_("Printer"), 'phpwiki-printer.css', 'print, screen');
1346         $this->addAlternateCSS(_("Top & bottom toolbars"), 'phpwiki-topbottombars.css');
1347         $this->addAlternateCSS(_("Modern"), 'phpwiki-modern.css');
1348
1349         if (isBrowserIE()) {
1350             $this->addMoreHeaders($this->_CSSlink(0,
1351                 $this->_findFile('IEFixes.css'), 'all'));
1352             $this->addMoreHeaders("\n");
1353         }
1354
1355         /**
1356          * The logo image appears on every page and links to the HomePage.
1357          */
1358         $this->addImageAlias('logo', WIKI_NAME . 'Logo.png');
1359
1360         $this->addImageAlias('search', 'search.png');
1361
1362         /**
1363          * The Signature image is shown after saving an edited page. If this
1364          * is set to false then the "Thank you for editing..." screen will
1365          * be omitted.
1366          */
1367
1368         $this->addImageAlias('signature', WIKI_NAME . "Signature.png");
1369         // Uncomment this next line to disable the signature.
1370         //$this->addImageAlias('signature', false);
1371
1372         /*
1373          * Link icons.
1374          */
1375         $this->setLinkIcon('http');
1376         $this->setLinkIcon('https');
1377         $this->setLinkIcon('ftp');
1378         $this->setLinkIcon('mailto');
1379         $this->setLinkIcon('interwiki');
1380         $this->setLinkIcon('wikiuser');
1381         $this->setLinkIcon('*', 'url');
1382
1383         $this->setButtonSeparator("\n | ");
1384
1385         /**
1386          * WikiWords can automatically be split by inserting spaces between
1387          * the words. The default is to leave WordsSmashedTogetherLikeSo.
1388          */
1389         $this->setAutosplitWikiWords(false);
1390
1391         /**
1392          * Layout improvement with dangling links for mostly closed wiki's:
1393          * If false, only users with edit permissions will be presented the
1394          * special wikiunknown class with "?" and Tooltip.
1395          * If true (default), any user will see the ?, but will be presented
1396          * the PrintLoginForm on a click.
1397          */
1398         //$this->setAnonEditUnknownLinks(false);
1399
1400         /*
1401          * You may adjust the formats used for formatting dates and times
1402          * below.  (These examples give the default formats.)
1403          * Formats are given as format strings to PHP strftime() function See
1404          * http://www.php.net/manual/en/function.strftime.php for details.
1405          * Do not include the server's zone (%Z), times are converted to the
1406          * user's time zone.
1407          *
1408          * Suggestion for french:
1409          *   $this->setDateFormat("%A %e %B %Y");
1410          *   $this->setTimeFormat("%H:%M:%S");
1411          * Suggestion for capable php versions, using the server locale:
1412          *   $this->setDateFormat("%x");
1413          *   $this->setTimeFormat("%X");
1414          */
1415         //$this->setDateFormat("%B %d, %Y");
1416         //$this->setTimeFormat("%I:%M %p");
1417
1418         /*
1419          * To suppress times in the "Last edited on" messages, give a
1420          * give a second argument of false:
1421          */
1422         //$this->setDateFormat("%B %d, %Y", false);
1423
1424         /**
1425          * Custom UserPreferences:
1426          * A list of name => _UserPreference class pairs.
1427          * Rationale: Certain themes should be able to extend the predefined list
1428          * of preferences. Display/editing is done in the theme specific userprefs.tmpl
1429          * but storage/sanification/update/... must be extended to the Get/SetPreferences methods.
1430          * See themes/wikilens/themeinfo.php
1431          */
1432         //$this->customUserPreference();
1433
1434         /**
1435          * Register custom PageList type and define custom PageList classes.
1436          * Rationale: Certain themes should be able to extend the predefined list
1437          * of pagelist types. E.g. certain plugins, like MostPopular might use
1438          * info=pagename,hits,rating
1439          * which displays the rating column whenever the wikilens theme is active.
1440          * See themes/wikilens/themeinfo.php
1441          */
1442         //$this->addPageListColumn();
1443
1444     } // end of load
1445
1446     /**
1447      * Custom UserPreferences:
1448      * A list of name => _UserPreference class pairs.
1449      * Rationale: Certain themes should be able to extend the predefined list
1450      * of preferences. Display/editing is done in the theme specific userprefs.tmpl
1451      * but storage/sanification/update/... must be extended to the Get/SetPreferences methods.
1452      * These values are just ignored if another theme is used.
1453      */
1454     function customUserPreferences($array)
1455     {
1456         global $customUserPreferenceColumns; // FIXME: really a global?
1457         if (empty($customUserPreferenceColumns)) $customUserPreferenceColumns = array();
1458         //array('wikilens' => new _UserPreference_wikilens());
1459         foreach ($array as $field => $prefobj) {
1460             $customUserPreferenceColumns[$field] = $prefobj;
1461         }
1462     }
1463
1464     /** addPageListColumn(array('rating' => new _PageList_Column_rating('rating', _("Rate"))))
1465      *  Register custom PageList types for special themes, like
1466      *  'rating' for wikilens
1467      */
1468     function addPageListColumn($array)
1469     {
1470         global $customPageListColumns;
1471         if (empty($customPageListColumns)) $customPageListColumns = array();
1472         foreach ($array as $column => $obj) {
1473             $customPageListColumns[$column] = $obj;
1474         }
1475     }
1476
1477     function initGlobals()
1478     {
1479         global $request;
1480         static $already = 0;
1481         if (!$already) {
1482             $script_url = deduce_script_name();
1483             if ((DEBUG & _DEBUG_REMOTE) and isset($_GET['start_debug']))
1484                 $script_url .= ("?start_debug=" . $_GET['start_debug']);
1485             $folderArrowPath = dirname($this->_findData('images/folderArrowLoading.gif'));
1486             $pagename = $request->getArg('pagename');
1487             $js = "var data_path = '" . javascript_quote_string(DATA_PATH) . "';\n"
1488                 // XSS warning with pagename
1489                 . "var pagename  = '" . javascript_quote_string($pagename) . "';\n"
1490                 . "var script_url= '" . javascript_quote_string($script_url) . "';\n"
1491                 . "var stylepath = data_path+'/" . javascript_quote_string($this->_theme) . "/';\n"
1492                 . "var folderArrowPath = '" . javascript_quote_string($folderArrowPath) . "';\n"
1493                 . "var use_path_info = " . (USE_PATH_INFO ? "true" : "false") . ";\n";
1494             $this->addMoreHeaders(JavaScript($js));
1495             $already = 1;
1496         }
1497     }
1498
1499     // Works only on action=browse. Patch #970004 by pixels
1500     // Usage: call $WikiTheme->initDoubleClickEdit() from theme init or
1501     // define ENABLE_DOUBLECLICKEDIT
1502     function initDoubleClickEdit()
1503     {
1504         if (!$this->HTML_DUMP_SUFFIX)
1505             $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';\""));
1506     }
1507
1508     // Immediate title search results via XMLHTML(HttpRequest)
1509     // by Bitflux GmbH, bitflux.ch. You need to install the livesearch.js separately.
1510     // Google's or acdropdown is better.
1511     function initLiveSearch()
1512     {
1513         //subclasses of Sidebar will init this twice
1514         static $already = 0;
1515         if (!$this->HTML_DUMP_SUFFIX and !$already) {
1516             $this->addMoreAttr('body', 'LiveSearch',
1517                 HTML::Raw(" onload=\"liveSearchInit()"));
1518             $this->addMoreHeaders(JavaScript('var liveSearchURI="'
1519                 . WikiURL(_("TitleSearch"), false, true) . '";'));
1520             $this->addMoreHeaders(JavaScript('', array
1521             ('src' => $this->_findData('livesearch.js'))));
1522             $already = 1;
1523         }
1524     }
1525
1526     // Immediate title search results via XMLHttpRequest
1527     // using the shipped moacdropdown js-lib
1528     function initMoAcDropDown()
1529     {
1530         //subclasses of Sidebar will init this twice
1531         static $already = 0;
1532         if (!$this->HTML_DUMP_SUFFIX and !$already) {
1533             $dir = $this->_findData('moacdropdown');
1534             if (!DEBUG and ($css = $this->_findFile('moacdropdown/css/dropdown.css'))) {
1535                 $this->addMoreHeaders($this->_CSSlink(0, $css, 'all'));
1536             } else {
1537                 $this->addMoreHeaders(HTML::style(array('type' => 'text/css'), "  @import url( $dir/css/dropdown.css );\n"));
1538             }
1539             $already = 1;
1540         }
1541     }
1542
1543     function calendarLink($date = false)
1544     {
1545         return $this->calendarBase() . SUBPAGE_SEPARATOR .
1546             strftime("%Y-%m-%d", $date ? $date : time());
1547     }
1548
1549     function calendarBase()
1550     {
1551         static $UserCalPageTitle = false;
1552         global $request;
1553
1554         if (!$UserCalPageTitle)
1555             $UserCalPageTitle = $request->_user->getId() .
1556                 SUBPAGE_SEPARATOR . _("Calendar");
1557         if (!$UserCalPageTitle)
1558             $UserCalPageTitle = (BLOG_EMPTY_DEFAULT_PREFIX ? ''
1559                 : ($request->_user->getId() . SUBPAGE_SEPARATOR)) . "Blog";
1560         return $UserCalPageTitle;
1561     }
1562
1563     function calendarInit($force = false)
1564     {
1565         $dbi = $GLOBALS['request']->getDbh();
1566         // display flat calender dhtml in the sidebar
1567         if ($force or $dbi->isWikiPage($this->calendarBase())) {
1568             $jslang = @$GLOBALS['LANG'];
1569             $this->addMoreHeaders
1570             (
1571                 $this->_CSSlink(0,
1572                     $this->_findFile('jscalendar/calendar-phpwiki.css'), 'all'));
1573             $this->addMoreHeaders
1574             (JavaScript('',
1575                 array('src' => $this->_findData('jscalendar/calendar' . (DEBUG ? '' : '_stripped') . '.js'))));
1576             if (!($langfile = $this->_findData("jscalendar/lang/calendar-$jslang.js")))
1577                 $langfile = $this->_findData("jscalendar/lang/calendar-en.js");
1578             $this->addMoreHeaders(JavaScript('', array('src' => $langfile)));
1579             $this->addMoreHeaders
1580             (JavaScript('',
1581                 array('src' =>
1582                 $this->_findData('jscalendar/calendar-setup' . (DEBUG ? '' : '_stripped') . '.js'))));
1583
1584             // Get existing date entries for the current user
1585             require_once 'lib/TextSearchQuery.php';
1586             $iter = $dbi->titleSearch(new TextSearchQuery("^" . $this->calendarBase() . SUBPAGE_SEPARATOR, true, "auto"));
1587             $existing = array();
1588             while ($page = $iter->next()) {
1589                 if ($page->exists())
1590                     $existing[] = basename($page->_pagename);
1591             }
1592             if (!empty($existing)) {
1593                 $js_exist = '{"' . join('":1,"', $existing) . '":1}';
1594                 //var SPECIAL_DAYS = {"2004-05-11":1,"2004-05-12":1,"2004-06-01":1}
1595                 $this->addMoreHeaders(JavaScript('
1596 /* This table holds the existing calender entries for the current user
1597  *  calculated from the database
1598  */
1599
1600 var SPECIAL_DAYS = ' . javascript_quote_string($js_exist) . ';
1601
1602 /* This function returns true if the date exists in SPECIAL_DAYS */
1603 function dateExists(date, y, m, d) {
1604     var year = date.getFullYear();
1605     m = m + 1;
1606     m = m < 10 ? "0" + m : m;  // integer, 0..11
1607     d = d < 10 ? "0" + d : d;  // integer, 1..31
1608     var date = year+"-"+m+"-"+d;
1609     var exists = SPECIAL_DAYS[date];
1610     if (!exists) return false;
1611     else return true;
1612 }
1613 // This is the actual date status handler.
1614 // Note that it receives the date object as well as separate
1615 // values of year, month and date.
1616 function dateStatusFunc(date, y, m, d) {
1617     if (dateExists(date, y, m, d)) return "existing";
1618     else return false;
1619 }
1620 '));
1621             } else {
1622                 $this->addMoreHeaders(JavaScript('
1623 function dateStatusFunc(date, y, m, d) { return false;}'));
1624             }
1625         }
1626     }
1627
1628     ////////////////////////////////////////////////////////////////
1629     //
1630     // Events
1631     //
1632     ////////////////////////////////////////////////////////////////
1633
1634     /**  CbUserLogin (&$request, $userid)
1635      * Callback when a user logs in
1636      */
1637     function CbUserLogin(&$request, $userid)
1638     {
1639         ; // do nothing
1640     }
1641
1642     /** CbNewUserEdit (&$request, $userid)
1643      * Callback when a new user creates or edits a page
1644      */
1645     function CbNewUserEdit(&$request, $userid)
1646     {
1647         ; // i.e. create homepage with Template/UserPage
1648     }
1649
1650     /** CbNewUserLogin (&$request, $userid)
1651      * Callback when a "new user" logs in.
1652      *  What is new? We only record changes, not logins.
1653      *  Should we track user actions?
1654      *  Let's say a new user is a user without homepage.
1655      */
1656     function CbNewUserLogin(&$request, $userid)
1657     {
1658         ; // do nothing
1659     }
1660
1661     /** CbUserLogout (&$request, $userid)
1662      * Callback when a user logs out
1663      */
1664     function CbUserLogout(&$request, $userid)
1665     {
1666         ; // do nothing
1667     }
1668
1669 }
1670
1671 /**
1672  * A class representing a clickable "button".
1673  *
1674  * In it's simplest (default) form, a "button" is just a link associated
1675  * with some sort of wiki-action.
1676  */
1677 class Button extends HtmlElement
1678 {
1679     /** Constructor
1680      *
1681      * @param $text string The text for the button.
1682      * @param $url string The url (href) for the button.
1683      * @param $class string The CSS class for the button.
1684      * @param $options array Additional attributes for the &lt;input&gt; tag.
1685      */
1686     function Button($text, $url, $class = false, $options = false)
1687     {
1688         global $request;
1689         $this->_init('a', array('href' => $url));
1690         if ($class)
1691             $this->setAttr('class', $class);
1692         if ($request->getArg('frame'))
1693             $this->setAttr('target', '_top');
1694         if (!empty($options) and is_array($options)) {
1695             foreach ($options as $key => $val)
1696                 $this->setAttr($key, $val);
1697         }
1698         // Google honors this
1699         if (in_array(strtolower($text), array('edit', 'create', 'diff', 'pdf'))
1700             and !$request->_user->isAuthenticated()
1701         )
1702             $this->setAttr('rel', 'nofollow');
1703         $this->pushContent($GLOBALS['WikiTheme']->maybeSplitWikiWord($text));
1704     }
1705
1706 }
1707
1708 /**
1709  * A clickable image button.
1710  */
1711 class ImageButton extends Button
1712 {
1713     /** Constructor
1714      *
1715      * @param $text string The text for the button.
1716      * @param $url string The url (href) for the button.
1717      * @param $class string The CSS class for the button.
1718      * @param $img_url string URL for button's image.
1719      * @param $img_attr array Additional attributes for the &lt;img&gt; tag.
1720      */
1721     function ImageButton($text, $url, $class, $img_url, $img_attr = false)
1722     {
1723         $this->__construct('a', array('href' => $url));
1724         if ($class)
1725             $this->setAttr('class', $class);
1726         // Google honors this
1727         if (in_array(strtolower($text), array('edit', 'create', 'diff', 'pdf'))
1728             and !$GLOBALS['request']->_user->isAuthenticated()
1729         )
1730             $this->setAttr('rel', 'nofollow');
1731
1732         if (!is_array($img_attr))
1733             $img_attr = array();
1734         $img_attr['src'] = $img_url;
1735         $img_attr['alt'] = $text;
1736         $img_attr['class'] = 'wiki-button';
1737         $this->pushContent(HTML::img($img_attr));
1738     }
1739 }
1740
1741 /**
1742  * A class representing a form <samp>submit</samp> button.
1743  */
1744 class SubmitButton extends HtmlElement
1745 {
1746     /** Constructor
1747      *
1748      * @param $text string The text for the button.
1749      * @param $name string The name of the form field.
1750      * @param $class string The CSS class for the button.
1751      * @param $options array Additional attributes for the &lt;input&gt; tag.
1752      */
1753     function SubmitButton($text, $name = false, $class = false, $options = false)
1754     {
1755         $this->__construct('input', array('type' => 'submit',
1756             'value' => $text));
1757         if ($name)
1758             $this->setAttr('name', $name);
1759         if ($class)
1760             $this->setAttr('class', $class);
1761         if (!empty($options)) {
1762             foreach ($options as $key => $val)
1763                 $this->setAttr($key, $val);
1764         }
1765     }
1766
1767 }
1768
1769 /**
1770  * A class representing an image form <samp>submit</samp> button.
1771  */
1772 class SubmitImageButton extends SubmitButton
1773 {
1774     /** Constructor
1775      *
1776      * @param $text string The text for the button.
1777      * @param $name string The name of the form field.
1778      * @param $class string The CSS class for the button.
1779      * @param $img_url string URL for button's image.
1780      * @param $img_attr array Additional attributes for the &lt;img&gt; tag.
1781      */
1782     function SubmitImageButton($text, $name = false, $class = false, $img_url, $img_attr = false)
1783     {
1784         $this->__construct('input', array('type' => 'image',
1785             'src' => $img_url,
1786             'value' => $text,
1787             'alt' => $text));
1788         if ($name)
1789             $this->setAttr('name', $name);
1790         if ($class)
1791             $this->setAttr('class', $class);
1792         if (!empty($img_attr)) {
1793             foreach ($img_attr as $key => $val)
1794                 $this->setAttr($key, $val);
1795         }
1796     }
1797
1798 }
1799
1800 /**
1801  * A sidebar box with title and body, narrow fixed-width.
1802  * To represent abbrevated content of plugins, links or forms,
1803  * like "Getting Started", "Search", "Sarch Pagename",
1804  * "Login", "Menu", "Recent Changes", "Last comments", "Last Blogs"
1805  * "Calendar"
1806  * ... See http://tikiwiki.org/
1807  *
1808  * Usage:
1809  * sidebar.tmpl:
1810  *   $menu = SidebarBox("Menu",HTML::dl(HTML::dt(...))); $menu->format();
1811  *   $menu = PluginSidebarBox("RecentChanges",array('limit'=>10)); $menu->format();
1812  */
1813 class SidebarBox
1814 {
1815
1816     function SidebarBox($title, $body)
1817     {
1818         require_once 'lib/WikiPlugin.php';
1819         $this->title = $title;
1820         $this->body = $body;
1821     }
1822
1823     function format()
1824     {
1825         return WikiPlugin::makeBox($this->title, $this->body);
1826     }
1827 }
1828
1829 /**
1830  * A sidebar box for plugins.
1831  * Any plugin may provide a box($args=false, $request=false, $basepage=false)
1832  * method, with the help of WikiPlugin::makeBox()
1833  */
1834 class PluginSidebarBox extends SidebarBox
1835 {
1836
1837     public $_plugin, $_args = false, $_basepage = false;
1838
1839     function PluginSidebarBox($name, $args = false, $basepage = false)
1840     {
1841         require_once 'lib/WikiPlugin.php';
1842
1843         $loader = new WikiPluginLoader();
1844         $plugin = $loader->getPlugin($name);
1845         if (!$plugin) {
1846             return $loader->_error(sprintf(_("Plugin %s: undefined"),
1847                 $name));
1848         }
1849         /*
1850                 if (!method_exists($plugin, 'box')) {
1851                     return $loader->_error(sprintf(_("%s: has no box method"),
1852                                                    get_class($plugin)));
1853                 }*/
1854         $this->_plugin =& $plugin;
1855         $this->_args = $args ? $args : array();
1856         $this->_basepage = $basepage;
1857     }
1858
1859     function format($args = false)
1860     {
1861         return $this->_plugin->box($args ? array_merge($this->_args, $args) : $this->_args,
1862             $GLOBALS['request'],
1863             $this->_basepage);
1864     }
1865 }
1866
1867 // Various boxes which are no plugins
1868 class RelatedLinksBox extends SidebarBox
1869 {
1870     function RelatedLinksBox($title = false, $body = '', $limit = 20)
1871     {
1872         global $request;
1873         $this->title = $title ? $title : _("Related Links");
1874         $this->body = HTML($body);
1875         $page = $request->getPage($request->getArg('pagename'));
1876         $revision = $page->getCurrentRevision();
1877         $page_content = $revision->getTransformedContent();
1878         //$cache = &$page->_wikidb->_cache;
1879         $counter = 0;
1880         $sp = HTML::Raw('&middot; ');
1881         foreach ($page_content->getWikiPageLinks() as $link) {
1882             $linkto = $link['linkto'];
1883             if (!$request->_dbi->isWikiPage($linkto)) continue;
1884             $this->body->pushContent($sp, WikiLink($linkto), HTML::br());
1885             $counter++;
1886             if ($limit and $counter > $limit) continue;
1887         }
1888     }
1889 }
1890
1891 class RelatedExternalLinksBox extends SidebarBox
1892 {
1893     function RelatedExternalLinksBox($title = false, $body = '', $limit = 20)
1894     {
1895         global $request;
1896         $this->title = $title ? $title : _("External Links");
1897         $this->body = HTML($body);
1898         $page = $request->getPage($request->getArg('pagename'));
1899         $cache = &$page->_wikidb->_cache;
1900         $counter = 0;
1901         $sp = HTML::Raw('&middot; ');
1902         foreach ($cache->getWikiPageLinks() as $link) {
1903             $linkto = $link['linkto'];
1904             if ($linkto) {
1905                 $this->body->pushContent($sp, WikiLink($linkto), HTML::br());
1906                 $counter++;
1907                 if ($limit and $counter > $limit) continue;
1908             }
1909         }
1910     }
1911 }
1912
1913 function listAvailableThemes()
1914 {
1915     $available_themes = array();
1916     $dir_root = 'themes';
1917     if (defined('PHPWIKI_DIR'))
1918         $dir_root = PHPWIKI_DIR . "/$dir_root";
1919     $dir = dir($dir_root);
1920     if ($dir) {
1921         while ($entry = $dir->read()) {
1922             if (is_dir($dir_root . '/' . $entry)
1923                 and file_exists($dir_root . '/' . $entry . '/themeinfo.php')
1924             ) {
1925                 array_push($available_themes, $entry);
1926             }
1927         }
1928         $dir->close();
1929     }
1930     return $available_themes;
1931 }
1932
1933 function listAvailableLanguages()
1934 {
1935     $available_languages = array('en');
1936     $dir_root = 'locale';
1937     if (defined('PHPWIKI_DIR'))
1938         $dir_root = PHPWIKI_DIR . "/$dir_root";
1939     if ($dir = dir($dir_root)) {
1940         while ($entry = $dir->read()) {
1941             if (is_dir($dir_root . "/" . $entry) and is_dir($dir_root . '/' . $entry . '/LC_MESSAGES')) {
1942                 array_push($available_languages, $entry);
1943             }
1944         }
1945         $dir->close();
1946     }
1947     return $available_languages;
1948 }
1949
1950 // Local Variables:
1951 // mode: php
1952 // tab-width: 8
1953 // c-basic-offset: 4
1954 // c-hanging-comment-ender-p: nil
1955 // indent-tabs-mode: nil
1956 // End: