]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/Theme.php
Enhancements to format dates as relative dates, such as "Today" or "Yesterday" when...
[SourceForge/phpwiki.git] / lib / Theme.php
1 <?php rcs_id('$Id: Theme.php,v 1.31 2002-02-03 22:12:34 carstenklapp Exp $');
2
3 require_once('lib/HtmlElement.php');
4
5
6 /**
7  * Make a link to a wiki page (in this wiki).
8  *
9  * This is a convenience function.
10  *
11  * @param $page_or_rev mixed
12  * Can be:<dl>
13  * <dt>A string</dt><dd>The page to link to.</dd>
14  * <dt>A WikiDB_Page object</dt><dd>The page to link to.</dd>
15  * <dt>A WikiDB_PageRevision object</dt><dd>A specific version of the page to link to.</dd>
16  * </dl>
17  *
18  * @param $type string
19  * One of:<dl>
20  * <dt>'unknown'</dt><dd>Make link appropriate for a non-existant page.</dd>
21  * <dt>'known'</dt><dd>Make link appropriate for an existing page.</dd>
22  * <dt>'auto'</dt><dd>Either 'unknown' or 'known' as appropriate.</dd>
23  * <dt>'button'</dt><dd>Make a button-style link.</dd>
24  * <dt>false (default)</dt><dd>Equivalent to 'known'.</dd>
25  * </dl>
26  * Unless $type of of the latter form, the link will be of class 'wiki', 'wikiunknown',
27  * 'named-wiki', or 'named-wikiunknown', as appropriate.
28  *
29  * @param $label mixed (string or XmlContent object)
30  * Label for the link.  If not given, defaults to the page name.
31  * (Label is ignored for $type == 'button'.)
32  */
33 function WikiLink ($page_or_rev, $type = false, $label = false) {
34     global $Theme;
35
36     if ($type == 'button') {
37         return $Theme->makeLinkButton($page_or_rev);
38     }
39
40     $version = false;
41     
42     if (isa($page_or_rev, 'WikiDB_PageRevision')) {
43         $version = $page_or_rev->getVersion();
44         $page = $page_or_rev->getPage();
45         $pagename = $page->getName();
46         $exists = true;
47     }
48     elseif (isa($page_or_rev, 'WikiDB_Page')) {
49         $page = $page_or_rev;
50         $pagename = $page->getName();
51     }
52     else {
53         $pagename = $page_or_rev;
54     }
55
56     if ($type === false || $type == 'auto') {
57         if (isset($page)) {
58             $current = $page->getCurrentRevision();
59             $exists = ! $current->hasDefaultContents();
60         }
61         else {
62             global $request;
63             $dbi = $request->getDbh();
64             $exists = $dbi->isWikiPage($pagename);
65         }
66     }
67     elseif ($type == 'unknown') {
68         $exists = false;
69     }
70     else {
71         $exists = true;
72     }
73     
74
75     if ($exists)
76         return $Theme->linkExistingWikiWord($pagename, $label, $version);
77     else
78         return $Theme->linkUnknownWikiWord($pagename, $label);
79 }
80
81      
82
83 /**
84  * Make a button.
85  *
86  * This is a convenience function.
87  *
88  * @param $action string
89  * One of <dl>
90  * <dt>[action]</dt><dd>Perform action (e.g. 'edit') on the selected page.</dd>
91  * <dt>[ActionPage]</dt><dd>Run the actionpage (e.g. 'BackLinks') on the selected page.</dd>
92  * <dt>'submit:'[name]</dt><dd>Make a form submission button with the given name.
93  *      ([name] can be blank for a nameless submit button.)</dd>
94  * <dt>a hash</dt><dd>Query args for the action. E.g.<pre>
95  *      array('action' => 'diff', 'previous' => 'author')
96  * </pre></dd>
97  * </dl>
98  *
99  * @param $label string
100  * A label for the button.  If ommited, a suitable default (based on the valued of $action
101  * will be picked.
102  *
103  * @param $page_or_rev mixed
104  * Which page (& version) to perform the action on.
105  * Can be one of:<dl>
106  * <dt>A string</dt><dd>The pagename.</dd>
107  * <dt>A WikiDB_Page object</dt><dd>The page.</dd>
108  * <dt>A WikiDB_PageRevision object</dt><dd>A specific version of the page.</dd>
109  * </dl>
110  * ($Page_or_rev is ignored for submit buttons.)
111  */
112 function Button ($action, $label = false, $page_or_rev = false) {
113     global $Theme;
114
115     if (!is_array($action) && preg_match('/submit:(.*)/A', $action, $m))
116         return $Theme->makeSubmitButton($label, $m[1]);
117     else
118         return $Theme->makeActionButton($action, $label, $page_or_rev);
119 }
120
121
122
123
124 class Theme {
125     function Theme ($theme_name = 'default') {
126         $this->_name = $theme_name;
127         $themes_dir = defined('PHPWIKI_DIR') ? PHPWIKI_DIR . "/themes" : "themes";
128         
129         $this->_path  = defined('PHPWIKI_DIR') ? PHPWIKI_DIR . "/" : "";
130         $this->_theme = "themes/$theme_name";
131
132         if ($theme_name != 'default')
133             $this->_default_theme = new Theme;
134     }
135
136     function file ($file) {
137         return $this->_path . "$this->_theme/$file";
138     }
139
140     function _findFile ($file, $missing_okay = false) {
141         if (file_exists($this->_path . "$this->_theme/$file"))
142             return "$this->_theme/$file";
143         
144         // FIXME: this is a short-term hack.  Delete this after all files
145         // get moved into themes/...
146         if (file_exists($this->_path . $file))
147             return $file;
148         
149
150         if (isset($this->_default_theme)) {
151             return $this->_default_theme->_findFile($file, $missing_okay);
152         }
153         else if (!$missing_okay) {
154             trigger_error("$file: not found", E_USER_ERROR);
155         }
156         return false;
157     }
158
159     function _findData ($file, $missing_okay = false) {
160         $path = $this->_findFile($file, $missing_okay);
161         if (!$path)
162             return false;
163         
164         if (defined('DATA_PATH'))
165             return DATA_PATH . "/$path";
166         return $path;
167     }
168
169     ////////////////////////////////////////////////////////////////
170     //
171     // Date and Time formatting
172     //
173     ////////////////////////////////////////////////////////////////
174     
175     var $_dateTimeFormat = "%B %e, %Y";
176     var $_dateFormat = "%B %e, %Y";
177     var $_timeFormat = "%l:%M %p";
178     //var $_timeFormat = "%l:%M:%S %p";
179
180     function setDateFormat ($fs) {
181         $this->_dateFormat = $fs;
182     }
183
184     function formatDate ($time_t) {
185         $offset_time = $time_t + PrefTimezoneOffset();
186
187         if (istoday($offset_time))
188             $date = _("Today");
189         else if (isyesterday($offset_time))
190             $date = _("Yesterday");
191         else
192             $date = strftime($this->_dateFormat, $offset_time);
193         return HTML($date, HTML::small("*"));
194         // The asterisk is temporary, for debugging it indicates a
195         // time has been converted to the user's local time.
196     }
197
198     function setDateTimeFormat ($fs) {
199         $this->_dateTimeFormat = $fs;
200     }
201
202     function formatDateTime ($time_t) {
203         $offset_time = $time_t + PrefTimezoneOffset();
204         if (istoday($offset_time))
205             $date = sprintf(_("Today at %s"), strtolower(strftime($this->_timeFormat, $offset_time)));
206         else if (isyesterday($offset_time))
207             $date = sprintf(_("Yesterday at %s"), strtolower(strftime($this->_timeFormat, $offset_time)));
208         else
209             $date = strftime($this->_dateTimeFormat, $offset_time);
210         return HTML($date, HTML::small("*"));
211         // The asterisk is temporary, for debugging it indicates a
212         // time has been converted to the user's local time.
213     }
214
215     
216     function formatTime ($time_t) {
217         //FIXME: make 24-hour mode configurable?
218         $offset_time = $time_t + PrefTimezoneOffset();
219         return HTML(preg_replace('/^0/', ' ',
220                             strtolower(strftime("%I:%M %p", $offset_time))),
221                     HTML::small("*"));
222         // The asterisk is temporary, for debugging it indicates a
223         // time has been converted to the user's local time.
224     }
225
226
227     ////////////////////////////////////////////////////////////////
228     //
229     // Hooks for other formatting
230     //
231     ////////////////////////////////////////////////////////////////
232
233     //FIXME: PHP 4.1 Warnings
234     //lib/Theme.php:84: Notice[8]: The call_user_method() function is deprecated,
235     //use the call_user_func variety with the array(&$obj, "method") syntax instead
236
237     function getFormatter ($type, $format) {
238         $method = strtolower("get${type}Formatter");
239         if (method_exists($this, $method))
240             return $this->{$method}($format);
241         return false;
242     }
243
244     ////////////////////////////////////////////////////////////////
245     //
246     // Links
247     //
248     ////////////////////////////////////////////////////////////////
249
250     var $_autosplitWikiWords = false;
251     
252     function setAutosplitWikiWords($autosplit=false) {
253         $this->_autosplitWikiWords = $autosplit ? true : false;
254     }
255
256     function maybeSplitWikiWord ($wikiword) {
257         if ($this->_autosplitWikiWords)
258             return split_pagename($wikiword);
259         else
260             return $wikiword;
261     }
262
263     function linkExistingWikiWord($wikiword, $linktext = '', $version = false) {
264         if ($version !== false)
265             $url = WikiURL($wikiword, array('version' => $version));
266         else
267             $url = WikiURL($wikiword);
268
269         $link = HTML::a(array('href' => $url));
270
271         if (!empty($linktext)) {
272             $link->pushContent($linktext);
273             $link->setAttr('class', 'named-wiki');
274             $link->setAttr('title', $this->maybeSplitWikiWord($wikiword));
275         }
276         else {
277             $link->pushContent($this->maybeSplitWikiWord($wikiword));
278             $link->setAttr('class', 'wiki');
279         }
280         return $link;
281     }
282
283     function linkUnknownWikiWord($wikiword, $linktext = '') {
284         $url = WikiURL($wikiword, array('action' => 'edit'));
285         //$link = HTML::span(HTML::a(array('href' => $url), '?'));
286         $button = $this->makeButton('?', $url);
287         $button->addTooltip(sprintf(_("Edit: %s"), $wikiword));
288         $link = HTML::span($button);
289         
290         
291         if (!empty($linktext)) {
292             $link->pushContent(HTML::u($linktext));
293             $link->setAttr('class', 'named-wikiunknown');
294         }
295         else {
296             $link->pushContent(HTML::u($this->maybeSplitWikiWord($wikiword)));
297             $link->setAttr('class', 'wikiunknown');
298         }
299         
300         return $link;
301     }
302
303     ////////////////////////////////////////////////////////////////
304     //
305     // Images and Icons
306     //
307     ////////////////////////////////////////////////////////////////
308
309     /**
310      *
311      * (To disable an image, alias the image to <code>false</code>.
312      */
313     function addImageAlias ($alias, $image_name) {
314         $this->_imageAliases[$alias] = $image_name;
315     }
316     
317     function getImageURL ($image) {
318         $aliases = &$this->_imageAliases;
319         
320         if (isset($aliases[$image])) {
321             $image = $aliases[$image];
322             if (!$image)
323                 return false;
324         }
325
326         // If not extension, default to .png.
327         if (!preg_match('/\.\w+$/', $image))
328             $image .= '.png';
329
330         // FIXME: this should probably be made to fall back
331         //        automatically to .gif, .jpg.
332         //        Also try .gif before .png if browser doesn't like png.
333         
334         return $this->_findData("images/$image", 'missing okay');
335     }
336
337     function setLinkIcon($proto, $image = false) {
338         if (!$image)
339             $image = $proto;
340         
341         $this->_linkIcons[$proto] = $image;
342     }
343     
344     function getLinkIconURL ($proto) {
345         $icons = &$this->_linkIcons;
346         if (!empty($icons[$proto]))
347             return $this->getImageURL($icons[$proto]);
348         elseif (!empty($icons['*']))
349             return $this->getImageURL($icons['*']);
350         return false;
351     }
352
353     function addButtonAlias ($text, $alias = false) {
354         $aliases = &$this->_buttonAliases;
355
356         if (is_array($text))
357             $aliases = array_merge($aliases, $text);
358         elseif ($alias === false)
359             unset($aliases[$text]);
360         else
361             $aliases[$text] = $alias;
362     }
363
364     function getButtonURL ($text) {
365         $aliases = &$this->_buttonAliases;
366         if (isset($aliases[$text]))
367             $text = $aliases[$text];
368
369         $qtext = urlencode($text);
370         $url = $this->_findButton("$qtext.png");
371         if ($url && strstr($url, '%')) {
372             $url = preg_replace('|([^/]+)$|e', 'urlencode("\\1")', $url);
373         }
374         return $url;
375     }
376
377     function _findButton ($button_file) {
378         if (!isset($this->_button_path))
379             $this->_button_path = $this->_getButtonPath();
380         
381         foreach ($this->_button_path as $dir) {
382             $path = "$this->_theme/$dir/$button_file";
383             if (file_exists($this->_path . $path))
384                 return defined('DATA_PATH') ? DATA_PATH . "/$path" : $path;
385         }
386         return false;
387     }
388
389     function _getButtonPath () {
390         $button_dir = $this->file("buttons");
391         if (!file_exists($button_dir) || !is_dir($button_dir))
392             return array();
393
394         $path = array('buttons');
395
396         $dir = dir($button_dir);
397         while (($subdir = $dir->read()) !== false) {
398             if ($subdir[0] == '.')
399                 continue;
400             if (is_dir("$button_dir/$subdir"))
401                 $path[] = "buttons/$subdir";
402         }
403         $dir->close();
404         
405         return $path;
406     }
407         
408     ////////////////////////////////////////////////////////////////
409     //
410     // Button style
411     //
412     ////////////////////////////////////////////////////////////////
413
414     function makeButton ($text, $url, $class = false) {
415         // FIXME: don't always try for image button?
416
417         // Special case: URLs like 'submit:preview' generate form
418         // submission buttons.
419         if (preg_match('/^submit:(.*)$/', $url, $m))
420             return $this->makeSubmitButton($text, $m[1], $class);
421         
422         $imgurl = $this->getButtonURL($text);
423         if ($imgurl)
424             return new ImageButton($text, $url, $class, $imgurl);
425         else
426             return new Button($text, $url, $class);
427     }
428
429     function makeSubmitButton ($text, $name, $class = false) {
430         $imgurl = $this->getButtonURL($text);
431
432         if ($imgurl)
433             return new SubmitImageButton($text, $name, $class, $imgurl);
434         else
435             return new SubmitButton($text, $name, $class);
436     }
437
438     /**
439      * Make button to perform action.
440      *
441      * This constructs a button which performs an action on the
442      * currently selected version of the current page.  (Or another
443      * page or version, if you want...)
444      *
445      * @param $action string The action to perform (e.g. 'edit', 'lock').
446      * This can also be the name of an "action page" like 'LikePages'.
447      * Alternatively you can give a hash of query args to be applied
448      * to the page.
449      *
450      * @param $label string Textual label for the button.  If left empty,
451      * a suitable name will be guessed.
452      *
453      * @param $page_or_rev mixed  The page to link to.  This can be
454      * given as a string (the page name), a WikiDB_Page object, or as
455      * WikiDB_PageRevision object.  If given as a WikiDB_PageRevision
456      * object, the button will link to a specific version of the
457      * designated page, otherwise the button links to the most recent
458      * version of the page.
459      *
460      * @return object A Button object.
461      */
462     function makeActionButton ($action, $label = false, $page_or_rev = false) {
463         extract($this->_get_name_and_rev($page_or_rev));
464
465         if (is_array($action)) {
466             $attr = $action;
467             $action = isset($attr['action']) ? $attr['action'] : 'browse';
468         }
469         else
470             $attr['action'] = $action;
471
472         $class = is_safe_action($action) ? 'wikiaction' : 'wikiadmin';
473         if (!$label)
474             $label = $this->_labelForAction($action);
475
476         if ($version)
477             $attr['version'] = $version;
478
479         if ($action == 'browse')
480             unset($attr['action']);
481
482         return $this->makeButton($label, WikiURL($pagename, $attr), $class);
483     }
484
485     /**
486      * Make a "button" which links to a wiki-page.
487      *
488      * These are really just regular WikiLinks, possibly
489      * disguised (e.g. behind an image button) by the theme.
490      *
491      * This method should probably only be used for links
492      * which appear in page navigation bars, or similar places.
493      *
494      * Use linkExistingWikiWord, or LinkWikiWord for normal links.
495      *
496      * @param $page_or_rev mixed The page to link to.  This can be
497      * given as a string (the page name), a WikiDB_Page object, or as
498      * WikiDB_PageRevision object.  If given as a WikiDB_PageRevision
499      * object, the button will link to a specific version of the
500      * designated page, otherwise the button links to the most recent
501      * version of the page.
502      *
503      * @return object A Button object.
504      */
505     function makeLinkButton ($page_or_rev) {
506         extract($this->_get_name_and_rev($page_or_rev));
507         
508         $args = $version ? array('version' => $version) : false;
509         
510         return $this->makeButton($pagename, WikiURL($pagename, $args), 'wiki');
511     }
512
513     function _get_name_and_rev ($page_or_rev) {
514         $version = false;
515         
516         if (empty($page_or_rev)) {
517             global $request;
518             $pagename = $request->getArg("pagename");
519             $version = $request->getArg("version");
520         }
521         elseif (is_object($page_or_rev)) {
522             if (isa($page_or_rev, 'WikiDB_PageRevision')) {
523                 $rev = $page_or_rev;
524                 $page = $rev->getPage();
525                 $version = $rev->getVersion();
526             }
527             else {
528                 $page = $page_or_rev;
529             }
530             $pagename = $page->getName();
531         }
532         else {
533             $pagename = (string) $page_or_rev;
534         }
535         return compact('pagename', 'version');
536     }
537
538     function _labelForAction ($action) {
539         switch ($action) {
540         case 'edit':    return _("Edit");
541         case 'diff':    return _("Diff");
542         case 'logout':  return _("Sign Out");
543         case 'login':   return _("Sign In");
544         case 'lock':    return _("Lock Page");
545         case 'unlock':  return _("Unlock Page");
546         case 'remove':  return _("Remove Page");
547         default:
548             // I don't think the rest of these actually get used.
549             // 'setprefs'
550             // 'upload' 'dumpserial' 'loadfile' 'zip'
551             // 'save' 'browse'
552             return ucfirst($action);
553         }
554     }
555  
556     //----------------------------------------------------------------
557     var $_buttonSeparator = ' | ';
558     
559     function setButtonSeparator($separator) {
560         $this->_buttonSeparator = $separator;
561     }
562
563     function getButtonSeparator() {
564         return $this->_buttonSeparator;
565     }
566
567
568     ////////////////////////////////////////////////////////////////
569     //
570     // CSS
571     //
572     ////////////////////////////////////////////////////////////////
573     
574     function _CSSlink($title, $css_file, $media, $is_alt = false) {
575         $link = HTML::link(array('rel'    => $is_alt ? 'alternate stylesheet' : 'stylesheet',
576                                  'title'  => $title,
577                                  'type'   => 'text/css',
578                                  'charset'=> CHARSET,
579                                  'href'   => $this->_findData($css_file)));
580         if ($media)
581             $link->setAttr('media', $media);
582         return $link;
583     }
584
585     function setDefaultCSS ($title, $css_file, $media = false) {
586         if (isset($this->_alternateCSS))
587             unset($this->_alternateCSS[$title]);
588         $this->_defaultCSS = $this->_CSSlink($title, $css_file, $media);
589     }
590
591     function addAlternateCSS ($title, $css_file, $media = false) {
592         $this->_alternateCSS[$title] = $this->_CSSlink($title, $css_file, $media, true);
593     }
594     
595     /**
596      * @return string HTML for CSS.
597      */
598     function getCSS () {
599         $css = HTML($this->_defaultCSS);
600         if (!empty($this->_alternateCSS))
601             $css->pushContent($this->_alternateCSS);
602         return $css;
603     }
604
605     function findTemplate ($name) {
606         return $this->_path . $this->_findFile("templates/$name.tmpl");
607     }
608 };
609
610
611 /**
612  * A class representing a clickable "button".
613  *
614  * In it's simplest (default) form, a "button" is just a link associated
615  * with some sort of wiki-action.
616  */
617 class Button extends HtmlElement {
618     /** Constructor
619      *
620      * @param $text string The text for the button.
621      * @param $url string The url (href) for the button.
622      * @param $class string The CSS class for the button.
623      */
624     function Button ($text, $url, $class = false) {
625         $this->HtmlElement('a', array('href' => $url));
626         if ($class)
627             $this->setAttr('class', $class);
628         $this->pushContent($text);
629     }
630
631 };
632
633
634 /**
635  * A clickable image button.
636  */
637 class ImageButton extends Button {
638     /** Constructor
639      *
640      * @param $text string The text for the button.
641      * @param $url string The url (href) for the button.
642      * @param $class string The CSS class for the button.
643      * @param $img_url string URL for button's image.
644      * @param $img_attr array Additional attributes for the &lt;img&gt; tag.
645      */
646     function ImageButton ($text, $url, $class, $img_url, $img_attr = false) {
647         $this->HtmlElement('a', array('href' => $url));
648         if ($class)
649             $this->setAttr('class', $class);
650  
651         if (!is_array($img_attr))
652             $img_attr = array();
653         $img_attr['src'] = $img_url;
654         $img_attr['alt'] = $text;
655         $img_attr['class'] = 'wiki-button';
656         $img_attr['border'] = 0;
657         $this->pushContent(HTML::img($img_attr));
658     }
659 };
660
661 /**
662  * A class representing a form <samp>submit</samp> button.
663  */
664 class SubmitButton extends HtmlElement {
665     /** Constructor
666      *
667      * @param $text string The text for the button.
668      * @param $name string The name of the form field.
669      * @param $class string The CSS class for the button.
670      */
671     function SubmitButton ($text, $name = false, $class = false) {
672         $this->HtmlElement('input', array('type' => 'submit',
673                                           'value' => $text));
674         if ($name)
675             $this->setAttr('name', $name);
676         if ($class)
677             $this->setAttr('class', $class);
678     }
679
680 };
681
682
683 /**
684  * A class representing an image form <samp>submit</samp> button.
685  */
686 class SubmitImageButton extends SubmitButton {
687     /** Constructor
688      *
689      * @param $text string The text for the button.
690      * @param $name string The name of the form field.
691      * @param $class string The CSS class for the button.
692      * @param $img_url string URL for button's image.
693      * @param $img_attr array Additional attributes for the &lt;img&gt; tag.
694      */
695     function SubmitImageButton ($text, $name = false, $class = false, $img_url) {
696         $this->HtmlElement('input', array('type' => 'image',
697                                           'src' => $img_url,
698                                           'value' => $text,
699                                           'alt' => $text));
700         if ($name)
701             $this->setAttr('name', $name);
702         if ($class)
703             $this->setAttr('class', $class);
704     }
705
706 };
707
708 // (c-file-style: "gnu")
709 // Local Variables:
710 // mode: php
711 // tab-width: 8
712 // c-basic-offset: 4
713 // c-hanging-comment-ender-p: nil
714 // indent-tabs-mode: nil
715 // End:   
716 ?>