]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/WikiTheme.php
Remove IEFixes
[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 string $action
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 string $label
158  * A label for the button.  If ommited, a suitable default (based on the valued of $action)
159  * will be picked.
160  *
161  * @param mixed $page_or_rev
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  * @param array $options
171  *
172  * @return object
173  */
174 function Button($action, $label = '', $page_or_rev = false, $options = array())
175 {
176     global $WikiTheme;
177
178     if (!is_array($action) && preg_match('/^submit:(.*)/', $action, $m))
179         return $WikiTheme->makeSubmitButton($label, $m[1], $page_or_rev, $options);
180     else
181         return $WikiTheme->makeActionButton($action, $label, $page_or_rev, $options);
182 }
183
184 class WikiTheme
185 {
186     public $HTML_DUMP_SUFFIX = '';
187     public $DUMP_MODE = false, $dumped_images, $dumped_css;
188
189     /**
190      * noinit: Do not initialize unnecessary items in default_theme fallback twice.
191      */
192     function WikiTheme($theme_name = 'default', $noinit = false)
193     {
194         $this->_name = $theme_name;
195         $this->_themes_dir = NormalizeLocalFileName("themes");
196         $this->_path = defined('PHPWIKI_DIR') ? NormalizeLocalFileName("") : "";
197         $this->_theme = "themes/$theme_name";
198         $this->_parents = array();
199
200         if ($theme_name != 'default') {
201             $parent = $this;
202             /* derived classes should search all parent classes */
203             while ($parent = get_parent_class($parent)) {
204                 if (strtolower($parent) == 'wikitheme') {
205                     $this->_default_theme = new WikiTheme('default', true);
206                     $this->_parents[] = $this->_default_theme;
207                 } elseif ($parent) {
208                     $this->_parents[] = new WikiTheme
209                     (preg_replace("/^WikiTheme_/i", "", $parent), true);
210                 }
211             }
212         }
213         if ($noinit) return;
214         $this->_css = array();
215
216         // on derived classes do not add headers twice
217         if (count($this->_parents) > 1) {
218             return;
219         }
220         $this->addMoreHeaders(JavaScript('', array('src' => $this->_findData("wikicommon.js"))));
221         if (!(defined('FUSIONFORGE') and FUSIONFORGE)) {
222             // FusionForge already loads this
223             $this->addMoreHeaders(JavaScript('', array('src' => $this->_findData("sortable.js"))));
224         }
225         // by pixels
226         if ((is_object($GLOBALS['request']) // guard against unittests
227             and $GLOBALS['request']->getPref('doubleClickEdit'))
228             or ENABLE_DOUBLECLICKEDIT
229         )
230             $this->initDoubleClickEdit();
231
232         // will be replaced by acDropDown
233         if (ENABLE_LIVESEARCH) { // by bitflux.ch
234             $this->initLiveSearch();
235         }
236         // replaces external LiveSearch
237         // enable ENABLE_AJAX for DynamicIncludePage
238         if (ENABLE_ACDROPDOWN or ENABLE_AJAX) {
239             $this->initMoAcDropDown();
240             if (ENABLE_AJAX and DEBUG) // minified all together
241                 $this->addMoreHeaders(JavaScript('', array('src' => $this->_findData("ajax.js"))));
242         }
243     }
244
245     function file($file)
246     {
247         return $this->_path . "$this->_theme/$file";
248     }
249
250     function _findFile($file, $missing_okay = false)
251     {
252         if (file_exists($this->file($file)))
253             return "$this->_theme/$file";
254
255         // FIXME: this is a short-term hack.  Delete this after all files
256         // get moved into themes/...
257         // Needed for button paths in parent themes
258         if (file_exists($this->_path . $file))
259             return $file;
260
261         /* Derived classes should search all parent classes */
262         foreach ($this->_parents as $parent) {
263             $path = $parent->_findFile($file, 1);
264             if ($path) {
265                 return $path;
266             } elseif (0 and DEBUG & (_DEBUG_VERBOSE + _DEBUG_REMOTE)) {
267                 trigger_error("$parent->_theme/$file: not found", E_USER_NOTICE);
268             }
269         }
270         if (isset($this->_default_theme)) {
271             return $this->_default_theme->_findFile($file, $missing_okay);
272         } elseif (!$missing_okay) {
273             trigger_error("$this->_theme/$file: not found", E_USER_NOTICE);
274             if (DEBUG & _DEBUG_TRACE) {
275                 echo "<pre>";
276                 printSimpleTrace(debug_backtrace());
277                 echo "</pre>\n";
278             }
279         }
280         return false;
281     }
282
283     function _findData($file, $missing_okay = false)
284     {
285         if (!string_starts_with($file, "themes")) { // common case
286             $path = $this->_findFile($file, $missing_okay);
287         } else {
288             // _findButton only
289             if (file_exists($file)) {
290                 $path = $file;
291             } elseif (defined('DATA_PATH')
292                 and file_exists(DATA_PATH . "/$file")
293             ) {
294                 $path = $file;
295             } else { // fallback for buttons in parent themes
296                 $path = $this->_findFile($file, $missing_okay);
297             }
298         }
299         if (!$path)
300             return false;
301         if (!DEBUG) {
302             $min = preg_replace("/\.(css|js)$/", "-min.\\1", $file);
303             if ($min and ($x = $this->_findFile($min, true))) $path = $x;
304         }
305
306         if (defined('DATA_PATH'))
307             return DATA_PATH . "/$path";
308         return $path;
309     }
310
311     ////////////////////////////////////////////////////////////////
312     //
313     // Date and Time formatting
314     //
315     ////////////////////////////////////////////////////////////////
316
317     // Note:  Windows' implementation of strftime does not include certain
318     // format specifiers, such as %e (for date without leading zeros).  In
319     // general, see:
320     // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vclib/html/_crt_strftime.2c_.wcsftime.asp
321     // As a result, we have to use %d, and strip out leading zeros ourselves.
322
323     public $_dateFormat = "%B %d, %Y";
324     public $_timeFormat = "%I:%M %p";
325
326     public $_showModTime = true;
327
328     /**
329      * Set format string used for dates.
330      *
331      * @param $fs string Format string for dates.
332      *
333      * @param $show_mod_time bool If true (default) then times
334      * are included in the messages generated by getLastModifiedMessage(),
335      * otherwise, only the date of last modification will be shown.
336      */
337     function setDateFormat($fs, $show_mod_time = true)
338     {
339         $this->_dateFormat = $fs;
340         $this->_showModTime = $show_mod_time;
341     }
342
343     /**
344      * Set format string used for times.
345      *
346      * @param $fs string Format string for times.
347      */
348     function setTimeFormat($fs)
349     {
350         $this->_timeFormat = $fs;
351     }
352
353     /**
354      * Format a date.
355      *
356      * Any time zone offset specified in the users preferences is
357      * taken into account by this method.
358      *
359      * @param $time_t integer Unix-style time.
360      *
361      * @return string The date.
362      */
363     function formatDate($time_t)
364     {
365         global $request;
366
367         $offset_time = $time_t + 3600 * $request->getPref('timeOffset');
368         // strip leading zeros from date elements (ie space followed by zero
369         // or leading 0 as in French "09 mai 2009")
370         return preg_replace('/ 0/', ' ', preg_replace('/^0/', ' ',
371             strftime($this->_dateFormat, $offset_time)));
372     }
373
374     /**
375      * Format a date.
376      *
377      * Any time zone offset specified in the users preferences is
378      * taken into account by this method.
379      *
380      * @param $time_t integer Unix-style time.
381      *
382      * @return string The time.
383      */
384     function formatTime($time_t)
385     {
386         //FIXME: make 24-hour mode configurable?
387         global $request;
388         $offset_time = $time_t + 3600 * $request->getPref('timeOffset');
389         return preg_replace('/^0/', ' ',
390             strtolower(strftime($this->_timeFormat, $offset_time)));
391     }
392
393     /**
394      * Format a date and time.
395      *
396      * Any time zone offset specified in the users preferences is
397      * taken into account by this method.
398      *
399      * @param $time_t integer Unix-style time.
400      *
401      * @return string The date and time.
402      */
403     function formatDateTime($time_t)
404     {
405         if ($time_t == 0) {
406             // Do not display "01 January 1970 1:00" for nonexistent pages
407             return "";
408         } else {
409             return $this->formatDate($time_t) . ' ' . $this->formatTime($time_t);
410         }
411     }
412
413     /**
414      * Format a (possibly relative) date.
415      *
416      * If enabled in the users preferences, this method might
417      * return a relative day (e.g. 'Today', 'Yesterday').
418      *
419      * Any time zone offset specified in the users preferences is
420      * taken into account by this method.
421      *
422      * @param $time_t integer Unix-style time.
423      *
424      * @return string The day.
425      */
426     function getDay($time_t)
427     {
428         global $request;
429
430         if ($request->getPref('relativeDates') && ($date = $this->_relativeDay($time_t))) {
431             return ucfirst($date);
432         }
433         return $this->formatDate($time_t);
434     }
435
436     /**
437      * Format the "last modified" message for a page revision.
438      *
439      * @param object $revision A WikiDB_PageRevision object.
440      *
441      * @param string $show_version Should the page version number
442      * be included in the message.  (If this argument is omitted,
443      * then the version number will be shown only iff the revision
444      * is not the current one.
445      *
446      * @return string The "last modified" message.
447      */
448     function getLastModifiedMessage($revision, $show_version = 'auto')
449     {
450         global $request;
451         if (!$revision) return '';
452
453         // dates >= this are considered invalid.
454         if (!defined('EPOCH'))
455             define('EPOCH', 0); // seconds since ~ January 1 1970
456
457         $mtime = $revision->get('mtime');
458         if ($mtime <= EPOCH)
459             return _("Never edited");
460
461         if ($show_version == 'auto')
462             $show_version = !$revision->isCurrent();
463
464         if ($request->getPref('relativeDates') && ($date = $this->_relativeDay($mtime))) {
465             if ($this->_showModTime)
466                 $date = sprintf(_("%s at %s"),
467                     $date, $this->formatTime($mtime));
468
469             if ($show_version)
470                 return fmt("Version %s, saved on %s", $revision->getVersion(), $date);
471             else
472                 return fmt("Last edited on %s", $date);
473         }
474
475         if ($this->_showModTime)
476             $date = $this->formatDateTime($mtime);
477         else
478             $date = $this->formatDate($mtime);
479
480         if ($show_version)
481             return fmt("Version %s, saved on %s", $revision->getVersion(), $date);
482         else
483             return fmt("Last edited on %s", $date);
484     }
485
486     function _relativeDay($time_t)
487     {
488         global $request;
489
490         if (is_numeric($request->getPref('timeOffset')))
491             $offset = 3600 * $request->getPref('timeOffset');
492         else
493             $offset = 0;
494
495         $now = time() + $offset;
496         $today = localtime($now, true);
497         $time = localtime($time_t + $offset, true);
498
499         if ($time['tm_yday'] == $today['tm_yday'] && $time['tm_year'] == $today['tm_year'])
500             return _("today");
501
502         // Note that due to daylight savings chages (and leap seconds), $now minus
503         // 24 hours is not guaranteed to be yesterday.
504         $yesterday = localtime($now - (12 + $today['tm_hour']) * 3600, true);
505         if ($time['tm_yday'] == $yesterday['tm_yday']
506             and $time['tm_year'] == $yesterday['tm_year']
507         )
508             return _("yesterday");
509
510         return false;
511     }
512
513     /**
514      * Format the "Author" and "Owner" messages for a page revision.
515      */
516     function getOwnerMessage($page)
517     {
518         if (!ENABLE_PAGEPERM or !class_exists("PagePermission"))
519             return '';
520         $dbi =& $GLOBALS['request']->_dbi;
521         $owner = $page->getOwner();
522         if ($owner) {
523             /*
524             if ( mayAccessPage('change',$page->getName()) )
525                 return fmt("Owner: %s", $this->makeActionButton(array('action'=>_("chown"),
526                                                                       's' => $page->getName()),
527                                                                 $owner, $page));
528             */
529             if ($dbi->isWikiPage($owner))
530                 return fmt("Owner: %s", WikiLink($owner));
531             else
532                 return fmt("Owner: %s", '"' . $owner . '"');
533         }
534         return '';
535     }
536
537     /* New behaviour: (by Matt Brown)
538        Prefer author (name) over internal author_id (IP) */
539     function getAuthorMessage($revision)
540     {
541         if (!$revision) return '';
542         $dbi =& $GLOBALS['request']->_dbi;
543         $author = $revision->get('author');
544         if (!$author) $author = $revision->get('author_id');
545         if (!$author) return '';
546         if ($dbi->isWikiPage($author)) {
547             return fmt("by %s", WikiLink($author));
548         } else {
549             return fmt("by %s", '"' . $author . '"');
550         }
551     }
552
553     ////////////////////////////////////////////////////////////////
554     //
555     // Hooks for other formatting
556     //
557     ////////////////////////////////////////////////////////////////
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     public $_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     public $_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     public $_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     public $_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      * @param array $options
942      *
943      * @return object A Button object.
944      */
945     function makeActionButton($action, $label = '',
946                               $page_or_rev = false, $options = array())
947     {
948         extract($this->_get_name_and_rev($page_or_rev));
949
950         if (is_array($action)) {
951             $attr = $action;
952             $action = isset($attr['action']) ? $attr['action'] : 'browse';
953         } else
954             $attr['action'] = $action;
955
956         $class = is_safe_action($action) ? 'wikiaction' : 'wikiadmin';
957         if (!$label)
958             $label = $this->_labelForAction($action);
959
960         if ($version)
961             $attr['version'] = $version;
962
963         if ($action == 'browse')
964             unset($attr['action']);
965
966         $options = $this->fixAccesskey($options);
967
968         return $this->makeButton($label, WikiURL($pagename, $attr), $class, $options);
969     }
970
971     function tooltipAccessKeyPrefix()
972     {
973         static $tooltipAccessKeyPrefix = null;
974         if ($tooltipAccessKeyPrefix) return $tooltipAccessKeyPrefix;
975
976         $tooltipAccessKeyPrefix = 'alt';
977         if (isBrowserOpera()) $tooltipAccessKeyPrefix = 'shift-esc';
978         elseif (isBrowserSafari() or browserDetect("Mac") or isBrowserKonqueror())
979             $tooltipAccessKeyPrefix = 'ctrl'; // ff2 win and x11 only
980         elseif ((browserDetect("firefox/2") or browserDetect("minefield/3") or browserDetect("SeaMonkey/1.1"))
981             and ((browserDetect("windows") or browserDetect("x11")))
982         )
983             $tooltipAccessKeyPrefix = 'alt-shift';
984         return $tooltipAccessKeyPrefix;
985     }
986
987     /** Define the accesskey in the title only, with ending [p] or [alt-p].
988      *  This fixes the prefix in the title and sets the accesskey.
989      */
990     function fixAccesskey($attrs)
991     {
992         if (!empty($attrs['title']) and preg_match("/\[(alt-)?(.)\]$/", $attrs['title'], $m)) {
993             if (empty($attrs['accesskey'])) $attrs['accesskey'] = $m[2];
994             // firefox 'alt-shift', MSIE: 'alt', ... see wikibits.js
995             $attrs['title'] = preg_replace("/\[(alt-)?(.)\]$/", "[" . $this->tooltipAccessKeyPrefix() . "-\\2]", $attrs['title']);
996         }
997         return $attrs;
998     }
999
1000     /**
1001      * Make a "button" which links to a wiki-page.
1002      *
1003      * These are really just regular WikiLinks, possibly
1004      * disguised (e.g. behind an image button) by the theme.
1005      *
1006      * This method should probably only be used for links
1007      * which appear in page navigation bars, or similar places.
1008      *
1009      * Use linkExistingWikiWord, or LinkWikiWord for normal links.
1010      *
1011      * @param mixed $page_or_rev The page to link to.  This can be
1012      * given as a string (the page name), a WikiDB_Page object, or as
1013      * WikiDB_PageRevision object.  If given as a WikiDB_PageRevision
1014      * object, the button will link to a specific version of the
1015      * designated page, otherwise the button links to the most recent
1016      * version of the page.
1017      *
1018      * @param string $label
1019      *
1020      * @param string $action
1021      *
1022      * @return object A Button object.
1023      */
1024     function makeLinkButton($page_or_rev, $label = '', $action = '')
1025     {
1026         extract($this->_get_name_and_rev($page_or_rev));
1027
1028         $args = $version ? array('version' => $version) : false;
1029         if ($action) $args['action'] = $action;
1030
1031         return $this->makeButton($label ? $label : $pagename,
1032             WikiURL($pagename, $args), 'wiki');
1033     }
1034
1035     function _get_name_and_rev($page_or_rev)
1036     {
1037         $version = false;
1038
1039         if (empty($page_or_rev)) {
1040             global $request;
1041             $pagename = $request->getArg("pagename");
1042             $version = $request->getArg("version");
1043         } elseif (is_object($page_or_rev)) {
1044             if (isa($page_or_rev, 'WikiDB_PageRevision')) {
1045                 $rev = $page_or_rev;
1046                 $page = $rev->getPage();
1047                 if (!$rev->isCurrent()) $version = $rev->getVersion();
1048             } else {
1049                 $page = $page_or_rev;
1050             }
1051             $pagename = $page->getName();
1052         } else {
1053             $pagename = (string)$page_or_rev;
1054         }
1055         return compact('pagename', 'version');
1056     }
1057
1058     function _labelForAction($action)
1059     {
1060         switch ($action) {
1061             case 'edit':
1062                 return _("Edit");
1063             case 'diff':
1064                 return _("Diff");
1065             case 'logout':
1066                 return _("Sign Out");
1067             case 'login':
1068                 return _("Sign In");
1069             case 'rename':
1070                 return _("Rename Page");
1071             case 'lock':
1072                 return _("Lock Page");
1073             case 'unlock':
1074                 return _("Unlock Page");
1075             case 'remove':
1076                 return _("Remove Page");
1077             case 'purge':
1078                 return _("Purge Page");
1079             default:
1080                 // I don't think the rest of these actually get used.
1081                 // 'setprefs'
1082                 // 'upload' 'dumpserial' 'loadfile' 'zip'
1083                 // 'save' 'browse'
1084                 return gettext(ucfirst($action));
1085         }
1086     }
1087
1088     //----------------------------------------------------------------
1089     public $_buttonSeparator = "\n | ";
1090
1091     function setButtonSeparator($separator)
1092     {
1093         $this->_buttonSeparator = $separator;
1094     }
1095
1096     function getButtonSeparator()
1097     {
1098         return $this->_buttonSeparator;
1099     }
1100
1101     ////////////////////////////////////////////////////////////////
1102     //
1103     // CSS
1104     //
1105     // Notes:
1106     //
1107     // Based on testing with Galeon 1.2.7 (Mozilla 1.2):
1108     // Automatic media-based style selection (via <link> tags) only
1109     // seems to work for the default style, not for alternate styles.
1110     //
1111     // Doing
1112     //
1113     //  <link rel="stylesheet" type="text/css" href="phpwiki.css" />
1114     //  <link rel="stylesheet" type="text/css" href="phpwiki-printer.css" media="print" />
1115     //
1116     // works to make it so that the printer style sheet get used
1117     // automatically when printing (or print-previewing) a page
1118     // (but when only when the default style is selected.)
1119     //
1120     // Attempts like:
1121     //
1122     //  <link rel="alternate stylesheet" title="Modern"
1123     //        type="text/css" href="phpwiki-modern.css" />
1124     //  <link rel="alternate stylesheet" title="Modern"
1125     //        type="text/css" href="phpwiki-printer.css" media="print" />
1126     //
1127     // Result in two "Modern" choices when trying to select alternate style.
1128     // If one selects the first of those choices, one gets phpwiki-modern
1129     // both when browsing and printing.  If one selects the second "Modern",
1130     // one gets no CSS when browsing, and phpwiki-printer when printing.
1131     //
1132     // The Real Fix?
1133     // =============
1134     //
1135     // We should probably move to doing the media based style
1136     // switching in the CSS files themselves using, e.g.:
1137     //
1138     //  @import url(print.css) print;
1139     //
1140     ////////////////////////////////////////////////////////////////
1141
1142     function _CSSlink($title, $css_file, $media, $is_alt = false)
1143     {
1144         // Don't set title on default style.  This makes it clear to
1145         // the user which is the default (i.e. most supported) style.
1146         if ($is_alt and isBrowserKonqueror())
1147             return HTML();
1148         $link = HTML::link(array('rel' => $is_alt ? 'alternate stylesheet' : 'stylesheet',
1149             'type' => 'text/css',
1150             'href' => $this->_findData($css_file)));
1151         if ($is_alt)
1152             $link->setAttr('title', $title);
1153
1154         if ($media)
1155             $link->setAttr('media', $media);
1156         if ($this->DUMP_MODE) {
1157             if (empty($this->dumped_css)) $this->dumped_css = array();
1158             if (!in_array($css_file, $this->dumped_css)) $this->dumped_css[] = $css_file;
1159             $link->setAttr('href', basename($link->getAttr('href')));
1160         }
1161
1162         return $link;
1163     }
1164
1165     /** Set default CSS source for this theme.
1166      *
1167      * To set styles to be used for different media, pass a
1168      * hash for the second argument, e.g.
1169      *
1170      * $theme->setDefaultCSS('default', array('' => 'normal.css',
1171      *                                        'print' => 'printer.css'));
1172      *
1173      * If you call this more than once, the last one called takes
1174      * precedence as the default style.
1175      *
1176      * @param string $title Name of style (currently ignored, unless
1177      * you call this more than once, in which case, some of the style
1178      * will become alternate (rather than default) styles, and then their
1179      * titles will be used.
1180      *
1181      * @param mixed $css_files Name of CSS file, or hash containing a mapping
1182      * between media types and CSS file names.  Use a key of '' (the empty string)
1183      * to set the default CSS for non-specified media.  (See above for an example.)
1184      */
1185     function setDefaultCSS($title, $css_files)
1186     {
1187         if (!is_array($css_files))
1188             $css_files = array('' => $css_files);
1189         // Add to the front of $this->_css
1190         unset($this->_css[$title]);
1191         $this->_css = array_merge(array($title => $css_files), $this->_css);
1192     }
1193
1194     /** Set alternate CSS source for this theme.
1195      *
1196      * @param string $title     Name of style.
1197      * @param string $css_files Name of CSS file.
1198      */
1199     function addAlternateCSS($title, $css_files)
1200     {
1201         if (!is_array($css_files))
1202             $css_files = array('' => $css_files);
1203         $this->_css[$title] = $css_files;
1204     }
1205
1206     /**
1207      * @return string HTML for CSS.
1208      */
1209     function getCSS()
1210     {
1211         $css = array();
1212         $is_alt = false;
1213         foreach ($this->_css as $title => $css_files) {
1214             ksort($css_files); // move $css_files[''] to front.
1215             foreach ($css_files as $media => $css_file) {
1216                 if (!empty($this->DUMP_MODE)) {
1217                     if ($media == 'print')
1218                         $css[] = $this->_CSSlink($title, $css_file, '', $is_alt);
1219                 } else {
1220                     $css[] = $this->_CSSlink($title, $css_file, $media, $is_alt);
1221                 }
1222                 if ($is_alt) break;
1223             }
1224             $is_alt = true;
1225         }
1226         return HTML($css);
1227     }
1228
1229     function findTemplate($name)
1230     {
1231         if ($tmp = $this->_findFile("templates/$name.tmpl", 1))
1232             return $this->_path . $tmp;
1233         else {
1234             $f1 = $this->file("templates/$name.tmpl");
1235             foreach ($this->_parents as $parent) {
1236                 if ($tmp = $parent->_findFile("templates/$name.tmpl", 1))
1237                     return $this->_path . $tmp;
1238             }
1239             trigger_error("$f1 not found", E_USER_ERROR);
1240             return false;
1241         }
1242     }
1243
1244     /**
1245      * Add a random header element to head
1246      * TODO: first css, then js. Maybe separate it into addJSHeaders/addCSSHeaders
1247      * or use an optional type argument, and separate it within _MoreHeaders[]
1248      */
1249     //$GLOBALS['request']->_MoreHeaders = array();
1250     function addMoreHeaders($element)
1251     {
1252         $GLOBALS['request']->_MoreHeaders[] = $element;
1253         if (!empty($this->_headers_printed) and $this->_headers_printed) {
1254             trigger_error(_("Some action(page) wanted to add more headers, but they were already printed.")
1255                     . "\n" . $element->asXML(),
1256                 E_USER_NOTICE);
1257         }
1258     }
1259
1260     /**
1261      * Singleton. Only called once, by the head template. See the warning above.
1262      */
1263     function getMoreHeaders()
1264     {
1265         global $request;
1266         // actionpages cannot add headers, because recursive template expansion
1267         // already expanded the head template before.
1268         $this->_headers_printed = 1;
1269         if (empty($request->_MoreHeaders))
1270             return '';
1271         $out = '';
1272         if (false and ($file = $this->_findData('delayed.js'))) {
1273             $request->_MoreHeaders[] = JavaScript('
1274 // Add a script element as a child of the body
1275 function downloadJSAtOnload() {
1276 var element = document.createElement("script");
1277 element.src = "' . $file . '";
1278 document.body.appendChild(element);
1279 }
1280 // Check for browser support of event handling capability
1281 if (window.addEventListener)
1282 window.addEventListener("load", downloadJSAtOnload, false);
1283 else if (window.attachEvent)
1284 window.attachEvent("onload", downloadJSAtOnload);
1285 else window.onload = downloadJSAtOnload;');
1286         }
1287         //$out = "<!-- More Headers -->\n";
1288         foreach ($request->_MoreHeaders as $h) {
1289             if (is_object($h))
1290                 $out .= $h->printXML();
1291             else
1292                 $out .= "$h\n";
1293         }
1294         return $out;
1295     }
1296
1297     //$GLOBALS['request']->_MoreAttr = array();
1298     // new arg: named elements to be able to remove them. such as DoubleClickEdit for htmldumps
1299     function addMoreAttr($tag, $name, $element)
1300     {
1301         global $request;
1302         // protect from duplicate attr (body jscript: themes, prefs, ...)
1303         static $_attr_cache = array();
1304         $hash = md5($tag . "/" . $element);
1305         if (!empty($_attr_cache[$hash])) return;
1306         $_attr_cache[$hash] = 1;
1307
1308         if (empty($request->_MoreAttr) or !is_array($request->_MoreAttr[$tag]))
1309             $request->_MoreAttr[$tag] = array($name => $element);
1310         else
1311             $request->_MoreAttr[$tag][$name] = $element;
1312     }
1313
1314     function getMoreAttr($tag)
1315     {
1316         global $request;
1317         if (empty($request->_MoreAttr[$tag]))
1318             return '';
1319         $out = '';
1320         foreach ($request->_MoreAttr[$tag] as $name => $element) {
1321             if (is_object($element))
1322                 $out .= $element->printXML();
1323             else
1324                 $out .= "$element";
1325         }
1326         return $out;
1327     }
1328
1329     /**
1330      * Common Initialisations
1331      */
1332
1333     /**
1334      * The ->load() method replaces the formerly global code in themeinfo.php.
1335      * This is run only once for the selected theme, and not for the parent themes.
1336      * Without this you would not be able to derive from other themes.
1337      */
1338     function load()
1339     {
1340
1341         $this->initGlobals();
1342
1343         // CSS file defines fonts, colors and background images for this
1344         // style.  The companion '*-heavy.css' file isn't defined, it's just
1345         // expected to be in the same directory that the base style is in.
1346
1347         // This should result in phpwiki-printer.css being used when
1348         // printing or print-previewing with style "PhpWiki" or "MacOSX" selected.
1349         $this->setDefaultCSS('PhpWiki',
1350             array('' => 'phpwiki.css',
1351                 'print' => 'phpwiki-printer.css'));
1352
1353         // This allows one to manually select "Printer" style (when browsing page)
1354         // to see what the printer style looks like.
1355         $this->addAlternateCSS(_("Printer"), 'phpwiki-printer.css', 'print, screen');
1356         $this->addAlternateCSS(_("Top & bottom toolbars"), 'phpwiki-topbottombars.css');
1357         $this->addAlternateCSS(_("Modern"), 'phpwiki-modern.css');
1358
1359         /**
1360          * The logo image appears on every page and links to the HomePage.
1361          */
1362         $this->addImageAlias('logo', WIKI_NAME . 'Logo.png');
1363
1364         $this->addImageAlias('search', 'search.png');
1365
1366         /**
1367          * The Signature image is shown after saving an edited page. If this
1368          * is set to false then the "Thank you for editing..." screen will
1369          * be omitted.
1370          */
1371
1372         $this->addImageAlias('signature', WIKI_NAME . "Signature.png");
1373         // Uncomment this next line to disable the signature.
1374         //$this->addImageAlias('signature', false);
1375
1376         /*
1377          * Link icons.
1378          */
1379         $this->setLinkIcon('http');
1380         $this->setLinkIcon('https');
1381         $this->setLinkIcon('ftp');
1382         $this->setLinkIcon('mailto');
1383         $this->setLinkIcon('interwiki');
1384         $this->setLinkIcon('wikiuser');
1385         $this->setLinkIcon('*', 'url');
1386
1387         $this->setButtonSeparator("\n | ");
1388
1389         /**
1390          * WikiWords can automatically be split by inserting spaces between
1391          * the words. The default is to leave WordsSmashedTogetherLikeSo.
1392          */
1393         $this->setAutosplitWikiWords(false);
1394
1395         /**
1396          * Layout improvement with dangling links for mostly closed wiki's:
1397          * If false, only users with edit permissions will be presented the
1398          * special wikiunknown class with "?" and Tooltip.
1399          * If true (default), any user will see the ?, but will be presented
1400          * the PrintLoginForm on a click.
1401          */
1402         //$this->setAnonEditUnknownLinks(false);
1403
1404         /*
1405          * You may adjust the formats used for formatting dates and times
1406          * below.  (These examples give the default formats.)
1407          * Formats are given as format strings to PHP strftime() function See
1408          * http://www.php.net/manual/en/function.strftime.php for details.
1409          * Do not include the server's zone (%Z), times are converted to the
1410          * user's time zone.
1411          *
1412          * Suggestion for french:
1413          *   $this->setDateFormat("%A %e %B %Y");
1414          *   $this->setTimeFormat("%H:%M:%S");
1415          * Suggestion for capable php versions, using the server locale:
1416          *   $this->setDateFormat("%x");
1417          *   $this->setTimeFormat("%X");
1418          */
1419         //$this->setDateFormat("%B %d, %Y");
1420         //$this->setTimeFormat("%I:%M %p");
1421
1422         /*
1423          * To suppress times in the "Last edited on" messages, give a
1424          * give a second argument of false:
1425          */
1426         //$this->setDateFormat("%B %d, %Y", false);
1427
1428         /**
1429          * Custom UserPreferences:
1430          * A list of name => _UserPreference class pairs.
1431          * Rationale: Certain themes should be able to extend the predefined list
1432          * of preferences. Display/editing is done in the theme specific userprefs.tmpl
1433          * but storage/sanification/update/... must be extended to the Get/SetPreferences methods.
1434          * See themes/wikilens/themeinfo.php
1435          */
1436         //$this->customUserPreference();
1437
1438         /**
1439          * Register custom PageList type and define custom PageList classes.
1440          * Rationale: Certain themes should be able to extend the predefined list
1441          * of pagelist types. E.g. certain plugins, like MostPopular might use
1442          * info=pagename,hits,rating
1443          * which displays the rating column whenever the wikilens theme is active.
1444          * See themes/wikilens/themeinfo.php
1445          */
1446         //$this->addPageListColumn();
1447
1448     } // end of load
1449
1450     /**
1451      * Custom UserPreferences:
1452      * A list of name => _UserPreference class pairs.
1453      * Rationale: Certain themes should be able to extend the predefined list
1454      * of preferences. Display/editing is done in the theme specific userprefs.tmpl
1455      * but storage/sanification/update/... must be extended to the Get/SetPreferences methods.
1456      * These values are just ignored if another theme is used.
1457      */
1458     function customUserPreferences($array)
1459     {
1460         global $customUserPreferenceColumns; // FIXME: really a global?
1461         if (empty($customUserPreferenceColumns)) $customUserPreferenceColumns = array();
1462         //array('wikilens' => new _UserPreference_wikilens());
1463         foreach ($array as $field => $prefobj) {
1464             $customUserPreferenceColumns[$field] = $prefobj;
1465         }
1466     }
1467
1468     /** addPageListColumn(array('rating' => new _PageList_Column_rating('rating', _("Rate"))))
1469      *  Register custom PageList types for special themes, like
1470      *  'rating' for wikilens
1471      */
1472     function addPageListColumn($array)
1473     {
1474         global $customPageListColumns;
1475         if (empty($customPageListColumns)) $customPageListColumns = array();
1476         foreach ($array as $column => $obj) {
1477             $customPageListColumns[$column] = $obj;
1478         }
1479     }
1480
1481     function initGlobals()
1482     {
1483         global $request;
1484         static $already = 0;
1485         if (!$already) {
1486             $script_url = deduce_script_name();
1487             if ((DEBUG & _DEBUG_REMOTE) and isset($_GET['start_debug']))
1488                 $script_url .= ("?start_debug=" . $_GET['start_debug']);
1489             $folderArrowPath = dirname($this->_findData('images/folderArrowLoading.gif'));
1490             $pagename = $request->getArg('pagename');
1491             $js = "var data_path = '" . javascript_quote_string(DATA_PATH) . "';\n"
1492                 // XSS warning with pagename
1493                 . "var pagename  = '" . javascript_quote_string($pagename) . "';\n"
1494                 . "var script_url= '" . javascript_quote_string($script_url) . "';\n"
1495                 . "var stylepath = data_path+'/" . javascript_quote_string($this->_theme) . "/';\n"
1496                 . "var folderArrowPath = '" . javascript_quote_string($folderArrowPath) . "';\n"
1497                 . "var use_path_info = " . (USE_PATH_INFO ? "true" : "false") . ";\n";
1498             $this->addMoreHeaders(JavaScript($js));
1499             $already = 1;
1500         }
1501     }
1502
1503     // Works only on action=browse. Patch #970004 by pixels
1504     // Usage: call $WikiTheme->initDoubleClickEdit() from theme init or
1505     // define ENABLE_DOUBLECLICKEDIT
1506     function initDoubleClickEdit()
1507     {
1508         if (!$this->HTML_DUMP_SUFFIX)
1509             $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';\""));
1510     }
1511
1512     // Immediate title search results via XMLHTML(HttpRequest)
1513     // by Bitflux GmbH, bitflux.ch. You need to install the livesearch.js separately.
1514     // Google's or acdropdown is better.
1515     function initLiveSearch()
1516     {
1517         //subclasses of Sidebar will init this twice
1518         static $already = 0;
1519         if (!$this->HTML_DUMP_SUFFIX and !$already) {
1520             $this->addMoreAttr('body', 'LiveSearch',
1521                 HTML::Raw(" onload=\"liveSearchInit()"));
1522             $this->addMoreHeaders(JavaScript('var liveSearchURI="'
1523                 . WikiURL(_("TitleSearch"), array(), true) . '";'));
1524             $this->addMoreHeaders(JavaScript('', array
1525             ('src' => $this->_findData('livesearch.js'))));
1526             $already = 1;
1527         }
1528     }
1529
1530     // Immediate title search results via XMLHttpRequest
1531     // using the shipped moacdropdown js-lib
1532     function initMoAcDropDown()
1533     {
1534         //subclasses of Sidebar will init this twice
1535         static $already = 0;
1536         if (!$this->HTML_DUMP_SUFFIX and !$already) {
1537             $dir = $this->_findData('moacdropdown');
1538             if (!DEBUG and ($css = $this->_findFile('moacdropdown/css/dropdown.css'))) {
1539                 $this->addMoreHeaders($this->_CSSlink(0, $css, 'all'));
1540             } else {
1541                 $this->addMoreHeaders(HTML::style(array('type' => 'text/css'), "  @import url( $dir/css/dropdown.css );\n"));
1542             }
1543             $already = 1;
1544         }
1545     }
1546
1547     function calendarLink($date = false)
1548     {
1549         return $this->calendarBase() . SUBPAGE_SEPARATOR .
1550             strftime("%Y-%m-%d", $date ? $date : time());
1551     }
1552
1553     function calendarBase()
1554     {
1555         static $UserCalPageTitle = false;
1556         global $request;
1557
1558         if (!$UserCalPageTitle)
1559             $UserCalPageTitle = $request->_user->getId() .
1560                 SUBPAGE_SEPARATOR . _("Calendar");
1561         if (!$UserCalPageTitle)
1562             $UserCalPageTitle = (BLOG_EMPTY_DEFAULT_PREFIX ? ''
1563                 : ($request->_user->getId() . SUBPAGE_SEPARATOR)) . "Blog";
1564         return $UserCalPageTitle;
1565     }
1566
1567     function calendarInit($force = false)
1568     {
1569         $dbi = $GLOBALS['request']->getDbh();
1570         // display flat calender dhtml in the sidebar
1571         if ($force or $dbi->isWikiPage($this->calendarBase())) {
1572             $jslang = @$GLOBALS['LANG'];
1573             $this->addMoreHeaders
1574             (
1575                 $this->_CSSlink(0,
1576                     $this->_findFile('jscalendar/calendar-phpwiki.css'), 'all'));
1577             $this->addMoreHeaders
1578             (JavaScript('',
1579                 array('src' => $this->_findData('jscalendar/calendar' . (DEBUG ? '' : '_stripped') . '.js'))));
1580             if (!($langfile = $this->_findData("jscalendar/lang/calendar-$jslang.js")))
1581                 $langfile = $this->_findData("jscalendar/lang/calendar-en.js");
1582             $this->addMoreHeaders(JavaScript('', array('src' => $langfile)));
1583             $this->addMoreHeaders
1584             (JavaScript('',
1585                 array('src' =>
1586                 $this->_findData('jscalendar/calendar-setup' . (DEBUG ? '' : '_stripped') . '.js'))));
1587
1588             // Get existing date entries for the current user
1589             require_once 'lib/TextSearchQuery.php';
1590             $iter = $dbi->titleSearch(new TextSearchQuery("^" . $this->calendarBase() . SUBPAGE_SEPARATOR, true, "auto"));
1591             $existing = array();
1592             while ($page = $iter->next()) {
1593                 if ($page->exists())
1594                     $existing[] = basename($page->_pagename);
1595             }
1596             if (!empty($existing)) {
1597                 $js_exist = '{"' . join('":1,"', $existing) . '":1}';
1598                 //var SPECIAL_DAYS = {"2004-05-11":1,"2004-05-12":1,"2004-06-01":1}
1599                 $this->addMoreHeaders(JavaScript('
1600 /* This table holds the existing calender entries for the current user
1601  *  calculated from the database
1602  */
1603
1604 var SPECIAL_DAYS = ' . javascript_quote_string($js_exist) . ';
1605
1606 /* This function returns true if the date exists in SPECIAL_DAYS */
1607 function dateExists(date, y, m, d) {
1608     var year = date.getFullYear();
1609     m = m + 1;
1610     m = m < 10 ? "0" + m : m;  // integer, 0..11
1611     d = d < 10 ? "0" + d : d;  // integer, 1..31
1612     var date = year+"-"+m+"-"+d;
1613     var exists = SPECIAL_DAYS[date];
1614     if (!exists) return false;
1615     else return true;
1616 }
1617 // This is the actual date status handler.
1618 // Note that it receives the date object as well as separate
1619 // values of year, month and date.
1620 function dateStatusFunc(date, y, m, d) {
1621     if (dateExists(date, y, m, d)) return "existing";
1622     else return false;
1623 }
1624 '));
1625             } else {
1626                 $this->addMoreHeaders(JavaScript('
1627 function dateStatusFunc(date, y, m, d) { return false;}'));
1628             }
1629         }
1630     }
1631
1632     ////////////////////////////////////////////////////////////////
1633     //
1634     // Events
1635     //
1636     ////////////////////////////////////////////////////////////////
1637
1638     /**  CbUserLogin (&$request, $userid)
1639      * Callback when a user logs in
1640      */
1641     function CbUserLogin(&$request, $userid)
1642     {
1643         ; // do nothing
1644     }
1645
1646     /** CbNewUserEdit (&$request, $userid)
1647      * Callback when a new user creates or edits a page
1648      */
1649     function CbNewUserEdit(&$request, $userid)
1650     {
1651         ; // i.e. create homepage with Template/UserPage
1652     }
1653
1654     /** CbNewUserLogin (&$request, $userid)
1655      * Callback when a "new user" logs in.
1656      *  What is new? We only record changes, not logins.
1657      *  Should we track user actions?
1658      *  Let's say a new user is a user without homepage.
1659      */
1660     function CbNewUserLogin(&$request, $userid)
1661     {
1662         ; // do nothing
1663     }
1664
1665     /** CbUserLogout (&$request, $userid)
1666      * Callback when a user logs out
1667      */
1668     function CbUserLogout(&$request, $userid)
1669     {
1670         ; // do nothing
1671     }
1672
1673 }
1674
1675 /**
1676  * A class representing a clickable "button".
1677  *
1678  * In it's simplest (default) form, a "button" is just a link associated
1679  * with some sort of wiki-action.
1680  */
1681 class Button extends HtmlElement
1682 {
1683     /** Constructor
1684      *
1685      * @param string $text The text for the button.
1686      * @param string $url The url (href) for the button.
1687      * @param string $class The CSS class for the button.
1688      * @param array $options  Additional attributes for the &lt;input&gt; tag.
1689      */
1690     function Button($text, $url, $class = '', $options = array())
1691     {
1692         global $request;
1693         $this->_init('a', array('href' => $url));
1694         if ($class)
1695             $this->setAttr('class', $class);
1696         if ($request->getArg('frame'))
1697             $this->setAttr('target', '_top');
1698         if (!empty($options) and is_array($options)) {
1699             foreach ($options as $key => $val)
1700                 $this->setAttr($key, $val);
1701         }
1702         // Google honors this
1703         if (in_array(strtolower($text), array('edit', 'create', 'diff', 'pdf'))
1704             and !$request->_user->isAuthenticated()
1705         )
1706             $this->setAttr('rel', 'nofollow');
1707         $this->pushContent($GLOBALS['WikiTheme']->maybeSplitWikiWord($text));
1708     }
1709
1710 }
1711
1712 /**
1713  * A clickable image button.
1714  */
1715 class ImageButton extends Button
1716 {
1717     /** Constructor
1718      *
1719      * @param $text string The text for the button.
1720      * @param $url string The url (href) for the button.
1721      * @param $class string The CSS class for the button.
1722      * @param $img_url string URL for button's image.
1723      * @param $img_attr array Additional attributes for the &lt;img&gt; tag.
1724      */
1725     function ImageButton($text, $url, $class, $img_url, $img_attr = array())
1726     {
1727         $this->__construct('a', array('href' => $url));
1728         if ($class)
1729             $this->setAttr('class', $class);
1730         // Google honors this
1731         if (in_array(strtolower($text), array('edit', 'create', 'diff', 'pdf'))
1732             and !$GLOBALS['request']->_user->isAuthenticated()
1733         )
1734             $this->setAttr('rel', 'nofollow');
1735
1736         if (!is_array($img_attr))
1737             $img_attr = array();
1738         $img_attr['src'] = $img_url;
1739         $img_attr['alt'] = $text;
1740         $img_attr['class'] = 'wiki-button';
1741         $this->pushContent(HTML::img($img_attr));
1742     }
1743 }
1744
1745 /**
1746  * A class representing a form <samp>submit</samp> button.
1747  */
1748 class SubmitButton extends HtmlElement
1749 {
1750     /** Constructor
1751      *
1752      * @param $text string The text for the button.
1753      * @param $name string The name of the form field.
1754      * @param $class string The CSS class for the button.
1755      * @param $options array Additional attributes for the &lt;input&gt; tag.
1756      */
1757     function SubmitButton($text, $name = '', $class = '', $options = array())
1758     {
1759         $this->__construct('input', array('type' => 'submit',
1760             'value' => $text));
1761         if ($name)
1762             $this->setAttr('name', $name);
1763         if ($class)
1764             $this->setAttr('class', $class);
1765         if (!empty($options)) {
1766             foreach ($options as $key => $val)
1767                 $this->setAttr($key, $val);
1768         }
1769     }
1770
1771 }
1772
1773 /**
1774  * A class representing an image form <samp>submit</samp> button.
1775  */
1776 class SubmitImageButton extends SubmitButton
1777 {
1778     /** Constructor
1779      *
1780      * @param $text string The text for the button.
1781      * @param $name string The name of the form field.
1782      * @param $class string The CSS class for the button.
1783      * @param $img_url string URL for button's image.
1784      * @param $img_attr array Additional attributes for the &lt;img&gt; tag.
1785      */
1786     function SubmitImageButton($text, $name = '', $class = '', $img_url, $img_attr = array())
1787     {
1788         $this->__construct('input', array('type' => 'image',
1789             'src' => $img_url,
1790             'value' => $text,
1791             'alt' => $text));
1792         if ($name)
1793             $this->setAttr('name', $name);
1794         if ($class)
1795             $this->setAttr('class', $class);
1796         if (!empty($img_attr)) {
1797             foreach ($img_attr as $key => $val)
1798                 $this->setAttr($key, $val);
1799         }
1800     }
1801
1802 }
1803
1804 /**
1805  * A sidebar box with title and body, narrow fixed-width.
1806  * To represent abbrevated content of plugins, links or forms,
1807  * like "Getting Started", "Search", "Sarch Pagename",
1808  * "Login", "Menu", "Recent Changes", "Last comments", "Last Blogs"
1809  * "Calendar"
1810  * ... See http://tikiwiki.org/
1811  *
1812  * Usage:
1813  * sidebar.tmpl:
1814  *   $menu = SidebarBox("Menu",HTML::dl(HTML::dt(...))); $menu->format();
1815  *   $menu = PluginSidebarBox("RecentChanges",array('limit'=>10)); $menu->format();
1816  */
1817 class SidebarBox
1818 {
1819
1820     function SidebarBox($title, $body)
1821     {
1822         require_once 'lib/WikiPlugin.php';
1823         $this->title = $title;
1824         $this->body = $body;
1825     }
1826
1827     function format()
1828     {
1829         return WikiPlugin::makeBox($this->title, $this->body);
1830     }
1831 }
1832
1833 /**
1834  * A sidebar box for plugins.
1835  * Any plugin may provide a box($args=false, $request=false, $basepage=false)
1836  * method, with the help of WikiPlugin::makeBox()
1837  */
1838 class PluginSidebarBox extends SidebarBox
1839 {
1840
1841     public $_plugin, $_args = false, $_basepage = false;
1842
1843     function PluginSidebarBox($name, $args = false, $basepage = false)
1844     {
1845         require_once 'lib/WikiPlugin.php';
1846
1847         $loader = new WikiPluginLoader();
1848         $plugin = $loader->getPlugin($name);
1849         if (!$plugin) {
1850             return $loader->_error(sprintf(_("Plugin %s: undefined"),
1851                 $name));
1852         }
1853         /*
1854                 if (!method_exists($plugin, 'box')) {
1855                     return $loader->_error(sprintf(_("%s: has no box method"),
1856                                                    get_class($plugin)));
1857                 }*/
1858         $this->_plugin =& $plugin;
1859         $this->_args = $args ? $args : array();
1860         $this->_basepage = $basepage;
1861     }
1862
1863     function format($args = false)
1864     {
1865         return $this->_plugin->box($args ? array_merge($this->_args, $args) : $this->_args,
1866             $GLOBALS['request'],
1867             $this->_basepage);
1868     }
1869 }
1870
1871 // Various boxes which are no plugins
1872 class RelatedLinksBox extends SidebarBox
1873 {
1874     function RelatedLinksBox($title = false, $body = '', $limit = 20)
1875     {
1876         global $request;
1877         $this->title = $title ? $title : _("Related Links");
1878         $this->body = HTML($body);
1879         $page = $request->getPage($request->getArg('pagename'));
1880         $revision = $page->getCurrentRevision();
1881         $page_content = $revision->getTransformedContent();
1882         //$cache = &$page->_wikidb->_cache;
1883         $counter = 0;
1884         $sp = HTML::Raw('&middot; ');
1885         foreach ($page_content->getWikiPageLinks() as $link) {
1886             $linkto = $link['linkto'];
1887             if (!$request->_dbi->isWikiPage($linkto)) continue;
1888             $this->body->pushContent($sp, WikiLink($linkto), HTML::br());
1889             $counter++;
1890             if ($limit and $counter > $limit) continue;
1891         }
1892     }
1893 }
1894
1895 class RelatedExternalLinksBox extends SidebarBox
1896 {
1897     function RelatedExternalLinksBox($title = false, $body = '', $limit = 20)
1898     {
1899         global $request;
1900         $this->title = $title ? $title : _("External Links");
1901         $this->body = HTML($body);
1902         $page = $request->getPage($request->getArg('pagename'));
1903         $cache = &$page->_wikidb->_cache;
1904         $counter = 0;
1905         $sp = HTML::Raw('&middot; ');
1906         foreach ($cache->getWikiPageLinks() as $link) {
1907             $linkto = $link['linkto'];
1908             if ($linkto) {
1909                 $this->body->pushContent($sp, WikiLink($linkto), HTML::br());
1910                 $counter++;
1911                 if ($limit and $counter > $limit) continue;
1912             }
1913         }
1914     }
1915 }
1916
1917 function listAvailableThemes()
1918 {
1919     $available_themes = array();
1920     $dir_root = 'themes';
1921     if (defined('PHPWIKI_DIR'))
1922         $dir_root = PHPWIKI_DIR . "/$dir_root";
1923     $dir = dir($dir_root);
1924     if ($dir) {
1925         while ($entry = $dir->read()) {
1926             if (is_dir($dir_root . '/' . $entry)
1927                 and file_exists($dir_root . '/' . $entry . '/themeinfo.php')
1928             ) {
1929                 array_push($available_themes, $entry);
1930             }
1931         }
1932         $dir->close();
1933     }
1934     return $available_themes;
1935 }
1936
1937 function listAvailableLanguages()
1938 {
1939     $available_languages = array('en');
1940     $dir_root = 'locale';
1941     if (defined('PHPWIKI_DIR'))
1942         $dir_root = PHPWIKI_DIR . "/$dir_root";
1943     if ($dir = dir($dir_root)) {
1944         while ($entry = $dir->read()) {
1945             if (is_dir($dir_root . "/" . $entry) and is_dir($dir_root . '/' . $entry . '/LC_MESSAGES')) {
1946                 array_push($available_languages, $entry);
1947             }
1948         }
1949         $dir->close();
1950     }
1951     return $available_languages;
1952 }
1953
1954 // Local Variables:
1955 // mode: php
1956 // tab-width: 8
1957 // c-basic-offset: 4
1958 // c-hanging-comment-ender-p: nil
1959 // indent-tabs-mode: nil
1960 // End: