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