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