]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/action.php
Merge commit 'refs/merge-requests/158' of git://gitorious.org/statusnet/mainline...
[quix0rs-gnu-social.git] / lib / action.php
1 <?php
2 /**
3  * StatusNet, 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   StatusNet
24  * @author    Evan Prodromou <evan@status.net>
25  * @author    Sarven Capadisli <csarven@status.net>
26  * @copyright 2008 StatusNet, Inc.
27  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
28  * @link      http://status.net/
29  */
30
31 if (!defined('STATUSNET') && !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  StatusNet
49  * @author   Evan Prodromou <evan@status.net>
50  * @author   Sarven Capadisli <csarven@status.net>
51  * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
52  * @link     http://status.net/
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=null)
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
87         if ($this->boolean('ajax')) {
88             StatusNet::setAjax(true);
89         }
90
91         return true;
92     }
93
94     /**
95      * Show page, a template method.
96      *
97      * @return nothing
98      */
99     function showPage()
100     {
101         if (Event::handle('StartShowHTML', array($this))) {
102             $this->startHTML();
103             $this->flush();
104             Event::handle('EndShowHTML', array($this));
105         }
106         if (Event::handle('StartShowHead', array($this))) {
107             $this->showHead();
108             $this->flush();
109             Event::handle('EndShowHead', array($this));
110         }
111         if (Event::handle('StartShowBody', array($this))) {
112             $this->showBody();
113             Event::handle('EndShowBody', array($this));
114         }
115         if (Event::handle('StartEndHTML', array($this))) {
116             $this->endHTML();
117             Event::handle('EndEndHTML', array($this));
118         }
119     }
120
121     function endHTML()
122     {
123         global $_startTime;
124
125         if (isset($_startTime)) {
126             $endTime = microtime(true);
127             $diff = round(($endTime - $_startTime) * 1000);
128             $this->raw("<!-- ${diff}ms -->");
129         }
130
131         return parent::endHTML();
132     }
133
134     /**
135      * Show head, a template method.
136      *
137      * @return nothing
138      */
139     function showHead()
140     {
141         // XXX: attributes (profile?)
142         $this->elementStart('head');
143         if (Event::handle('StartShowHeadElements', array($this))) {
144             if (Event::handle('StartShowHeadTitle', array($this))) {
145                 $this->showTitle();
146                 Event::handle('EndShowHeadTitle', array($this));
147             }
148             $this->showShortcutIcon();
149             $this->showStylesheets();
150             $this->showOpenSearch();
151             $this->showFeeds();
152             $this->showDescription();
153             $this->extraHead();
154             Event::handle('EndShowHeadElements', array($this));
155         }
156         $this->elementEnd('head');
157     }
158
159     /**
160      * Show title, a template method.
161      *
162      * @return nothing
163      */
164     function showTitle()
165     {
166         $this->element('title', null,
167                        // TRANS: Page title. %1$s is the title, %2$s is the site name.
168                        sprintf(_('%1$s - %2$s'),
169                                $this->title(),
170                                common_config('site', 'name')));
171     }
172
173     /**
174      * Returns the page title
175      *
176      * SHOULD overload
177      *
178      * @return string page title
179      */
180
181     function title()
182     {
183         // TRANS: Page title for a page without a title set.
184         return _('Untitled page');
185     }
186
187     /**
188      * Show themed shortcut icon
189      *
190      * @return nothing
191      */
192     function showShortcutIcon()
193     {
194         if (is_readable(INSTALLDIR . '/theme/' . common_config('site', 'theme') . '/favicon.ico')) {
195             $this->element('link', array('rel' => 'shortcut icon',
196                                          'href' => Theme::path('favicon.ico')));
197         } else {
198             // favicon.ico should be HTTPS if the rest of the page is
199             $this->element('link', array('rel' => 'shortcut icon',
200                                          'href' => common_path('favicon.ico', StatusNet::isHTTPS())));
201         }
202
203         if (common_config('site', 'mobile')) {
204             if (is_readable(INSTALLDIR . '/theme/' . common_config('site', 'theme') . '/apple-touch-icon.png')) {
205                 $this->element('link', array('rel' => 'apple-touch-icon',
206                                              'href' => Theme::path('apple-touch-icon.png')));
207             } else {
208                 $this->element('link', array('rel' => 'apple-touch-icon',
209                                              'href' => common_path('apple-touch-icon.png')));
210             }
211         }
212     }
213
214     /**
215      * Show stylesheets
216      *
217      * @return nothing
218      */
219     function showStylesheets()
220     {
221         if (Event::handle('StartShowStyles', array($this))) {
222
223             // Use old name for StatusNet for compatibility on events
224
225             if (Event::handle('StartShowStatusNetStyles', array($this)) &&
226                 Event::handle('StartShowLaconicaStyles', array($this))) {
227                 $this->primaryCssLink(null, 'screen, projection, tv, print');
228                 Event::handle('EndShowStatusNetStyles', array($this));
229                 Event::handle('EndShowLaconicaStyles', array($this));
230             }
231
232             $this->cssLink(common_path('js/css/smoothness/jquery-ui.css'));
233
234             if (Event::handle('StartShowUAStyles', array($this))) {
235                 $this->comment('[if IE]><link rel="stylesheet" type="text/css" '.
236                                'href="'.Theme::path('css/ie.css', 'base').'?version='.STATUSNET_VERSION.'" /><![endif]');
237                 foreach (array(6,7) as $ver) {
238                     if (file_exists(Theme::file('css/ie'.$ver.'.css', 'base'))) {
239                         // Yes, IE people should be put in jail.
240                         $this->comment('[if lte IE '.$ver.']><link rel="stylesheet" type="text/css" '.
241                                        'href="'.Theme::path('css/ie'.$ver.'.css', 'base').'?version='.STATUSNET_VERSION.'" /><![endif]');
242                     }
243                 }
244                 if (file_exists(Theme::file('css/ie.css'))) {
245                     $this->comment('[if IE]><link rel="stylesheet" type="text/css" '.
246                                'href="'.Theme::path('css/ie.css', null).'?version='.STATUSNET_VERSION.'" /><![endif]');
247                 }
248                 Event::handle('EndShowUAStyles', array($this));
249             }
250
251             Event::handle('EndShowStyles', array($this));
252
253             if (common_config('custom_css', 'enabled')) {
254                 $css = common_config('custom_css', 'css');
255                 if (Event::handle('StartShowCustomCss', array($this, &$css))) {
256                     if (trim($css) != '') {
257                         $this->style($css);
258                     }
259                     Event::handle('EndShowCustomCss', array($this));
260                 }
261             }
262         }
263     }
264
265     function primaryCssLink($mainTheme=null, $media=null)
266     {
267         $theme = new Theme($mainTheme);
268
269         // Some themes may have external stylesheets, such as using the
270         // Google Font APIs to load webfonts.
271         foreach ($theme->getExternals() as $url) {
272             $this->cssLink($url, $mainTheme, $media);
273         }
274
275         // If the currently-selected theme has dependencies on other themes,
276         // we'll need to load their display.css files as well in order.
277         $baseThemes = $theme->getDeps();
278         foreach ($baseThemes as $baseTheme) {
279             $this->cssLink('css/display.css', $baseTheme, $media);
280         }
281         $this->cssLink('css/display.css', $mainTheme, $media);
282
283         // Additional styles for RTL languages
284         if (is_rtl(common_language())) {
285             if (file_exists(Theme::file('css/rtl.css'))) {
286                 $this->cssLink('css/rtl.css', $mainTheme, $media);
287             }
288         }
289     }
290
291     /**
292      * Show javascript headers
293      *
294      * @return nothing
295      */
296     function showScripts()
297     {
298         if (Event::handle('StartShowScripts', array($this))) {
299             if (Event::handle('StartShowJQueryScripts', array($this))) {
300                 if (common_config('site', 'minify')) {
301                     $this->script('jquery.min.js');
302                     $this->script('jquery.form.min.js');
303                     $this->script('jquery-ui.min.js');
304                     $this->script('jquery.cookie.min.js');
305                     $this->inlineScript('if (typeof window.JSON !== "object") { $.getScript("'.common_path('js/json2.min.js').'"); }');
306                     $this->script('jquery.joverlay.min.js');
307                     $this->script('jquery.infieldlabel.min.js');
308                 } else {
309                     $this->script('jquery.js');
310                     $this->script('jquery.form.js');
311                     $this->script('jquery-ui.min.js');
312                     $this->script('jquery.cookie.js');
313                     $this->inlineScript('if (typeof window.JSON !== "object") { $.getScript("'.common_path('js/json2.js').'"); }');
314                     $this->script('jquery.joverlay.js');
315                     $this->script('jquery.infieldlabel.js');
316                 }
317
318                 Event::handle('EndShowJQueryScripts', array($this));
319             }
320             if (Event::handle('StartShowStatusNetScripts', array($this)) &&
321                 Event::handle('StartShowLaconicaScripts', array($this))) {
322                 if (common_config('site', 'minify')) {
323                     $this->script('util.min.js');
324                 } else {
325                     $this->script('util.js');
326                     $this->script('xbImportNode.js');
327                     $this->script('geometa.js');
328                 }
329                 // This route isn't available in single-user mode.
330                 // Not sure why, but it causes errors here.
331                 if (!common_config('singleuser', 'enabled')) {
332                     $this->inlineScript('var _peopletagAC = "' .
333                                         common_local_url('peopletagautocomplete') . '";');
334                 }
335                 $this->showScriptMessages();
336                 // Anti-framing code to avoid clickjacking attacks in older browsers.
337                 // This will show a blank page if the page is being framed, which is
338                 // consistent with the behavior of the 'X-Frame-Options: SAMEORIGIN'
339                 // header, which prevents framing in newer browser.
340                 if (common_config('javascript', 'bustframes')) {
341                     $this->inlineScript('if (window.top !== window.self) { document.write = ""; window.top.location = window.self.location; setTimeout(function () { document.body.innerHTML = ""; }, 1); window.self.onload = function () { document.body.innerHTML = ""; }; }');
342                 }
343                 Event::handle('EndShowStatusNetScripts', array($this));
344                 Event::handle('EndShowLaconicaScripts', array($this));
345             }
346             Event::handle('EndShowScripts', array($this));
347         }
348     }
349
350     /**
351      * Exports a map of localized text strings to JavaScript code.
352      *
353      * Plugins can add to what's exported by hooking the StartScriptMessages or EndScriptMessages
354      * events and appending to the array. Try to avoid adding strings that won't be used, as
355      * they'll be added to HTML output.
356      */
357     function showScriptMessages()
358     {
359         $messages = array();
360
361         if (Event::handle('StartScriptMessages', array($this, &$messages))) {
362             // Common messages needed for timeline views etc...
363
364             // TRANS: Localized tooltip for '...' expansion button on overlong remote messages.
365             $messages['showmore_tooltip'] = _m('TOOLTIP', 'Show more');
366
367             // TRANS: Inline reply form submit button: submits a reply comment.
368             $messages['reply_submit'] = _m('BUTTON', 'Reply');
369
370             // TRANS: Placeholder text for inline reply form. Clicking in this box will turn it into a mini notice form.
371             $messages['reply_placeholder'] = _m('Write a reply...');
372
373             $messages = array_merge($messages, $this->getScriptMessages());
374
375             Event::handle('EndScriptMessages', array($this, &$messages));
376         }
377
378         if (!empty($messages)) {
379             $this->inlineScript('SN.messages=' . json_encode($messages));
380         }
381
382         return $messages;
383     }
384
385     /**
386      * If the action will need localizable text strings, export them here like so:
387      *
388      * return array('pool_deepend' => _('Deep end'),
389      *              'pool_shallow' => _('Shallow end'));
390      *
391      * The exported map will be available via SN.msg() to JS code:
392      *
393      *   $('#pool').html('<div class="deepend"></div><div class="shallow"></div>');
394      *   $('#pool .deepend').text(SN.msg('pool_deepend'));
395      *   $('#pool .shallow').text(SN.msg('pool_shallow'));
396      *
397      * Exports a map of localized text strings to JavaScript code.
398      *
399      * Plugins can add to what's exported on any action by hooking the StartScriptMessages or
400      * EndScriptMessages events and appending to the array. Try to avoid adding strings that won't
401      * be used, as they'll be added to HTML output.
402      */
403     function getScriptMessages()
404     {
405         return array();
406     }
407
408     /**
409      * Show OpenSearch headers
410      *
411      * @return nothing
412      */
413     function showOpenSearch()
414     {
415         $this->element('link', array('rel' => 'search',
416                                      'type' => 'application/opensearchdescription+xml',
417                                      'href' =>  common_local_url('opensearch', array('type' => 'people')),
418                                      'title' => common_config('site', 'name').' People Search'));
419         $this->element('link', array('rel' => 'search', 'type' => 'application/opensearchdescription+xml',
420                                      'href' =>  common_local_url('opensearch', array('type' => 'notice')),
421                                      'title' => common_config('site', 'name').' Notice Search'));
422     }
423
424     /**
425      * Show feed headers
426      *
427      * MAY overload
428      *
429      * @return nothing
430      */
431     function showFeeds()
432     {
433         $feeds = $this->getFeeds();
434
435         if ($feeds) {
436             foreach ($feeds as $feed) {
437                 $this->element('link', array('rel' => $feed->rel(),
438                                              'href' => $feed->url,
439                                              'type' => $feed->mimeType(),
440                                              'title' => $feed->title));
441             }
442         }
443     }
444
445     /**
446      * Show description.
447      *
448      * SHOULD overload
449      *
450      * @return nothing
451      */
452     function showDescription()
453     {
454         // does nothing by default
455     }
456
457     /**
458      * Show extra stuff in <head>.
459      *
460      * MAY overload
461      *
462      * @return nothing
463      */
464     function extraHead()
465     {
466         // does nothing by default
467     }
468
469     /**
470      * Show body.
471      *
472      * Calls template methods
473      *
474      * @return nothing
475      */
476     function showBody()
477     {
478         $this->elementStart('body', (common_current_user()) ? array('id' => strtolower($this->trimmed('action')),
479                                                                     'class' => 'user_in')
480                             : array('id' => strtolower($this->trimmed('action'))));
481         $this->elementStart('div', array('id' => 'wrap'));
482         if (Event::handle('StartShowHeader', array($this))) {
483             $this->showHeader();
484             $this->flush();
485             Event::handle('EndShowHeader', array($this));
486         }
487         $this->showCore();
488         $this->flush();
489         if (Event::handle('StartShowFooter', array($this))) {
490             $this->showFooter();
491             $this->flush();
492             Event::handle('EndShowFooter', array($this));
493         }
494         $this->elementEnd('div');
495         $this->showScripts();
496         $this->elementEnd('body');
497     }
498
499     /**
500      * Show header of the page.
501      *
502      * Calls template methods
503      *
504      * @return nothing
505      */
506     function showHeader()
507     {
508         $this->elementStart('div', array('id' => 'header'));
509         $this->showLogo();
510         $this->showPrimaryNav();
511         if (Event::handle('StartShowSiteNotice', array($this))) {
512             $this->showSiteNotice();
513
514             Event::handle('EndShowSiteNotice', array($this));
515         }
516
517         $this->elementEnd('div');
518     }
519
520     /**
521      * Show configured logo.
522      *
523      * @return nothing
524      */
525     function showLogo()
526     {
527         $this->elementStart('address', array('id' => 'site_contact',
528                                              'class' => 'vcard'));
529         if (Event::handle('StartAddressData', array($this))) {
530             if (common_config('singleuser', 'enabled')) {
531                 $user = User::singleUser();
532                 $url = common_local_url('showstream',
533                                         array('nickname' => $user->nickname));
534             } else if (common_logged_in()) {
535                 $cur = common_current_user();
536                 $url = common_local_url('all', array('nickname' => $cur->nickname));
537             } else {
538                 $url = common_local_url('public');
539             }
540
541             $this->elementStart('a', array('class' => 'url home bookmark',
542                                            'href' => $url));
543
544             if (StatusNet::isHTTPS()) {
545                 $logoUrl = common_config('site', 'ssllogo');
546                 if (empty($logoUrl)) {
547                     // if logo is an uploaded file, try to fall back to HTTPS file URL
548                     $httpUrl = common_config('site', 'logo');
549                     if (!empty($httpUrl)) {
550                         $f = File::staticGet('url', $httpUrl);
551                         if (!empty($f) && !empty($f->filename)) {
552                             // this will handle the HTTPS case
553                             $logoUrl = File::url($f->filename);
554                         }
555                     }
556                 }
557             } else {
558                 $logoUrl = common_config('site', 'logo');
559             }
560
561             if (empty($logoUrl) && file_exists(Theme::file('logo.png'))) {
562                 // This should handle the HTTPS case internally
563                 $logoUrl = Theme::path('logo.png');
564             }
565
566             if (!empty($logoUrl)) {
567                 $this->element('img', array('class' => 'logo photo',
568                                             'src' => $logoUrl,
569                                             'alt' => common_config('site', 'name')));
570             }
571
572             $this->text(' ');
573             $this->element('span', array('class' => 'fn org'), common_config('site', 'name'));
574             $this->elementEnd('a');
575
576             Event::handle('EndAddressData', array($this));
577         }
578         $this->elementEnd('address');
579     }
580
581     /**
582      * Show primary navigation.
583      *
584      * @return nothing
585      */
586     function showPrimaryNav()
587     {
588         $this->elementStart('div', array('id' => 'site_nav_global_primary'));
589
590         $user = common_current_user();
591
592         if (!empty($user) || !common_config('site', 'private')) {
593             $form = new SearchForm($this);
594             $form->show();
595         }
596
597         $pn = new PrimaryNav($this);
598         $pn->show();
599         $this->elementEnd('div');
600     }
601
602     /**
603      * Show site notice.
604      *
605      * @return nothing
606      */
607     function showSiteNotice()
608     {
609         // Revist. Should probably do an hAtom pattern here
610         $text = common_config('site', 'notice');
611         if ($text) {
612             $this->elementStart('div', array('id' => 'site_notice',
613                                             'class' => 'system_notice'));
614             $this->raw($text);
615             $this->elementEnd('div');
616         }
617     }
618
619     /**
620      * Show notice form.
621      *
622      * MAY overload if no notice form needed... or direct message box????
623      *
624      * @return nothing
625      */
626     function showNoticeForm()
627     {
628         // TRANS: Tab on the notice form.
629         $tabs = array('status' => _m('TAB','Status'));
630
631         $this->elementStart('div', 'input_forms');
632
633         if (Event::handle('StartShowEntryForms', array(&$tabs))) {
634             $this->elementStart('ul', array('class' => 'nav',
635                                             'id' => 'input_form_nav'));
636
637             foreach ($tabs as $tag => $title) {
638                 $attrs = array('id' => 'input_form_nav_'.$tag,
639                                'class' => 'input_form_nav_tab');
640
641                 if ($tag == 'status') {
642                     // We're actually showing the placeholder form,
643                     // but we special-case the 'Status' tab as if
644                     // it were a small version of it.
645                     $attrs['class'] .= ' current';
646                 }
647                 $this->elementStart('li', $attrs);
648
649                 $this->element('a',
650                                array('href' => 'javascript:SN.U.switchInputFormTab("'.$tag.'")'),
651                                $title);
652                 $this->elementEnd('li');
653             }
654
655             $this->elementEnd('ul');
656
657             $attrs = array('class' => 'input_form current',
658                            'id' => 'input_form_placeholder');
659             $this->elementStart('div', $attrs);
660             $form = new NoticePlaceholderForm($this);
661             $form->show();
662             $this->elementEnd('div');
663
664             foreach ($tabs as $tag => $title) {
665                 $attrs = array('class' => 'input_form',
666                                'id' => 'input_form_'.$tag);
667
668                 $this->elementStart('div', $attrs);
669
670                 $form = null;
671
672                 if (Event::handle('StartMakeEntryForm', array($tag, $this, &$form))) {
673                     if ($tag == 'status') {
674                         $options = $this->noticeFormOptions();
675                         $form = new NoticeForm($this, $options);
676                     }
677                     Event::handle('EndMakeEntryForm', array($tag, $this, $form));
678                 }
679
680                 if (!empty($form)) {
681                     $form->show();
682                 }
683
684                 $this->elementEnd('div');
685             }
686         }
687
688         $this->elementEnd('div');
689     }
690
691     function noticeFormOptions()
692     {
693         return array();
694     }
695
696     /**
697      * Show anonymous message.
698      *
699      * SHOULD overload
700      *
701      * @return nothing
702      */
703     function showAnonymousMessage()
704     {
705         // needs to be defined by the class
706     }
707
708     /**
709      * Show core.
710      *
711      * Shows local navigation, content block and aside.
712      *
713      * @return nothing
714      */
715     function showCore()
716     {
717         $this->elementStart('div', array('id' => 'core'));
718         $this->elementStart('div', array('id' => 'aside_primary_wrapper'));
719         $this->elementStart('div', array('id' => 'content_wrapper'));
720         $this->elementStart('div', array('id' => 'site_nav_local_views_wrapper'));
721         if (Event::handle('StartShowLocalNavBlock', array($this))) {
722             $this->showLocalNavBlock();
723             $this->flush();
724             Event::handle('EndShowLocalNavBlock', array($this));
725         }
726         if (Event::handle('StartShowContentBlock', array($this))) {
727             $this->showContentBlock();
728             $this->flush();
729             Event::handle('EndShowContentBlock', array($this));
730         }
731         if (Event::handle('StartShowAside', array($this))) {
732             $this->showAside();
733             $this->flush();
734             Event::handle('EndShowAside', array($this));
735         }
736         $this->elementEnd('div');
737         $this->elementEnd('div');
738         $this->elementEnd('div');
739         $this->elementEnd('div');
740     }
741
742     /**
743      * Show local navigation block.
744      *
745      * @return nothing
746      */
747     function showLocalNavBlock()
748     {
749         // Need to have this ID for CSS; I'm too lazy to add it to
750         // all menus
751         $this->elementStart('div', array('id' => 'site_nav_local_views'));
752         // Cheat cheat cheat!
753         $this->showLocalNav();
754         $this->elementEnd('div');
755     }
756
757     /**
758      * If there's a logged-in user, show a bit of login context
759      *
760      * @return nothing
761      */
762     function showProfileBlock()
763     {
764         if (common_logged_in()) {
765             $block = new DefaultProfileBlock($this);
766             $block->show();
767         }
768     }
769
770     /**
771      * Show local navigation.
772      *
773      * SHOULD overload
774      *
775      * @return nothing
776      */
777     function showLocalNav()
778     {
779         $nav = new DefaultLocalNav($this);
780         $nav->show();
781     }
782
783     /**
784      * Show menu for an object (group, profile)
785      *
786      * This block will only show if a subclass has overridden
787      * the showObjectNav() method.
788      *
789      * @return nothing
790      */
791     function showObjectNavBlock()
792     {
793         $rmethod = new ReflectionMethod($this, 'showObjectNav');
794         $dclass = $rmethod->getDeclaringClass()->getName();
795
796         if ($dclass != 'Action') {
797             // Need to have this ID for CSS; I'm too lazy to add it to
798             // all menus
799             $this->elementStart('div', array('id' => 'site_nav_object',
800                                              'class' => 'section'));
801             $this->showObjectNav();
802             $this->elementEnd('div');
803         }
804     }
805
806     /**
807      * Show object navigation.
808      *
809      * If there are things to do with this object, show it here.
810      *
811      * @return nothing
812      */
813     function showObjectNav()
814     {
815         /* Nothing here. */
816     }
817
818     /**
819      * Show content block.
820      *
821      * @return nothing
822      */
823     function showContentBlock()
824     {
825         $this->elementStart('div', array('id' => 'content'));
826         if (common_logged_in()) {
827             if (Event::handle('StartShowNoticeForm', array($this))) {
828                 $this->showNoticeForm();
829                 Event::handle('EndShowNoticeForm', array($this));
830             }
831         }
832         if (Event::handle('StartShowPageTitle', array($this))) {
833             $this->showPageTitle();
834             Event::handle('EndShowPageTitle', array($this));
835         }
836         $this->showPageNoticeBlock();
837         $this->elementStart('div', array('id' => 'content_inner'));
838         // show the actual content (forms, lists, whatever)
839         $this->showContent();
840         $this->elementEnd('div');
841         $this->elementEnd('div');
842     }
843
844     /**
845      * Show page title.
846      *
847      * @return nothing
848      */
849     function showPageTitle()
850     {
851         $this->element('h1', null, $this->title());
852     }
853
854     /**
855      * Show page notice block.
856      *
857      * Only show the block if a subclassed action has overrided
858      * Action::showPageNotice(), or an event handler is registered for
859      * the StartShowPageNotice event, in which case we assume the
860      * 'page_notice' definition list is desired.  This is to prevent
861      * empty 'page_notice' definition lists from being output everywhere.
862      *
863      * @return nothing
864      */
865     function showPageNoticeBlock()
866     {
867         $rmethod = new ReflectionMethod($this, 'showPageNotice');
868         $dclass = $rmethod->getDeclaringClass()->getName();
869
870         if ($dclass != 'Action' || Event::hasHandler('StartShowPageNotice')) {
871
872             $this->elementStart('div', array('id' => 'page_notice',
873                                             'class' => 'system_notice'));
874             if (Event::handle('StartShowPageNotice', array($this))) {
875                 $this->showPageNotice();
876                 Event::handle('EndShowPageNotice', array($this));
877             }
878             $this->elementEnd('div');
879         }
880     }
881
882     /**
883      * Show page notice.
884      *
885      * SHOULD overload (unless there's not a notice)
886      *
887      * @return nothing
888      */
889     function showPageNotice()
890     {
891     }
892
893     /**
894      * Show content.
895      *
896      * MUST overload (unless there's not a notice)
897      *
898      * @return nothing
899      */
900     function showContent()
901     {
902     }
903
904     /**
905      * Show Aside.
906      *
907      * @return nothing
908      */
909     function showAside()
910     {
911         $this->elementStart('div', array('id' => 'aside_primary',
912                                          'class' => 'aside'));
913         $this->showProfileBlock();
914         if (Event::handle('StartShowObjectNavBlock', array($this))) {
915             $this->showObjectNavBlock();
916             Event::handle('EndShowObjectNavBlock', array($this));
917         }
918         if (Event::handle('StartShowSections', array($this))) {
919             $this->showSections();
920             Event::handle('EndShowSections', array($this));
921         }
922         if (Event::handle('StartShowExportData', array($this))) {
923             $this->showExportData();
924             Event::handle('EndShowExportData', array($this));
925         }
926         $this->elementEnd('div');
927     }
928
929     /**
930      * Show export data feeds.
931      *
932      * @return void
933      */
934     function showExportData()
935     {
936         $feeds = $this->getFeeds();
937         if ($feeds) {
938             $fl = new FeedList($this);
939             $fl->show($feeds);
940         }
941     }
942
943     /**
944      * Show sections.
945      *
946      * SHOULD overload
947      *
948      * @return nothing
949      */
950     function showSections()
951     {
952         // for each section, show it
953     }
954
955     /**
956      * Show footer.
957      *
958      * @return nothing
959      */
960     function showFooter()
961     {
962         $this->elementStart('div', array('id' => 'footer'));
963         if (Event::handle('StartShowInsideFooter', array($this))) {
964             $this->showSecondaryNav();
965             $this->showLicenses();
966             Event::handle('EndShowInsideFooter', array($this));
967         }
968         $this->elementEnd('div');
969     }
970
971     /**
972      * Show secondary navigation.
973      *
974      * @return nothing
975      */
976     function showSecondaryNav()
977     {
978         $sn = new SecondaryNav($this);
979         $sn->show();
980     }
981
982     /**
983      * Show licenses.
984      *
985      * @return nothing
986      */
987     function showLicenses()
988     {
989         $this->showStatusNetLicense();
990         $this->showContentLicense();
991     }
992
993     /**
994      * Show StatusNet license.
995      *
996      * @return nothing
997      */
998     function showStatusNetLicense()
999     {
1000         if (common_config('site', 'broughtby')) {
1001             // TRANS: First sentence of the StatusNet site license. Used if 'broughtby' is set.
1002             // TRANS: Text between [] is a link description, text between () is the link itself.
1003             // TRANS: Make sure there is no whitespace between "]" and "(".
1004             // TRANS: "%%site.broughtby%%" is the value of the variable site.broughtby
1005             $instr = _('**%%site.name%%** is a microblogging service brought to you by [%%site.broughtby%%](%%site.broughtbyurl%%).');
1006         } else {
1007             // TRANS: First sentence of the StatusNet site license. Used if 'broughtby' is not set.
1008             $instr = _('**%%site.name%%** is a microblogging service.');
1009         }
1010         $instr .= ' ';
1011         // TRANS: Second sentence of the StatusNet site license. Mentions the StatusNet source code license.
1012         // TRANS: Make sure there is no whitespace between "]" and "(".
1013         // TRANS: Text between [] is a link description, text between () is the link itself.
1014         // TRANS: %s is the version of StatusNet that is being used.
1015         $instr .= sprintf(_('It runs the [StatusNet](http://status.net/) microblogging software, version %s, available under the [GNU Affero General Public License](http://www.fsf.org/licensing/licenses/agpl-3.0.html).'), STATUSNET_VERSION);
1016         $output = common_markup_to_html($instr);
1017         $this->raw($output);
1018         // do it
1019     }
1020
1021     /**
1022      * Show content license.
1023      *
1024      * @return nothing
1025      */
1026     function showContentLicense()
1027     {
1028         if (Event::handle('StartShowContentLicense', array($this))) {
1029             switch (common_config('license', 'type')) {
1030             case 'private':
1031                 // TRANS: Content license displayed when license is set to 'private'.
1032                 // TRANS: %1$s is the site name.
1033                 $this->element('p', null, sprintf(_('Content and data of %1$s are private and confidential.'),
1034                                                   common_config('site', 'name')));
1035                 // fall through
1036             case 'allrightsreserved':
1037                 if (common_config('license', 'owner')) {
1038                     // TRANS: Content license displayed when license is set to 'allrightsreserved'.
1039                     // TRANS: %1$s is the copyright owner.
1040                     $this->element('p', null, sprintf(_('Content and data copyright by %1$s. All rights reserved.'),
1041                                                       common_config('license', 'owner')));
1042                 } else {
1043                     // TRANS: Content license displayed when license is set to 'allrightsreserved' and no owner is set.
1044                     $this->element('p', null, _('Content and data copyright by contributors. All rights reserved.'));
1045                 }
1046                 break;
1047             case 'cc': // fall through
1048             default:
1049                 $this->elementStart('p');
1050
1051                 $image    = common_config('license', 'image');
1052                 $sslimage = common_config('license', 'sslimage');
1053
1054                 if (StatusNet::isHTTPS()) {
1055                     if (!empty($sslimage)) {
1056                         $url = $sslimage;
1057                     } else if (preg_match('#^http://i.creativecommons.org/#', $image)) {
1058                         // CC support HTTPS on their images
1059                         $url = preg_replace('/^http/', 'https', $image);
1060                     } else {
1061                         // Better to show mixed content than no content
1062                         $url = $image;
1063                     }
1064                 } else {
1065                     $url = $image;
1066                 }
1067
1068                 $this->element('img', array('id' => 'license_cc',
1069                                             'src' => $url,
1070                                             'alt' => common_config('license', 'title'),
1071                                             'width' => '80',
1072                                             'height' => '15'));
1073                 $this->text(' ');
1074                 // TRANS: license message in footer.
1075                 // TRANS: %1$s is the site name, %2$s is a link to the license URL, with a licence name set in configuration.
1076                 $notice = _('All %1$s content and data are available under the %2$s license.');
1077                 $link = "<a class=\"license\" rel=\"external license\" href=\"" .
1078                         htmlspecialchars(common_config('license', 'url')) .
1079                         "\">" .
1080                         htmlspecialchars(common_config('license', 'title')) .
1081                         "</a>";
1082                 $this->raw(sprintf(htmlspecialchars($notice),
1083                                    htmlspecialchars(common_config('site', 'name')),
1084                                    $link));
1085                 $this->elementEnd('p');
1086                 break;
1087             }
1088
1089             Event::handle('EndShowContentLicense', array($this));
1090         }
1091     }
1092
1093     /**
1094      * Return last modified, if applicable.
1095      *
1096      * MAY override
1097      *
1098      * @return string last modified http header
1099      */
1100     function lastModified()
1101     {
1102         // For comparison with If-Last-Modified
1103         // If not applicable, return null
1104         return null;
1105     }
1106
1107     /**
1108      * Return etag, if applicable.
1109      *
1110      * MAY override
1111      *
1112      * @return string etag http header
1113      */
1114     function etag()
1115     {
1116         return null;
1117     }
1118
1119     /**
1120      * Return true if read only.
1121      *
1122      * MAY override
1123      *
1124      * @param array $args other arguments
1125      *
1126      * @return boolean is read only action?
1127      */
1128     function isReadOnly($args)
1129     {
1130         return false;
1131     }
1132
1133     /**
1134      * Returns query argument or default value if not found
1135      *
1136      * @param string $key requested argument
1137      * @param string $def default value to return if $key is not provided
1138      *
1139      * @return boolean is read only action?
1140      */
1141     function arg($key, $def=null)
1142     {
1143         if (array_key_exists($key, $this->args)) {
1144             return $this->args[$key];
1145         } else {
1146             return $def;
1147         }
1148     }
1149
1150     /**
1151      * Returns trimmed query argument or default value if not found
1152      *
1153      * @param string $key requested argument
1154      * @param string $def default value to return if $key is not provided
1155      *
1156      * @return boolean is read only action?
1157      */
1158     function trimmed($key, $def=null)
1159     {
1160         $arg = $this->arg($key, $def);
1161         return is_string($arg) ? trim($arg) : $arg;
1162     }
1163
1164     /**
1165      * Handler method
1166      *
1167      * @param array $argarray is ignored since it's now passed in in prepare()
1168      *
1169      * @return boolean is read only action?
1170      */
1171     function handle($argarray=null)
1172     {
1173         header('Vary: Accept-Encoding,Cookie');
1174
1175         $lm   = $this->lastModified();
1176         $etag = $this->etag();
1177
1178         if ($etag) {
1179             header('ETag: ' . $etag);
1180         }
1181
1182         if ($lm) {
1183             header('Last-Modified: ' . date(DATE_RFC1123, $lm));
1184             if ($this->isCacheable()) {
1185                 header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
1186                 header( "Cache-Control: private, must-revalidate, max-age=0" );
1187                 header( "Pragma:");
1188             }
1189         }
1190
1191         $checked = false;
1192         if ($etag) {
1193             $if_none_match = (array_key_exists('HTTP_IF_NONE_MATCH', $_SERVER)) ?
1194               $_SERVER['HTTP_IF_NONE_MATCH'] : null;
1195             if ($if_none_match) {
1196                 // If this check fails, ignore the if-modified-since below.
1197                 $checked = true;
1198                 if ($this->_hasEtag($etag, $if_none_match)) {
1199                     header('HTTP/1.1 304 Not Modified');
1200                     // Better way to do this?
1201                     exit(0);
1202                 }
1203             }
1204         }
1205
1206         if (!$checked && $lm && array_key_exists('HTTP_IF_MODIFIED_SINCE', $_SERVER)) {
1207             $if_modified_since = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
1208             $ims = strtotime($if_modified_since);
1209             if ($lm <= $ims) {
1210                 header('HTTP/1.1 304 Not Modified');
1211                 // Better way to do this?
1212                 exit(0);
1213             }
1214         }
1215     }
1216
1217     /**
1218      * Is this action cacheable?
1219      *
1220      * If the action returns a last-modified
1221      *
1222      * @param array $argarray is ignored since it's now passed in in prepare()
1223      *
1224      * @return boolean is read only action?
1225      */
1226     function isCacheable()
1227     {
1228         return true;
1229     }
1230
1231     /**
1232      * HasĀ etag? (private)
1233      *
1234      * @param string $etag          etag http header
1235      * @param string $if_none_match ifNoneMatch http header
1236      *
1237      * @return boolean
1238      */
1239     function _hasEtag($etag, $if_none_match)
1240     {
1241         $etags = explode(',', $if_none_match);
1242         return in_array($etag, $etags) || in_array('*', $etags);
1243     }
1244
1245     /**
1246      * Boolean understands english (yes, no, true, false)
1247      *
1248      * @param string $key query key we're interested in
1249      * @param string $def default value
1250      *
1251      * @return boolean interprets yes/no strings as boolean
1252      */
1253     function boolean($key, $def=false)
1254     {
1255         $arg = strtolower($this->trimmed($key));
1256
1257         if (is_null($arg)) {
1258             return $def;
1259         } else if (in_array($arg, array('true', 'yes', '1', 'on'))) {
1260             return true;
1261         } else if (in_array($arg, array('false', 'no', '0'))) {
1262             return false;
1263         } else {
1264             return $def;
1265         }
1266     }
1267
1268     /**
1269      * Integer value of an argument
1270      *
1271      * @param string $key      query key we're interested in
1272      * @param string $defValue optional default value (default null)
1273      * @param string $maxValue optional max value (default null)
1274      * @param string $minValue optional min value (default null)
1275      *
1276      * @return integer integer value
1277      */
1278     function int($key, $defValue=null, $maxValue=null, $minValue=null)
1279     {
1280         $arg = strtolower($this->trimmed($key));
1281
1282         if (is_null($arg) || !is_integer($arg)) {
1283             return $defValue;
1284         }
1285
1286         if (!is_null($maxValue)) {
1287             $arg = min($arg, $maxValue);
1288         }
1289
1290         if (!is_null($minValue)) {
1291             $arg = max($arg, $minValue);
1292         }
1293
1294         return $arg;
1295     }
1296
1297     /**
1298      * Server error
1299      *
1300      * @param string  $msg  error message to display
1301      * @param integer $code http error code, 500 by default
1302      *
1303      * @return nothing
1304      */
1305     function serverError($msg, $code=500)
1306     {
1307         $action = $this->trimmed('action');
1308         common_debug("Server error '$code' on '$action': $msg", __FILE__);
1309         throw new ServerException($msg, $code);
1310     }
1311
1312     /**
1313      * Client error
1314      *
1315      * @param string  $msg  error message to display
1316      * @param integer $code http error code, 400 by default
1317      *
1318      * @return nothing
1319      */
1320     function clientError($msg, $code=400)
1321     {
1322         $action = $this->trimmed('action');
1323         common_debug("User error '$code' on '$action': $msg", __FILE__);
1324         throw new ClientException($msg, $code);
1325     }
1326
1327     /**
1328      * Returns the current URL
1329      *
1330      * @return string current URL
1331      */
1332     function selfUrl()
1333     {
1334         list($action, $args) = $this->returnToArgs();
1335         return common_local_url($action, $args);
1336     }
1337
1338     /**
1339      * Returns arguments sufficient for re-constructing URL
1340      *
1341      * @return array two elements: action, other args
1342      */
1343     function returnToArgs()
1344     {
1345         $action = $this->trimmed('action');
1346         $args   = $this->args;
1347         unset($args['action']);
1348         if (common_config('site', 'fancy')) {
1349             unset($args['p']);
1350         }
1351         if (array_key_exists('submit', $args)) {
1352             unset($args['submit']);
1353         }
1354         foreach (array_keys($_COOKIE) as $cookie) {
1355             unset($args[$cookie]);
1356         }
1357         return array($action, $args);
1358     }
1359
1360     /**
1361      * Generate a menu item
1362      *
1363      * @param string  $url         menu URL
1364      * @param string  $text        menu name
1365      * @param string  $title       title attribute, null by default
1366      * @param boolean $is_selected current menu item, false by default
1367      * @param string  $id          element id, null by default
1368      *
1369      * @return nothing
1370      */
1371     function menuItem($url, $text, $title=null, $is_selected=false, $id=null, $class=null)
1372     {
1373         // Added @id to li for some control.
1374         // XXX: We might want to move this to htmloutputter.php
1375         $lattrs  = array();
1376         $classes = array();
1377         if ($class !== null) {
1378             $classes[] = trim($class);
1379         }
1380         if ($is_selected) {
1381             $classes[] = 'current';
1382         }
1383
1384         if (!empty($classes)) {
1385             $lattrs['class'] = implode(' ', $classes);
1386         }
1387
1388         if (!is_null($id)) {
1389             $lattrs['id'] = $id;
1390         }
1391
1392         $this->elementStart('li', $lattrs);
1393         $attrs['href'] = $url;
1394         if ($title) {
1395             $attrs['title'] = $title;
1396         }
1397         $this->element('a', $attrs, $text);
1398         $this->elementEnd('li');
1399     }
1400
1401     /**
1402      * Generate pagination links
1403      *
1404      * @param boolean $have_before is there something before?
1405      * @param boolean $have_after  is there something after?
1406      * @param integer $page        current page
1407      * @param string  $action      current action
1408      * @param array   $args        rest of query arguments
1409      *
1410      * @return nothing
1411      */
1412     // XXX: The messages in this pagination method only tailor to navigating
1413     //      notices. In other lists, "Previous"/"Next" type navigation is
1414     //      desirable, but not available.
1415     function pagination($have_before, $have_after, $page, $action, $args=null)
1416     {
1417         // Does a little before-after block for next/prev page
1418         if ($have_before || $have_after) {
1419             $this->elementStart('ul', array('class' => 'nav',
1420                                             'id' => 'pagination'));
1421         }
1422         if ($have_before) {
1423             $pargs   = array('page' => $page-1);
1424             $this->elementStart('li', array('class' => 'nav_prev'));
1425             $this->element('a', array('href' => common_local_url($action, $args, $pargs),
1426                                       'rel' => 'prev'),
1427                            // TRANS: Pagination message to go to a page displaying information more in the
1428                            // TRANS: present than the currently displayed information.
1429                            _('After'));
1430             $this->elementEnd('li');
1431         }
1432         if ($have_after) {
1433             $pargs   = array('page' => $page+1);
1434             $this->elementStart('li', array('class' => 'nav_next'));
1435             $this->element('a', array('href' => common_local_url($action, $args, $pargs),
1436                                       'rel' => 'next'),
1437                            // TRANS: Pagination message to go to a page displaying information more in the
1438                            // TRANS: past than the currently displayed information.
1439                            _('Before'));
1440             $this->elementEnd('li');
1441         }
1442         if ($have_before || $have_after) {
1443             $this->elementEnd('ul');
1444         }
1445     }
1446
1447     /**
1448      * An array of feeds for this action.
1449      *
1450      * Returns an array of potential feeds for this action.
1451      *
1452      * @return array Feed object to show in head and links
1453      */
1454     function getFeeds()
1455     {
1456         return null;
1457     }
1458
1459     /**
1460      * Check the session token.
1461      *
1462      * Checks that the current form has the correct session token,
1463      * and throw an exception if it does not.
1464      *
1465      * @return void
1466      */
1467     // XXX: Finding this type of check with the same message about 50 times.
1468     //      Possible to refactor?
1469     function checkSessionToken()
1470     {
1471         // CSRF protection
1472         $token = $this->trimmed('token');
1473         if (empty($token) || $token != common_session_token()) {
1474             // TRANS: Client error text when there is a problem with the session token.
1475             $this->clientError(_('There was a problem with your session token.'));
1476         }
1477     }
1478
1479     /**
1480      * Check if the current request is a POST
1481      *
1482      * @return boolean true if POST; otherwise false.
1483      */
1484
1485     function isPost()
1486     {
1487         return ($_SERVER['REQUEST_METHOD'] == 'POST');
1488     }
1489 }