]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/action.php
Merge branch '0.7.x' into 0.8.x
[quix0rs-gnu-social.git] / lib / action.php
1 <?php
2 /**
3  * Laconica, the distributed open-source microblogging tool
4  *
5  * Base class for all actions (~views)
6  *
7  * PHP version 5
8  *
9  * LICENCE: This program is free software: you can redistribute it and/or modify
10  * it under the terms of the GNU Affero General Public License as published by
11  * the Free Software Foundation, either version 3 of the License, or
12  * (at your option) any later version.
13  *
14  * This program is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  * GNU Affero General Public License for more details.
18  *
19  * You should have received a copy of the GNU Affero General Public License
20  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
21  *
22  * @category  Action
23  * @package   Laconica
24  * @author    Evan Prodromou <evan@controlyourself.ca>
25  * @author    Sarven Capadisli <csarven@controlyourself.ca>
26  * @copyright 2008 Control Yourself, Inc.
27  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
28  * @link      http://laconi.ca/
29  */
30
31 if (!defined('LACONICA')) {
32     exit(1);
33 }
34
35 require_once INSTALLDIR.'/lib/noticeform.php';
36 require_once INSTALLDIR.'/lib/htmloutputter.php';
37
38 /**
39  * Base class for all actions
40  *
41  * This is the base class for all actions in the package. An action is
42  * more or less a "view" in an MVC framework.
43  *
44  * Actions are responsible for extracting and validating parameters; using
45  * model classes to read and write to the database; and doing ouput.
46  *
47  * @category Output
48  * @package  Laconica
49  * @author   Evan Prodromou <evan@controlyourself.ca>
50  * @author   Sarven Capadisli <csarven@controlyourself.ca>
51  * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
52  * @link     http://laconi.ca/
53  *
54  * @see      HTMLOutputter
55  */
56 class Action extends HTMLOutputter // lawsuit
57 {
58     var $args;
59
60     /**
61      * Constructor
62      *
63      * Just wraps the HTMLOutputter constructor.
64      *
65      * @param string  $output URI to output to, default = stdout
66      * @param boolean $indent Whether to indent output, default true
67      *
68      * @see XMLOutputter::__construct
69      * @see HTMLOutputter::__construct
70      */
71     function __construct($output='php://output', $indent=true)
72     {
73         parent::__construct($output, $indent);
74     }
75
76     /**
77      * For initializing members of the class.
78      *
79      * @param array $argarray misc. arguments
80      *
81      * @return boolean true
82      */
83     function prepare($argarray)
84     {
85         $this->args =& common_copy_args($argarray);
86         return true;
87     }
88
89     /**
90      * Show page, a template method.
91      *
92      * @return nothing
93      */
94     function showPage()
95     {
96         if (Event::handle('StartShowHTML', array($this))) {
97             $this->startHTML();
98             Event::handle('EndShowHTML', array($this));
99         }
100         $this->showHead();
101         $this->showBody();
102         $this->endHTML();
103     }
104
105     /**
106      * Show head, a template method.
107      *
108      * @return nothing
109      */
110     function showHead()
111     {
112         // XXX: attributes (profile?)
113         $this->elementStart('head');
114         $this->showTitle();
115         $this->showStylesheets();
116         $this->showScripts();
117         $this->showOpenSearch();
118         $this->showFeeds();
119         $this->showDescription();
120         $this->extraHead();
121         $this->elementEnd('head');
122     }
123
124     /**
125      * Show title, a template method.
126      *
127      * @return nothing
128      */
129     function showTitle()
130     {
131         $this->element('title', null,
132                        sprintf(_("%s - %s"),
133                                $this->title(),
134                                common_config('site', 'name')));
135     }
136
137     /**
138      * Returns the page title
139      *
140      * SHOULD overload
141      *
142      * @return string page title
143      */
144
145     function title()
146     {
147         return _("Untitled page");
148     }
149
150     /**
151      * Show stylesheets
152      *
153      * @return nothing
154      */
155     function showStylesheets()
156     {
157         if (Event::handle('StartShowStyles', array($this))) {
158             if (Event::handle('StartShowLaconicaStyles', array($this))) {
159
160                 $this->element('link', array('rel' => 'stylesheet',
161                                              'type' => 'text/css',
162                                              'href' => theme_path('css/display.css', 'base') . '?version=' . LACONICA_VERSION,
163                                              'media' => 'screen, projection, tv'));
164                 $this->element('link', array('rel' => 'stylesheet',
165                                              'type' => 'text/css',
166                                              'href' => theme_path('css/modal.css', 'base') . '?version=' . LACONICA_VERSION,
167                                              'media' => 'screen, projection, tv'));
168                 $this->element('link', array('rel' => 'stylesheet',
169                                              'type' => 'text/css',
170                                              'href' => theme_path('css/display.css', null) . '?version=' . LACONICA_VERSION,
171                                              'media' => 'screen, projection, tv'));
172                 if (common_config('site', 'mobile')) {
173                     $this->element('link', array('rel' => 'stylesheet',
174                                                  'type' => 'text/css',
175                                                  'href' => theme_path('css/mobile.css', 'base') . '?version=' . LACONICA_VERSION,
176                                                  // TODO: "handheld" CSS for other mobile devices
177                                                  'media' => 'only screen and (max-device-width: 480px)')); // Mobile WebKit
178                 }
179                 $this->element('link', array('rel' => 'stylesheet',
180                                              'type' => 'text/css',
181                                              'href' => theme_path('css/print.css', 'base') . '?version=' . LACONICA_VERSION,
182                                              'media' => 'print'));
183                 Event::handle('EndShowLaconicaStyles', array($this));
184             }
185             if (Event::handle('StartShowUAStyles', array($this))) {
186                 $this->comment('[if IE]><link rel="stylesheet" type="text/css" '.
187                                'href="'.theme_path('css/ie.css', 'base').'?version='.LACONICA_VERSION.'" /><![endif]');
188                 foreach (array(6,7) as $ver) {
189                     if (file_exists(theme_file('css/ie'.$ver.'.css', 'base'))) {
190                         // Yes, IE people should be put in jail.
191                         $this->comment('[if lte IE '.$ver.']><link rel="stylesheet" type="text/css" '.
192                                        'href="'.theme_path('css/ie'.$ver.'.css', 'base').'?version='.LACONICA_VERSION.'" /><![endif]');
193                     }
194                 }
195                 $this->comment('[if IE]><link rel="stylesheet" type="text/css" '.
196                                'href="'.theme_path('css/ie.css', null).'?version='.LACONICA_VERSION.'" /><![endif]');
197                 Event::handle('EndShowUAStyles', array($this));
198             }
199             Event::handle('EndShowStyles', array($this));
200         }
201     }
202
203     /**
204      * Show javascript headers
205      *
206      * @return nothing
207      */
208     function showScripts()
209     {
210         if (Event::handle('StartShowScripts', array($this))) {
211             if (Event::handle('StartShowJQueryScripts', array($this))) {
212                 $this->element('script', array('type' => 'text/javascript',
213                                                'src' => common_path('js/jquery.min.js')),
214                                ' ');
215                 $this->element('script', array('type' => 'text/javascript',
216                                                'src' => common_path('js/jquery.form.js')),
217                                ' ');
218
219                 $this->element('script', array('type' => 'text/javascript',
220                                                'src' => common_path('js/jquery.simplemodal-1.2.2.pack.js')),
221                                ' ');
222
223                 Event::handle('EndShowJQueryScripts', array($this));
224             }
225             if (Event::handle('StartShowLaconicaScripts', array($this))) {
226                 $this->element('script', array('type' => 'text/javascript',
227                                                'src' => common_path('js/xbImportNode.js')),
228                                ' ');
229                 $this->element('script', array('type' => 'text/javascript',
230                                                'src' => common_path('js/util.js?version='.LACONICA_VERSION)),
231                                ' ');
232                 // Frame-busting code to avoid clickjacking attacks.
233                 $this->element('script', array('type' => 'text/javascript'),
234                                'if (window.top !== window.self) { window.top.location.href = window.self.location.href; }');
235
236                 $this->element('script', array('type' => 'text/javascript',
237                                                'src' => common_path('js/flowplayer-3.0.5.min.js')),
238                                ' ');
239
240                 $this->element('script', array('type' => 'text/javascript',
241                                                'src' => common_path('js/video.js')),
242                                ' ');
243                 Event::handle('EndShowLaconicaScripts', array($this));
244             }
245             Event::handle('EndShowScripts', array($this));
246         }
247     }
248
249     /**
250      * Show OpenSearch headers
251      *
252      * @return nothing
253      */
254     function showOpenSearch()
255     {
256         $this->element('link', array('rel' => 'search',
257                                      'type' => 'application/opensearchdescription+xml',
258                                      'href' =>  common_local_url('opensearch', array('type' => 'people')),
259                                      'title' => common_config('site', 'name').' People Search'));
260         $this->element('link', array('rel' => 'search', 'type' => 'application/opensearchdescription+xml',
261                                      'href' =>  common_local_url('opensearch', array('type' => 'notice')),
262                                      'title' => common_config('site', 'name').' Notice Search'));
263     }
264
265     /**
266      * Show feed headers
267      *
268      * MAY overload
269      *
270      * @return nothing
271      */
272
273     function showFeeds()
274     {
275         $feeds = $this->getFeeds();
276
277         if ($feeds) {
278             foreach ($feeds as $feed) {
279                 $this->element('link', array('rel' => $feed->rel(),
280                                              'href' => $feed->url,
281                                              'type' => $feed->mimeType(),
282                                              'title' => $feed->title));
283             }
284         }
285     }
286
287     /**
288      * Show description.
289      *
290      * SHOULD overload
291      *
292      * @return nothing
293      */
294     function showDescription()
295     {
296         // does nothing by default
297     }
298
299     /**
300      * Show extra stuff in <head>.
301      *
302      * MAY overload
303      *
304      * @return nothing
305      */
306     function extraHead()
307     {
308         // does nothing by default
309     }
310
311     /**
312      * Show body.
313      *
314      * Calls template methods
315      *
316      * @return nothing
317      */
318     function showBody()
319     {
320         $this->elementStart('body', array('id' => $this->trimmed('action')));
321         $this->elementStart('div', array('id' => 'wrap'));
322         if (Event::handle('StartShowHeader', array($this))) {
323             $this->showHeader();
324             Event::handle('EndShowHeader', array($this));
325         }
326         $this->showCore();
327         if (Event::handle('StartShowFooter', array($this))) {
328             $this->showFooter();
329             Event::handle('EndShowFooter', array($this));
330         }
331         $this->elementEnd('div');
332         $this->elementEnd('body');
333     }
334
335     /**
336      * Show header of the page.
337      *
338      * Calls template methods
339      *
340      * @return nothing
341      */
342     function showHeader()
343     {
344         $this->elementStart('div', array('id' => 'header'));
345         $this->showLogo();
346         $this->showPrimaryNav();
347         $this->showSiteNotice();
348         if (common_logged_in()) {
349             $this->showNoticeForm();
350         } else {
351             $this->showAnonymousMessage();
352         }
353         $this->elementEnd('div');
354     }
355
356     /**
357      * Show configured logo.
358      *
359      * @return nothing
360      */
361     function showLogo()
362     {
363         $this->elementStart('address', array('id' => 'site_contact',
364                                              'class' => 'vcard'));
365         $this->elementStart('a', array('class' => 'url home bookmark',
366                                        'href' => common_local_url('public')));
367         if (common_config('site', 'logo') || file_exists(theme_file('logo.png'))) {
368             $this->element('img', array('class' => 'logo photo',
369                                         'src' => (common_config('site', 'logo')) ? common_config('site', 'logo') : theme_path('logo.png'),
370                                         'alt' => common_config('site', 'name')));
371         }
372         $this->element('span', array('class' => 'fn org'), common_config('site', 'name'));
373         $this->elementEnd('a');
374         $this->elementEnd('address');
375     }
376
377     /**
378      * Show primary navigation.
379      *
380      * @return nothing
381      */
382     function showPrimaryNav()
383     {
384         $user = common_current_user();
385
386         $this->elementStart('dl', array('id' => 'site_nav_global_primary'));
387         $this->element('dt', null, _('Primary site navigation'));
388         $this->elementStart('dd');
389         $this->elementStart('ul', array('class' => 'nav'));
390         if (Event::handle('StartPrimaryNav', array($this))) {
391             if ($user) {
392                 $this->menuItem(common_local_url('all', array('nickname' => $user->nickname)),
393                                 _('Home'), _('Personal profile and friends timeline'), false, 'nav_home');
394             }
395             $this->menuItem(common_local_url('peoplesearch'),
396                             _('Search'), _('Search for people or text'), false, 'nav_search');
397             if ($user) {
398                 $this->menuItem(common_local_url('profilesettings'),
399                                 _('Account'), _('Change your email, avatar, password, profile'), false, 'nav_account');
400
401                 if (common_config('xmpp', 'enabled')) {
402                     $this->menuItem(common_local_url('imsettings'),
403                                     _('Connect'), _('Connect to IM, SMS, Twitter'), false, 'nav_connect');
404                 } else {
405                     $this->menuItem(common_local_url('smssettings'),
406                                     _('Connect'), _('Connect to SMS, Twitter'), false, 'nav_connect');
407                 }
408                 $this->menuItem(common_local_url('logout'),
409                                 _('Logout'), _('Logout from the site'), false, 'nav_logout');
410             } else {
411                 $this->menuItem(common_local_url('login'),
412                                 _('Login'), _('Login to the site'), false, 'nav_login');
413                 if (!common_config('site', 'closed')) {
414                     $this->menuItem(common_local_url('register'),
415                                     _('Register'), _('Create an account'), false, 'nav_register');
416                 }
417                 $this->menuItem(common_local_url('openidlogin'),
418                                 _('OpenID'), _('Login with OpenID'), false, 'nav_openid');
419             }
420             $this->menuItem(common_local_url('doc', array('title' => 'help')),
421                             _('Help'), _('Help me!'), false, 'nav_help');
422             Event::handle('EndPrimaryNav', array($this));
423         }
424         $this->elementEnd('ul');
425         $this->elementEnd('dd');
426         $this->elementEnd('dl');
427     }
428
429     /**
430      * Show site notice.
431      *
432      * @return nothing
433      */
434     function showSiteNotice()
435     {
436         // Revist. Should probably do an hAtom pattern here
437         $text = common_config('site', 'notice');
438         if ($text) {
439             $this->elementStart('dl', array('id' => 'site_notice',
440                                             'class' => 'system_notice'));
441             $this->element('dt', null, _('Site notice'));
442             $this->elementStart('dd', null);
443             $this->raw($text);
444             $this->elementEnd('dd');
445             $this->elementEnd('dl');
446         }
447     }
448
449     /**
450      * Show notice form.
451      *
452      * MAY overload if no notice form needed... or direct message box????
453      *
454      * @return nothing
455      */
456     function showNoticeForm()
457     {
458         $notice_form = new NoticeForm($this);
459         $notice_form->show();
460     }
461
462     /**
463      * Show anonymous message.
464      *
465      * SHOULD overload
466      *
467      * @return nothing
468      */
469     function showAnonymousMessage()
470     {
471         // needs to be defined by the class
472     }
473
474     /**
475      * Show core.
476      *
477      * Shows local navigation, content block and aside.
478      *
479      * @return nothing
480      */
481     function showCore()
482     {
483         $this->elementStart('div', array('id' => 'core'));
484         if (Event::handle('StartShowLocalNavBlock', array($this))) {
485             $this->showLocalNavBlock();
486             Event::handle('EndShowLocalNavBlock', array($this));
487         }
488         if (Event::handle('StartShowContentBlock', array($this))) {
489             $this->showContentBlock();
490             Event::handle('EndShowContentBlock', array($this));
491         }
492         $this->showAside();
493         $this->elementEnd('div');
494     }
495
496     /**
497      * Show local navigation block.
498      *
499      * @return nothing
500      */
501     function showLocalNavBlock()
502     {
503         $this->elementStart('dl', array('id' => 'site_nav_local_views'));
504         $this->element('dt', null, _('Local views'));
505         $this->elementStart('dd');
506         $this->showLocalNav();
507         $this->elementEnd('dd');
508         $this->elementEnd('dl');
509     }
510
511     /**
512      * Show local navigation.
513      *
514      * SHOULD overload
515      *
516      * @return nothing
517      */
518     function showLocalNav()
519     {
520         // does nothing by default
521     }
522
523     /**
524      * Show content block.
525      *
526      * @return nothing
527      */
528     function showContentBlock()
529     {
530         $this->elementStart('div', array('id' => 'content'));
531         $this->showPageTitle();
532         $this->showPageNoticeBlock();
533         $this->elementStart('div', array('id' => 'content_inner'));
534         // show the actual content (forms, lists, whatever)
535         $this->showContent();
536         $this->elementEnd('div');
537         $this->elementEnd('div');
538     }
539
540     /**
541      * Show page title.
542      *
543      * @return nothing
544      */
545     function showPageTitle()
546     {
547         $this->element('h1', null, $this->title());
548     }
549
550     /**
551      * Show page notice block.
552      *
553      * @return nothing
554      */
555     function showPageNoticeBlock()
556     {
557         $this->elementStart('dl', array('id' => 'page_notice',
558                                         'class' => 'system_notice'));
559         $this->element('dt', null, _('Page notice'));
560         $this->elementStart('dd');
561         $this->showPageNotice();
562         $this->elementEnd('dd');
563         $this->elementEnd('dl');
564     }
565
566     /**
567      * Show page notice.
568      *
569      * SHOULD overload (unless there's not a notice)
570      *
571      * @return nothing
572      */
573     function showPageNotice()
574     {
575     }
576
577     /**
578      * Show content.
579      *
580      * MUST overload (unless there's not a notice)
581      *
582      * @return nothing
583      */
584     function showContent()
585     {
586     }
587
588     /**
589      * Show Aside.
590      *
591      * @return nothing
592      */
593
594     function showAside()
595     {
596         $this->elementStart('div', array('id' => 'aside_primary',
597                                          'class' => 'aside'));
598         $this->showExportData();
599         if (Event::handle('StartShowSections', array($this))) {
600             $this->showSections();
601             Event::handle('EndShowSections', array($this));
602         }
603         $this->elementEnd('div');
604     }
605
606     /**
607      * Show export data feeds.
608      *
609      * @return void
610      */
611
612     function showExportData()
613     {
614         $feeds = $this->getFeeds();
615         if ($feeds) {
616             $fl = new FeedList($this);
617             $fl->show($feeds);
618         }
619     }
620
621     /**
622      * Show sections.
623      *
624      * SHOULD overload
625      *
626      * @return nothing
627      */
628     function showSections()
629     {
630         // for each section, show it
631     }
632
633     /**
634      * Show footer.
635      *
636      * @return nothing
637      */
638     function showFooter()
639     {
640         $this->elementStart('div', array('id' => 'footer'));
641         $this->showSecondaryNav();
642         $this->showLicenses();
643         $this->elementEnd('div');
644     }
645
646     /**
647      * Show secondary navigation.
648      *
649      * @return nothing
650      */
651     function showSecondaryNav()
652     {
653         $this->elementStart('dl', array('id' => 'site_nav_global_secondary'));
654         $this->element('dt', null, _('Secondary site navigation'));
655         $this->elementStart('dd', null);
656         $this->elementStart('ul', array('class' => 'nav'));
657         if (Event::handle('StartSecondaryNav', array($this))) {
658             $this->menuItem(common_local_url('doc', array('title' => 'help')),
659                             _('Help'));
660             $this->menuItem(common_local_url('doc', array('title' => 'about')),
661                             _('About'));
662             $this->menuItem(common_local_url('doc', array('title' => 'faq')),
663                             _('FAQ'));
664             $this->menuItem(common_local_url('doc', array('title' => 'privacy')),
665                             _('Privacy'));
666             $this->menuItem(common_local_url('doc', array('title' => 'source')),
667                             _('Source'));
668             $this->menuItem(common_local_url('doc', array('title' => 'contact')),
669                             _('Contact'));
670             $this->menuItem(common_local_url('doc', array('title' => 'badge')),
671                             _('Badge'));
672             Event::handle('EndSecondaryNav', array($this));
673         }
674         $this->elementEnd('ul');
675         $this->elementEnd('dd');
676         $this->elementEnd('dl');
677     }
678
679     /**
680      * Show licenses.
681      *
682      * @return nothing
683      */
684     function showLicenses()
685     {
686         $this->elementStart('dl', array('id' => 'licenses'));
687         $this->showLaconicaLicense();
688         $this->showContentLicense();
689         $this->elementEnd('dl');
690     }
691
692     /**
693      * Show Laconica license.
694      *
695      * @return nothing
696      */
697     function showLaconicaLicense()
698     {
699         $this->element('dt', array('id' => 'site_laconica_license'), _('Laconica software license'));
700         $this->elementStart('dd', null);
701         if (common_config('site', 'broughtby')) {
702             $instr = _('**%%site.name%%** is a microblogging service brought to you by [%%site.broughtby%%](%%site.broughtbyurl%%). ');
703         } else {
704             $instr = _('**%%site.name%%** is a microblogging service. ');
705         }
706         $instr .= sprintf(_('It runs the [Laconica](http://laconi.ca/) microblogging software, version %s, available under the [GNU Affero General Public License](http://www.fsf.org/licensing/licenses/agpl-3.0.html).'), LACONICA_VERSION);
707         $output = common_markup_to_html($instr);
708         $this->raw($output);
709         $this->elementEnd('dd');
710         // do it
711     }
712
713     /**
714      * Show content license.
715      *
716      * @return nothing
717      */
718     function showContentLicense()
719     {
720         $this->element('dt', array('id' => 'site_content_license'), _('Laconica software license'));
721         $this->elementStart('dd', array('id' => 'site_content_license_cc'));
722         $this->elementStart('p');
723         $this->element('img', array('id' => 'license_cc',
724                                     'src' => common_config('license', 'image'),
725                                     'alt' => common_config('license', 'title')));
726         //TODO: This is dirty: i18n
727         $this->text(_('All '.common_config('site', 'name').' content and data are available under the '));
728         $this->element('a', array('class' => 'license',
729                                   'rel' => 'external license',
730                                   'href' => common_config('license', 'url')),
731                        common_config('license', 'title'));
732         $this->text(_('license.'));
733         $this->elementEnd('p');
734         $this->elementEnd('dd');
735     }
736
737     /**
738      * Return last modified, if applicable.
739      *
740      * MAY override
741      *
742      * @return string last modified http header
743      */
744     function lastModified()
745     {
746         // For comparison with If-Last-Modified
747         // If not applicable, return null
748         return null;
749     }
750
751     /**
752      * Return etag, if applicable.
753      *
754      * MAY override
755      *
756      * @return string etag http header
757      */
758     function etag()
759     {
760         return null;
761     }
762
763     /**
764      * Return true if read only.
765      *
766      * MAY override
767      *
768      * @return boolean is read only action?
769      */
770     function isReadOnly()
771     {
772         return false;
773     }
774
775     /**
776      * Returns query argument or default value if not found
777      *
778      * @param string $key requested argument
779      * @param string $def default value to return if $key is not provided
780      *
781      * @return boolean is read only action?
782      */
783     function arg($key, $def=null)
784     {
785         if (array_key_exists($key, $this->args)) {
786             return $this->args[$key];
787         } else {
788             return $def;
789         }
790     }
791
792     /**
793      * Returns trimmed query argument or default value if not found
794      *
795      * @param string $key requested argument
796      * @param string $def default value to return if $key is not provided
797      *
798      * @return boolean is read only action?
799      */
800     function trimmed($key, $def=null)
801     {
802         $arg = $this->arg($key, $def);
803         return is_string($arg) ? trim($arg) : $arg;
804     }
805
806     /**
807      * Handler method
808      *
809      * @param array $argarray is ignored since it's now passed in in prepare()
810      *
811      * @return boolean is read only action?
812      */
813     function handle($argarray=null)
814     {
815         $lm   = $this->lastModified();
816         $etag = $this->etag();
817         if ($etag) {
818             header('ETag: ' . $etag);
819         }
820         if ($lm) {
821             header('Last-Modified: ' . date(DATE_RFC1123, $lm));
822             if (array_key_exists('HTTP_IF_MODIFIED_SINCE', $_SERVER)) {
823                 $if_modified_since = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
824                 $ims = strtotime($if_modified_since);
825                 if ($lm <= $ims) {
826                     $if_none_match = (array_key_exists('HTTP_IF_NONE_MATCH', $_SERVER)) ?
827                       $_SERVER['HTTP_IF_NONE_MATCH'] : null;
828                     if (!$if_none_match ||
829                         !$etag ||
830                         $this->_hasEtag($etag, $if_none_match)) {
831                         header('HTTP/1.1 304 Not Modified');
832                         // Better way to do this?
833                         exit(0);
834                     }
835                 }
836             }
837         }
838     }
839
840     /**
841      * HasĀ etag? (private)
842      *
843      * @param string $etag          etag http header
844      * @param string $if_none_match ifNoneMatch http header
845      *
846      * @return boolean
847      */
848
849     function _hasEtag($etag, $if_none_match)
850     {
851         $etags = explode(',', $if_none_match);
852         return in_array($etag, $etags) || in_array('*', $etags);
853     }
854
855     /**
856      * Boolean understands english (yes, no, true, false)
857      *
858      * @param string $key query key we're interested in
859      * @param string $def default value
860      *
861      * @return boolean interprets yes/no strings as boolean
862      */
863     function boolean($key, $def=false)
864     {
865         $arg = strtolower($this->trimmed($key));
866
867         if (is_null($arg)) {
868             return $def;
869         } else if (in_array($arg, array('true', 'yes', '1'))) {
870             return true;
871         } else if (in_array($arg, array('false', 'no', '0'))) {
872             return false;
873         } else {
874             return $def;
875         }
876     }
877
878     /**
879      * Server error
880      *
881      * @param string  $msg  error message to display
882      * @param integer $code http error code, 500 by default
883      *
884      * @return nothing
885      */
886
887     function serverError($msg, $code=500)
888     {
889         $action = $this->trimmed('action');
890         common_debug("Server error '$code' on '$action': $msg", __FILE__);
891         throw new ServerException($msg, $code);
892     }
893
894     /**
895      * Client error
896      *
897      * @param string  $msg  error message to display
898      * @param integer $code http error code, 400 by default
899      *
900      * @return nothing
901      */
902
903     function clientError($msg, $code=400)
904     {
905         $action = $this->trimmed('action');
906         common_debug("User error '$code' on '$action': $msg", __FILE__);
907         throw new ClientException($msg, $code);
908     }
909
910     /**
911      * Returns the current URL
912      *
913      * @return string current URL
914      */
915     function selfUrl()
916     {
917         $action = $this->trimmed('action');
918         $args   = $this->args;
919         unset($args['action']);
920         foreach (array_keys($_COOKIE) as $cookie) {
921             unset($args[$cookie]);
922         }
923         return common_local_url($action, $args);
924     }
925
926     /**
927      * Generate a menu item
928      *
929      * @param string  $url         menu URL
930      * @param string  $text        menu name
931      * @param string  $title       title attribute, null by default
932      * @param boolean $is_selected current menu item, false by default
933      * @param string  $id          element id, null by default
934      *
935      * @return nothing
936      */
937     function menuItem($url, $text, $title=null, $is_selected=false, $id=null)
938     {
939         // Added @id to li for some control.
940         // XXX: We might want to move this to htmloutputter.php
941         $lattrs = array();
942         if ($is_selected) {
943             $lattrs['class'] = 'current';
944         }
945
946         (is_null($id)) ? $lattrs : $lattrs['id'] = $id;
947
948         $this->elementStart('li', $lattrs);
949         $attrs['href'] = $url;
950         if ($title) {
951             $attrs['title'] = $title;
952         }
953         $this->element('a', $attrs, $text);
954         $this->elementEnd('li');
955     }
956
957     /**
958      * Generate pagination links
959      *
960      * @param boolean $have_before is there something before?
961      * @param boolean $have_after  is there something after?
962      * @param integer $page        current page
963      * @param string  $action      current action
964      * @param array   $args        rest of query arguments
965      *
966      * @return nothing
967      */
968     function pagination($have_before, $have_after, $page, $action, $args=null)
969     {
970         // Does a little before-after block for next/prev page
971         if ($have_before || $have_after) {
972             $this->elementStart('div', array('class' => 'pagination'));
973             $this->elementStart('dl', null);
974             $this->element('dt', null, _('Pagination'));
975             $this->elementStart('dd', null);
976             $this->elementStart('ul', array('class' => 'nav'));
977         }
978         if ($have_before) {
979             $pargs   = array('page' => $page-1);
980             $newargs = $args ? array_merge($args, $pargs) : $pargs;
981             $this->elementStart('li', array('class' => 'nav_prev'));
982             $this->element('a', array('href' => common_local_url($action, $newargs), 'rel' => 'prev'),
983                            _('After'));
984             $this->elementEnd('li');
985         }
986         if ($have_after) {
987             $pargs   = array('page' => $page+1);
988             $newargs = $args ? array_merge($args, $pargs) : $pargs;
989             $this->elementStart('li', array('class' => 'nav_next'));
990             $this->element('a', array('href' => common_local_url($action, $newargs), 'rel' => 'next'),
991                            _('Before'));
992             $this->elementEnd('li');
993         }
994         if ($have_before || $have_after) {
995             $this->elementEnd('ul');
996             $this->elementEnd('dd');
997             $this->elementEnd('dl');
998             $this->elementEnd('div');
999         }
1000     }
1001
1002     /**
1003      * An array of feeds for this action.
1004      *
1005      * Returns an array of potential feeds for this action.
1006      *
1007      * @return array Feed object to show in head and links
1008      */
1009
1010     function getFeeds()
1011     {
1012         return null;
1013     }
1014 }