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