]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/action.php
[XML/HTML Outputter] General improvements and refactoring as well as some bug fixes
[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('GNUSOCIAL')) {
32     exit(1);
33 }
34
35 /**
36  * Base class for all actions
37  *
38  * This is the base class for all actions in the package. An action is
39  * more or less a "view" in an MVC framework.
40  *
41  * Actions are responsible for extracting and validating parameters; using
42  * model classes to read and write to the database; and doing ouput.
43  *
44  * @category Output
45  * @package  StatusNet
46  * @author   Evan Prodromou <evan@status.net>
47  * @author   Sarven Capadisli <csarven@status.net>
48  * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
49  * @link     http://status.net/
50  *
51  * @see      HTMLOutputter
52  */
53 class Action extends HTMLOutputter // lawsuit
54 {
55     // This should be protected/private in the future
56     public $args = [];
57
58     // Action properties, set per-class
59     protected $action = false;
60     protected $ajax = false;
61     protected $menus = true;
62     protected $needLogin = false;
63     protected $needPost = false;    // implies canPost if true
64     protected $canPost = false;     // can this action handle POST method?
65
66     // The currently scoped profile (normally Profile::current; from $this->auth_user for API)
67     protected $scoped = null;
68
69     // Related to front-end user representation
70     protected $format = null;
71     protected $error = null;
72     protected $msg = null;
73
74     /**
75      * Constructor
76      *
77      * Just wraps the HTMLOutputter constructor.
78      *
79      * @param string $output URI to output to, default = stdout
80      * @param boolean $indent Whether to indent output, default true
81      *
82      * @see XMLOutputter::__construct
83      * @see HTMLOutputter::__construct
84      */
85     public function __construct($output = 'php://output', $indent = null)
86     {
87         parent::__construct($output, $indent);
88     }
89
90     public static function run(array $args = [], $output = 'php://output', $indent = null)
91     {
92         $class = get_called_class();
93         $action = new $class($output, $indent);
94         set_exception_handler(array($action, 'handleError'));
95         $action->execute($args);
96         return $action;
97     }
98
99     public function getInfo()
100     {
101         return $this->msg;
102     }
103
104     public function handleError($e)
105     {
106         if ($e instanceof ClientException) {
107             $this->clientError($e->getMessage(), $e->getCode());
108         } elseif ($e instanceof ServerException) {
109             $this->serverError($e->getMessage(), $e->getCode());
110         } else {
111             // If it wasn't specified more closely which kind of exception it was
112             $this->serverError($e->getMessage(), 500);
113         }
114     }
115
116     /**
117      * Client error
118      *
119      * @param string $msg error message to display
120      * @param integer $code http error code, 400 by default
121      * @param string $format error format (json, xml, text) for ApiAction
122      *
123      * @return void
124      * @throws ClientException always
125      */
126     public function clientError($msg, $code = 400, $format = null)
127     {
128         // $format is currently only relevant for an ApiAction anyway
129         if ($format === null) {
130             $format = $this->format;
131         }
132
133         common_debug("User error '{$code}' on '{$this->action}': {$msg}", __FILE__);
134
135         if (!array_key_exists($code, ClientErrorAction::$status)) {
136             $code = 400;
137         }
138
139         $status_string = ClientErrorAction::$status[$code];
140
141         switch ($format) {
142             case 'xml':
143                 header("HTTP/1.1 {$code} {$status_string}");
144                 $this->initDocument('xml');
145                 $this->elementStart('hash');
146                 $this->element('error', null, $msg);
147                 $this->element('request', null, $_SERVER['REQUEST_URI']);
148                 $this->elementEnd('hash');
149                 $this->endDocument('xml');
150                 break;
151             case 'json':
152                 if (!isset($this->callback)) {
153                     header("HTTP/1.1 {$code} {$status_string}");
154                 }
155                 $this->initDocument('json');
156                 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
157                 print(json_encode($error_array));
158                 $this->endDocument('json');
159                 break;
160             case 'text':
161                 header("HTTP/1.1 {$code} {$status_string}");
162                 header('Content-Type: text/plain; charset=utf-8');
163                 echo $msg;
164                 break;
165             default:
166                 common_log(LOG_ERR, 'Handled clientError (' . _ve($code) . ') but cannot output into desired format (' . _ve($this->format) . '): ' . _ve($msg));
167                 $action = new ClientErrorAction($msg, $code);
168                 $action->execute();
169         }
170         exit((int)$code);
171     }
172
173     public function execute(array $args = [])
174     {
175         // checkMirror stuff
176         if (common_config('db', 'mirror') && $this->isReadOnly($args)) {
177             if (is_array(common_config('db', 'mirror'))) {
178                 // "load balancing", ha ha
179                 $arr = common_config('db', 'mirror');
180                 $k = array_rand($arr);
181                 $mirror = $arr[$k];
182             } else {
183                 $mirror = common_config('db', 'mirror');
184             }
185
186             // everyone else uses the mirror
187             common_config_set('db', 'database', $mirror);
188         }
189
190         if (Event::handle('StartActionExecute', array($this, &$args))) {
191             $prepared = $this->prepare($args);
192             if ($prepared) {
193                 $this->handle();
194             } else {
195                 common_debug('Prepare failed for Action.');
196             }
197
198             $this->flush();
199             Event::handle('EndActionExecute', array($this));
200         }
201     }
202
203     /**
204      * Return true if read only.
205      *
206      * MAY override
207      *
208      * @param array $args other arguments
209      *
210      * @return boolean is read only action?
211      */
212     public function isReadOnly($args)
213     {
214         return false;
215     }
216
217     /**
218      * For initializing members of the class.
219      *
220      * @param array $args misc. arguments
221      *
222      * @return boolean true
223      * @throws ClientException
224      */
225     protected function prepare(array $args = [])
226     {
227         if ($this->needPost && !$this->isPost()) {
228             // TRANS: Client error. POST is a HTTP command. It should not be translated.
229             $this->clientError(_('This method requires a POST.'), 405);
230         }
231
232         // needPost, of course, overrides canPost if true
233         if (!$this->canPost) {
234             $this->canPost = $this->needPost;
235         }
236
237         $this->args = common_copy_args($args);
238
239         // This could be set with get_called_action and then
240         // chop off 'Action' from the class name. In lower case.
241         $this->action = strtolower($this->trimmed('action'));
242
243         if ($this->ajax || $this->boolean('ajax')) {
244             // check with GNUsocial::isAjax()
245             GNUsocial::setAjax(true);
246         }
247
248         if ($this->needLogin) {
249             $this->checkLogin(); // if not logged in, this redirs/excepts
250         }
251
252         $this->updateScopedProfile();
253
254         return true;
255     }
256
257     /**
258      * Check if the current request is a POST
259      *
260      * @return boolean true if POST; otherwise false.
261      */
262
263     public function isPost()
264     {
265         return ($_SERVER['REQUEST_METHOD'] == 'POST');
266     }
267
268     // Must be run _after_ prepare
269
270     /**
271      * Returns trimmed query argument or default value if not found
272      *
273      * @param string $key requested argument
274      * @param string $def default value to return if $key is not provided
275      *
276      * @return boolean is read only action?
277      */
278     public function trimmed($key, $def = null)
279     {
280         $arg = $this->arg($key, $def);
281         return is_string($arg) ? trim($arg) : $arg;
282     }
283
284     /**
285      * Returns query argument or default value if not found
286      *
287      * @param string $key requested argument
288      * @param string $def default value to return if $key is not provided
289      *
290      * @return boolean is read only action?
291      */
292     public function arg($key, $def = null)
293     {
294         if (array_key_exists($key, $this->args)) {
295             return $this->args[$key];
296         } else {
297             return $def;
298         }
299     }
300
301     /**
302      * Boolean understands english (yes, no, true, false)
303      *
304      * @param string $key query key we're interested in
305      * @param string $def default value
306      *
307      * @return boolean interprets yes/no strings as boolean
308      */
309     public function boolean($key, $def = false)
310     {
311         $arg = strtolower($this->trimmed($key));
312
313         if (is_null($arg)) {
314             return $def;
315         } elseif (in_array($arg, array('true', 'yes', '1', 'on'))) {
316             return true;
317         } elseif (in_array($arg, array('false', 'no', '0'))) {
318             return false;
319         } else {
320             return $def;
321         }
322     }
323
324     /**
325      * If not logged in, take appropriate action (redir or exception)
326      *
327      * @param boolean $redir Redirect to login if not logged in
328      *
329      * @return boolean true if logged in (never returns if not)
330      * @throws ClientException
331      */
332     public function checkLogin($redir = true)
333     {
334         if (common_logged_in()) {
335             return true;
336         }
337
338         if ($redir == true) {
339             common_set_returnto($_SERVER['REQUEST_URI']);
340             common_redirect(common_local_url('login'));
341         }
342
343         // TRANS: Error message displayed when trying to perform an action that requires a logged in user.
344         $this->clientError(_('Not logged in.'), 403);
345     }
346
347     public function updateScopedProfile()
348     {
349         $this->scoped = Profile::current();
350         return $this->scoped;
351     }
352
353     /**
354      * Handler method
355      */
356     protected function handle()
357     {
358         header('Vary: Accept-Encoding,Cookie');
359
360         $lm = $this->lastModified();
361         $etag = $this->etag();
362
363         if ($etag) {
364             header('ETag: ' . $etag);
365         }
366
367         if ($lm) {
368             header('Last-Modified: ' . date(DATE_RFC1123, $lm));
369             if ($this->isCacheable()) {
370                 header('Expires: ' . gmdate('D, d M Y H:i:s', 0) . ' GMT');
371                 header("Cache-Control: private, must-revalidate, max-age=0");
372                 header("Pragma:");
373             }
374         }
375
376         $checked = false;
377         if ($etag) {
378             $if_none_match = (array_key_exists('HTTP_IF_NONE_MATCH', $_SERVER)) ?
379                 $_SERVER['HTTP_IF_NONE_MATCH'] : null;
380             if ($if_none_match) {
381                 // If this check fails, ignore the if-modified-since below.
382                 $checked = true;
383                 if ($this->_hasEtag($etag, $if_none_match)) {
384                     header('HTTP/1.1 304 Not Modified');
385                     // Better way to do this?
386                     exit(0);
387                 }
388             }
389         }
390
391         if (!$checked && $lm && array_key_exists('HTTP_IF_MODIFIED_SINCE', $_SERVER)) {
392             $if_modified_since = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
393             $ims = strtotime($if_modified_since);
394             if ($lm <= $ims) {
395                 header('HTTP/1.1 304 Not Modified');
396                 // Better way to do this?
397                 exit(0);
398             }
399         }
400     }
401
402     /**
403      * Return last modified, if applicable.
404      *
405      * MAY override
406      *
407      * @return string last modified http header
408      */
409     public function lastModified()
410     {
411         // For comparison with If-Last-Modified
412         // If not applicable, return null
413         return null;
414     }
415
416     /**
417      * Return etag, if applicable.
418      *
419      * MAY override
420      *
421      * @return string etag http header
422      */
423     public function etag()
424     {
425         return null;
426     }
427
428     /**
429      * Is this action cacheable?
430      *
431      * If the action returns a last-modified
432      *
433      * @return boolean is read only action?
434      */
435     public function isCacheable()
436     {
437         return true;
438     }
439
440     /**
441      * Has etag? (private)
442      *
443      * @param string $etag etag http header
444      * @param string $if_none_match ifNoneMatch http header
445      *
446      * @return boolean
447      */
448     public function _hasEtag($etag, $if_none_match)
449     {
450         $etags = explode(',', $if_none_match);
451         return in_array($etag, $etags) || in_array('*', $etags);
452     }
453
454     /**
455      * Server error
456      *
457      * @param string $msg error message to display
458      * @param integer $code http error code, 500 by default
459      *
460      * @param string $format
461      * @return void
462      */
463     public function serverError($msg, $code = 500, $format = null)
464     {
465         if ($format === null) {
466             $format = $this->format;
467         }
468
469         common_debug("Server error '{$code}' on '{$this->action}': {$msg}", __FILE__);
470
471         if (!array_key_exists($code, ServerErrorAction::$status)) {
472             $code = 500;
473         }
474
475         $status_string = ServerErrorAction::$status[$code];
476
477         switch ($format) {
478             case 'xml':
479                 header("HTTP/1.1 {$code} {$status_string}");
480                 $this->initDocument('xml');
481                 $this->elementStart('hash');
482                 $this->element('error', null, $msg);
483                 $this->element('request', null, $_SERVER['REQUEST_URI']);
484                 $this->elementEnd('hash');
485                 $this->endDocument('xml');
486                 break;
487             case 'json':
488                 if (!isset($this->callback)) {
489                     header("HTTP/1.1 {$code} {$status_string}");
490                 }
491                 $this->initDocument('json');
492                 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
493                 print(json_encode($error_array));
494                 $this->endDocument('json');
495                 break;
496             default:
497                 common_log(LOG_ERR, 'Handled serverError (' . _ve($code) . ') but cannot output into desired format (' . _ve($this->format) . '): ' . _ve($msg));
498                 $action = new ServerErrorAction($msg, $code);
499                 $action->execute();
500         }
501
502         exit((int)$code);
503     }
504
505     public function getScoped()
506     {
507         return ($this->scoped instanceof Profile) ? $this->scoped : null;
508     }
509
510     public function isAction(array $names)
511     {
512         foreach ($names as $class) {
513             // PHP is case insensitive, and we have stuff like ApiUpperCaseAction,
514             // but we at least make a point out of wanting to do stuff case-sensitive.
515             $class = ucfirst($class) . 'Action';
516             if ($this instanceof $class) {
517                 return true;
518             }
519         }
520         return false;
521     }
522
523     /**
524      * Show page, a template method.
525      *
526      * @return void
527      * @throws ClientException
528      * @throws ReflectionException
529      * @throws ServerException
530      */
531     public function showPage()
532     {
533         if (GNUsocial::isAjax()) {
534             self::showAjax();
535             return;
536         }
537         if (Event::handle('StartShowHTML', array($this))) {
538             $this->startHTML();
539             $this->flush();
540             Event::handle('EndShowHTML', array($this));
541         }
542         if (Event::handle('StartShowHead', array($this))) {
543             $this->showHead();
544             $this->flush();
545             Event::handle('EndShowHead', array($this));
546         }
547         if (Event::handle('StartShowBody', array($this))) {
548             $this->showBody();
549             Event::handle('EndShowBody', array($this));
550         }
551         if (Event::handle('StartEndHTML', array($this))) {
552             $this->endHTML();
553             Event::handle('EndEndHTML', array($this));
554         }
555     }
556
557     public function showAjax()
558     {
559         $this->startHTML('text/xml;charset=utf-8');
560         $this->elementStart('head');
561         // TRANS: Title for conversation page.
562         $this->element('title', null, $this->title());
563         $this->elementEnd('head');
564         $this->elementStart('body');
565         if ($this->getError()) {
566             $this->element('p', array('id' => 'error'), $this->getError());
567         } else {
568             $this->showContent();
569         }
570         $this->elementEnd('body');
571         $this->endHTML();
572     }
573
574     /**
575      * Returns the page title
576      *
577      * SHOULD overload
578      *
579      * @return string page title
580      */
581
582     public function title()
583     {
584         // TRANS: Page title for a page without a title set.
585         return _('Untitled page');
586     }
587
588     public function getError()
589     {
590         return $this->error;
591     }
592
593     /**
594      * Show content.
595      *
596      * MUST overload (unless there's not a notice)
597      *
598      * @return void
599      */
600     protected function showContent()
601     {
602     }
603
604     public function endHTML()
605     {
606         global $_startTime;
607
608         if (isset($_startTime)) {
609             $endTime = microtime(true);
610             $diff = round(($endTime - $_startTime) * 1000);
611             $this->raw("<!-- ${diff}ms -->");
612         }
613
614         parent::endHTML();
615     }
616
617     /**
618      * Show head, a template method.
619      *
620      * @return void
621      */
622     public function showHead()
623     {
624         // XXX: attributes (profile?)
625         $this->elementStart('head');
626         if (Event::handle('StartShowHeadElements', array($this))) {
627             if (Event::handle('StartShowHeadTitle', array($this))) {
628                 $this->showTitle();
629                 Event::handle('EndShowHeadTitle', array($this));
630             }
631             $this->showShortcutIcon();
632             $this->showStylesheets();
633             $this->showOpenSearch();
634             $this->showFeeds();
635             $this->showDescription();
636             $this->extraHead();
637             Event::handle('EndShowHeadElements', array($this));
638         }
639         $this->elementEnd('head');
640     }
641
642     /**
643      * Show title, a template method.
644      *
645      * @return void
646      */
647     public function showTitle()
648     {
649         $this->element(
650             'title',
651             null,
652             // TRANS: Page title. %1$s is the title, %2$s is the site name.
653             sprintf(
654                 _('%1$s - %2$s'),
655                 $this->title(),
656                 common_config('site', 'name')
657             )
658         );
659     }
660
661     /**
662      * Show themed shortcut icon
663      *
664      * @return void
665      */
666     public function showShortcutIcon()
667     {
668         if (is_readable(INSTALLDIR . '/theme/' . common_config('site', 'theme') . '/favicon.ico')) {
669             $this->element('link', array('rel' => 'shortcut icon',
670                 'href' => Theme::path('favicon.ico')));
671         } else {
672             // favicon.ico should be HTTPS if the rest of the page is
673             $this->element('link', array('rel' => 'shortcut icon',
674                 'href' => common_path('favicon.ico', GNUsocial::isHTTPS())));
675         }
676
677         if (common_config('site', 'mobile')) {
678             if (is_readable(INSTALLDIR . '/theme/' . common_config('site', 'theme') . '/apple-touch-icon.png')) {
679                 $this->element('link', array('rel' => 'apple-touch-icon',
680                     'href' => Theme::path('apple-touch-icon.png')));
681             } else {
682                 $this->element('link', array('rel' => 'apple-touch-icon',
683                     'href' => common_path('apple-touch-icon.png')));
684             }
685         }
686     }
687
688     /**
689      * Show stylesheets
690      *
691      * @return void
692      */
693     public function showStylesheets()
694     {
695         if (Event::handle('StartShowStyles', array($this))) {
696
697             // Use old name for StatusNet for compatibility on events
698
699             if (Event::handle('StartShowStylesheets', array($this))) {
700                 $this->primaryCssLink(null, 'screen, projection, tv, print');
701                 Event::handle('EndShowStylesheets', array($this));
702             }
703
704             $this->cssLink('js/extlib/jquery-ui/css/smoothness/jquery-ui.css');
705
706             if (Event::handle('StartShowUAStyles', array($this))) {
707                 Event::handle('EndShowUAStyles', array($this));
708             }
709
710             Event::handle('EndShowStyles', array($this));
711
712             if (common_config('custom_css', 'enabled')) {
713                 $css = common_config('custom_css', 'css');
714                 if (Event::handle('StartShowCustomCss', array($this, &$css))) {
715                     if (trim($css) != '') {
716                         $this->style($css);
717                     }
718                     Event::handle('EndShowCustomCss', array($this));
719                 }
720             }
721         }
722     }
723
724     public function primaryCssLink($mainTheme = null, $media = null)
725     {
726         $theme = new Theme($mainTheme);
727
728         // Some themes may have external stylesheets
729         foreach ($theme->getExternals() as $url) {
730             $this->cssLink($url, $mainTheme, $media);
731         }
732
733         // If the currently-selected theme has dependencies on other themes,
734         // we'll need to load their display.css files as well in order.
735         $baseThemes = $theme->getDeps();
736         foreach ($baseThemes as $baseTheme) {
737             $this->cssLink('css/display.css', $baseTheme, $media);
738         }
739         $this->cssLink('css/display.css', $mainTheme, $media);
740
741         // Additional styles for RTL languages
742         if (is_rtl(common_language())) {
743             if (file_exists(Theme::file('css/rtl.css'))) {
744                 $this->cssLink('css/rtl.css', $mainTheme, $media);
745             }
746         }
747     }
748
749     /**
750      * Show OpenSearch headers
751      *
752      * @return void
753      */
754     public function showOpenSearch()
755     {
756         $this->element('link', array('rel' => 'search',
757             'type' => 'application/opensearchdescription+xml',
758             'href' => common_local_url('opensearch', array('type' => 'people')),
759             'title' => common_config('site', 'name') . ' People Search'));
760         $this->element('link', array('rel' => 'search', 'type' => 'application/opensearchdescription+xml',
761             'href' => common_local_url('opensearch', array('type' => 'notice')),
762             'title' => common_config('site', 'name') . ' Notice Search'));
763     }
764
765     /**
766      * Show feed headers
767      *
768      * MAY overload
769      *
770      * @return void
771      */
772     public function showFeeds()
773     {
774         foreach ($this->getFeeds() as $feed) {
775             $this->element('link', array('rel' => $feed->rel(),
776                 'href' => $feed->url,
777                 'type' => $feed->mimeType(),
778                 'title' => $feed->title));
779         }
780     }
781
782     /**
783      * An array of feeds for this action.
784      *
785      * Returns an array of potential feeds for this action.
786      *
787      * @return array Feed object to show in head and links
788      */
789     public function getFeeds()
790     {
791         return [];
792     }
793
794     /**
795      * Show description.
796      *
797      * SHOULD overload
798      *
799      * @return void
800      */
801     public function showDescription()
802     {
803         // does nothing by default
804     }
805
806     /**
807      * Show extra stuff in <head>.
808      *
809      * MAY overload
810      *
811      * @return void
812      */
813     public function extraHead()
814     {
815         // does nothing by default
816     }
817
818     /**
819      * Show body.
820      *
821      * Calls template methods
822      *
823      * @return void
824      * @throws ServerException
825      * @throws ReflectionException
826      */
827     public function showBody()
828     {
829         $params = array('id' => $this->getActionName());
830         if ($this->scoped instanceof Profile) {
831             $params['class'] = 'user_in';
832         }
833         $this->elementStart('body', $params);
834         $this->elementStart('div', array('id' => 'wrap'));
835         if (Event::handle('StartShowHeader', array($this))) {
836             $this->showHeader();
837             $this->flush();
838             Event::handle('EndShowHeader', array($this));
839         }
840         $this->showCore();
841         $this->flush();
842         if (Event::handle('StartShowFooter', array($this))) {
843             $this->showFooter();
844             $this->flush();
845             Event::handle('EndShowFooter', array($this));
846         }
847         $this->elementEnd('div');
848         $this->showScripts();
849         $this->elementEnd('body');
850     }
851
852     public function getActionName()
853     {
854         return $this->action;
855     }
856
857     /**
858      * Show header of the page.
859      *
860      * Calls template methods
861      *
862      * @return void
863      * @throws ServerException
864      */
865     public function showHeader()
866     {
867         $this->elementStart('div', array('id' => 'header'));
868         $this->showLogo();
869         $this->showPrimaryNav();
870         if (Event::handle('StartShowSiteNotice', array($this))) {
871             $this->showSiteNotice();
872
873             Event::handle('EndShowSiteNotice', array($this));
874         }
875
876         $this->elementEnd('div');
877     }
878
879     /**
880      * Show configured logo.
881      *
882      * @return void
883      * @throws ServerException
884      */
885     public function showLogo()
886     {
887         $this->elementStart('address', array('id' => 'site_contact', 'class' => 'h-card'));
888         if (Event::handle('StartAddressData', array($this))) {
889             if (common_config('singleuser', 'enabled')) {
890                 $user = User::singleUser();
891                 $url = common_local_url(
892                     'showstream',
893                     array('nickname' => $user->nickname)
894                 );
895             } elseif (common_logged_in()) {
896                 $cur = common_current_user();
897                 $url = common_local_url('all', array('nickname' => $cur->nickname));
898             } else {
899                 $url = common_local_url('public');
900             }
901
902             $this->elementStart('a', array('class' => 'home bookmark',
903                 'href' => $url));
904
905             if (GNUsocial::isHTTPS()) {
906                 $logoUrl = common_config('site', 'ssllogo');
907                 if (empty($logoUrl)) {
908                     // if logo is an uploaded file, try to fall back to HTTPS file URL
909                     $httpUrl = common_config('site', 'logo');
910                     if (!empty($httpUrl)) {
911                         try {
912                             $f = File::getByUrl($httpUrl);
913                             if (!empty($f->filename)) {
914                                 // this will handle the HTTPS case
915                                 $logoUrl = File::url($f->filename);
916                             }
917                         } catch (NoResultException $e) {
918                             // no match
919                         }
920                     }
921                 }
922             } else {
923                 $logoUrl = common_config('site', 'logo');
924             }
925
926             if (empty($logoUrl) && file_exists(Theme::file('logo.png'))) {
927                 // This should handle the HTTPS case internally
928                 $logoUrl = Theme::path('logo.png');
929             }
930
931             if (!empty($logoUrl)) {
932                 $this->element('img', array('class' => 'logo u-photo p-name',
933                     'src' => $logoUrl,
934                     'alt' => common_config('site', 'name')));
935             }
936
937             $this->elementEnd('a');
938
939             Event::handle('EndAddressData', array($this));
940         }
941         $this->elementEnd('address');
942     }
943
944     /**
945      * Show primary navigation.
946      *
947      * @return void
948      */
949     public function showPrimaryNav()
950     {
951         $this->elementStart('div', array('id' => 'site_nav_global_primary'));
952
953         $user = common_current_user();
954
955         if (!empty($user) || !common_config('site', 'private')) {
956             $form = new SearchForm($this);
957             $form->show();
958         }
959
960         $pn = new PrimaryNav($this);
961         $pn->show();
962         $this->elementEnd('div');
963     }
964
965     /**
966      * Show site notice.
967      *
968      * @return void
969      */
970     public function showSiteNotice()
971     {
972         // Revist. Should probably do an hAtom pattern here
973         $text = common_config('site', 'notice');
974         if ($text) {
975             $this->elementStart('div', array('id' => 'site_notice',
976                 'class' => 'system_notice'));
977             $this->raw($text);
978             $this->elementEnd('div');
979         }
980     }
981
982     /**
983      * Show core.
984      *
985      * Shows local navigation, content block and aside.
986      *
987      * @return void
988      * @throws ReflectionException
989      */
990     public function showCore()
991     {
992         $this->elementStart('div', array('id' => 'core'));
993         $this->elementStart('div', array('id' => 'aside_primary_wrapper'));
994         $this->elementStart('div', array('id' => 'content_wrapper'));
995         $this->elementStart('div', array('id' => 'site_nav_local_views_wrapper'));
996         if (Event::handle('StartShowLocalNavBlock', array($this))) {
997             $this->showLocalNavBlock();
998             $this->flush();
999             Event::handle('EndShowLocalNavBlock', array($this));
1000         }
1001         if (Event::handle('StartShowContentBlock', array($this))) {
1002             $this->showContentBlock();
1003             $this->flush();
1004             Event::handle('EndShowContentBlock', array($this));
1005         }
1006         if (Event::handle('StartShowAside', array($this))) {
1007             $this->showAside();
1008             $this->flush();
1009             Event::handle('EndShowAside', array($this));
1010         }
1011         $this->elementEnd('div');
1012         $this->elementEnd('div');
1013         $this->elementEnd('div');
1014         $this->elementEnd('div');
1015     }
1016
1017     /**
1018      * Show local navigation block.
1019      *
1020      * @return void
1021      */
1022     public function showLocalNavBlock()
1023     {
1024         // Need to have this ID for CSS; I'm too lazy to add it to
1025         // all menus
1026         $this->elementStart('div', array('id' => 'site_nav_local_views'));
1027         // Cheat cheat cheat!
1028         $this->showLocalNav();
1029         $this->elementEnd('div');
1030     }
1031
1032     /**
1033      * Show local navigation.
1034      *
1035      * SHOULD overload
1036      *
1037      * @return void
1038      */
1039     public function showLocalNav()
1040     {
1041         $nav = new DefaultLocalNav($this);
1042         $nav->show();
1043     }
1044
1045     /**
1046      * Show content block.
1047      *
1048      * @return void
1049      * @throws ReflectionException
1050      */
1051     public function showContentBlock()
1052     {
1053         $this->elementStart('div', array('id' => 'content'));
1054         if (common_logged_in()) {
1055             if (Event::handle('StartShowNoticeForm', array($this))) {
1056                 $this->showNoticeForm();
1057                 Event::handle('EndShowNoticeForm', array($this));
1058             }
1059         }
1060         if (Event::handle('StartShowPageTitle', array($this))) {
1061             $this->showPageTitle();
1062             Event::handle('EndShowPageTitle', array($this));
1063         }
1064         $this->showPageNoticeBlock();
1065         $this->elementStart('div', array('id' => 'content_inner'));
1066         // show the actual content (forms, lists, whatever)
1067         $this->showContent();
1068         $this->elementEnd('div');
1069         $this->elementEnd('div');
1070     }
1071
1072     /**
1073      * Show notice form.
1074      *
1075      * MAY overload if no notice form needed... or direct message box????
1076      *
1077      * @return void
1078      */
1079     public function showNoticeForm()
1080     {
1081         // TRANS: Tab on the notice form.
1082         $tabs = array('status' => array('title' => _m('TAB', 'Status'),
1083             'href' => common_local_url('newnotice')));
1084
1085         $this->elementStart('div', 'input_forms');
1086
1087         $this->element('label', array('for' => 'input_form_nav'), _m('TAB', 'Share your:'));
1088
1089         if (Event::handle('StartShowEntryForms', array(&$tabs))) {
1090             $this->elementStart('ul', array('class' => 'nav',
1091                 'id' => 'input_form_nav'));
1092
1093             foreach ($tabs as $tag => $data) {
1094                 $tag = htmlspecialchars($tag);
1095                 $attrs = array('id' => 'input_form_nav_' . $tag,
1096                     'class' => 'input_form_nav_tab');
1097
1098                 if ($tag == 'status') {
1099                     $attrs['class'] .= ' current';
1100                 }
1101                 $this->elementStart('li', $attrs);
1102
1103                 $this->element(
1104                     'a',
1105                     array('onclick' => 'return SN.U.switchInputFormTab("' . $tag . '");',
1106                         'href' => $data['href']),
1107                     $data['title']
1108                 );
1109                 $this->elementEnd('li');
1110             }
1111
1112             $this->elementEnd('ul');
1113
1114             foreach ($tabs as $tag => $data) {
1115                 $attrs = array('class' => 'input_form',
1116                     'id' => 'input_form_' . $tag);
1117                 if ($tag == 'status') {
1118                     $attrs['class'] .= ' current';
1119                 }
1120
1121                 $this->elementStart('div', $attrs);
1122
1123                 $form = null;
1124
1125                 if (Event::handle('StartMakeEntryForm', array($tag, $this, &$form))) {
1126                     if ($tag == 'status') {
1127                         $options = $this->noticeFormOptions();
1128                         $form = new NoticeForm($this, $options);
1129                     }
1130                     Event::handle('EndMakeEntryForm', array($tag, $this, $form));
1131                 }
1132
1133                 if (!empty($form)) {
1134                     $form->show();
1135                 }
1136
1137                 $this->elementEnd('div');
1138             }
1139         }
1140
1141         $this->elementEnd('div');
1142     }
1143
1144     public function noticeFormOptions()
1145     {
1146         return [];
1147     }
1148
1149     /**
1150      * Show page title.
1151      *
1152      * @return void
1153      */
1154     public function showPageTitle()
1155     {
1156         $this->element('h1', null, $this->title());
1157     }
1158
1159     /**
1160      * Show page notice block.
1161      *
1162      * Only show the block if a subclassed action has overrided
1163      * Action::showPageNotice(), or an event handler is registered for
1164      * the StartShowPageNotice event, in which case we assume the
1165      * 'page_notice' definition list is desired.  This is to prevent
1166      * empty 'page_notice' definition lists from being output everywhere.
1167      *
1168      * @return void
1169      * @throws ReflectionException
1170      */
1171     public function showPageNoticeBlock()
1172     {
1173         $rmethod = new ReflectionMethod($this, 'showPageNotice');
1174         $dclass = $rmethod->getDeclaringClass()->getName();
1175
1176         if ($dclass != 'Action' || Event::hasHandler('StartShowPageNotice')) {
1177             $this->elementStart('div', array('id' => 'page_notice',
1178                 'class' => 'system_notice'));
1179             if (Event::handle('StartShowPageNotice', array($this))) {
1180                 $this->showPageNotice();
1181                 Event::handle('EndShowPageNotice', array($this));
1182             }
1183             $this->elementEnd('div');
1184         }
1185     }
1186
1187     /**
1188      * Show page notice.
1189      *
1190      * SHOULD overload (unless there's not a notice)
1191      *
1192      * @return void
1193      */
1194     public function showPageNotice()
1195     {
1196     }
1197
1198     /**
1199      * Show Aside.
1200      *
1201      * @return void
1202      * @throws ReflectionException
1203      */
1204     public function showAside()
1205     {
1206         $this->elementStart('div', array('id' => 'aside_primary',
1207             'class' => 'aside'));
1208         $this->showProfileBlock();
1209         if (Event::handle('StartShowObjectNavBlock', array($this))) {
1210             $this->showObjectNavBlock();
1211             Event::handle('EndShowObjectNavBlock', array($this));
1212         }
1213         if (Event::handle('StartShowSections', array($this))) {
1214             $this->showSections();
1215             Event::handle('EndShowSections', array($this));
1216         }
1217         if (Event::handle('StartShowExportData', array($this))) {
1218             $this->showExportData();
1219             Event::handle('EndShowExportData', array($this));
1220         }
1221         $this->elementEnd('div');
1222     }
1223
1224     /**
1225      * If there's a logged-in user, show a bit of login context
1226      *
1227      * @return void
1228      * @throws Exception
1229      */
1230     public function showProfileBlock()
1231     {
1232         if (common_logged_in()) {
1233             $block = new DefaultProfileBlock($this);
1234             $block->show();
1235         }
1236     }
1237
1238     /**
1239      * Show menu for an object (group, profile)
1240      *
1241      * This block will only show if a subclass has overridden
1242      * the showObjectNav() method.
1243      *
1244      * @return void
1245      * @throws ReflectionException
1246      */
1247     public function showObjectNavBlock()
1248     {
1249         $rmethod = new ReflectionMethod($this, 'showObjectNav');
1250         $dclass = $rmethod->getDeclaringClass()->getName();
1251
1252         if ($dclass != 'Action') {
1253             // Need to have this ID for CSS; I'm too lazy to add it to
1254             // all menus
1255             $this->elementStart('div', array('id' => 'site_nav_object',
1256                 'class' => 'section'));
1257             $this->showObjectNav();
1258             $this->elementEnd('div');
1259         }
1260     }
1261
1262     /**
1263      * Show object navigation.
1264      *
1265      * If there are things to do with this object, show it here.
1266      *
1267      * @return void
1268      */
1269     public function showObjectNav()
1270     {
1271         /* Nothing here. */
1272     }
1273
1274     /**
1275      * Show sections.
1276      *
1277      * SHOULD overload
1278      *
1279      * @return void
1280      */
1281     public function showSections()
1282     {
1283         // for each section, show it
1284     }
1285
1286     /**
1287      * Show export data feeds.
1288      *
1289      * @return void
1290      */
1291     public function showExportData()
1292     {
1293         $feeds = $this->getFeeds();
1294         if (!empty($feeds)) {
1295             $fl = new FeedList($this, $feeds);
1296             $fl->show();
1297         }
1298     }
1299
1300     /**
1301      * Show footer.
1302      *
1303      * @return void
1304      */
1305     public function showFooter()
1306     {
1307         $this->elementStart('div', array('id' => 'footer'));
1308         if (Event::handle('StartShowInsideFooter', array($this))) {
1309             $this->showSecondaryNav();
1310             $this->showLicenses();
1311             Event::handle('EndShowInsideFooter', array($this));
1312         }
1313         $this->elementEnd('div');
1314     }
1315
1316     /**
1317      * Show secondary navigation.
1318      *
1319      * @return void
1320      */
1321     public function showSecondaryNav()
1322     {
1323         $sn = new SecondaryNav($this);
1324         $sn->show();
1325     }
1326
1327     /**
1328      * Show licenses.
1329      *
1330      * @return void
1331      */
1332     public function showLicenses()
1333     {
1334         $this->showGNUsocialLicense();
1335         $this->showContentLicense();
1336     }
1337
1338     /**
1339      * Show GNU social license.
1340      *
1341      * @return void
1342      */
1343     public function showGNUsocialLicense()
1344     {
1345         if (common_config('site', 'broughtby')) {
1346             // TRANS: First sentence of the GNU social site license. Used if 'broughtby' is set.
1347             // TRANS: Text between [] is a link description, text between () is the link itself.
1348             // TRANS: Make sure there is no whitespace between "]" and "(".
1349             // TRANS: "%%site.broughtby%%" is the value of the variable site.broughtby
1350             $instr = _('**%%site.name%%** is a social network, courtesy of [%%site.broughtby%%](%%site.broughtbyurl%%).');
1351         } else {
1352             // TRANS: First sentence of the GNU social site license. Used if 'broughtby' is not set.
1353             $instr = _('**%%site.name%%** is a social network.');
1354         }
1355         $instr .= ' ';
1356         // TRANS: Second sentence of the GNU social site license. Mentions the GNU social source code license.
1357         // TRANS: Make sure there is no whitespace between "]" and "(".
1358         // TRANS: [%1$s](%2$s) is a link description followed by the link itself
1359         // TRANS: %3$s is the version of GNU social that is being used.
1360         $instr .= sprintf(_('It runs on [%1$s](%2$s), version %3$s, available under the [GNU Affero General Public License](http://www.fsf.org/licensing/licenses/agpl-3.0.html).'), GNUSOCIAL_ENGINE, GNUSOCIAL_ENGINE_URL, GNUSOCIAL_VERSION);
1361         $output = common_markup_to_html($instr);
1362         $this->raw($output);
1363         // do it
1364     }
1365
1366     /**
1367      * Show content license.
1368      *
1369      * @return void
1370      */
1371     public function showContentLicense()
1372     {
1373         if (Event::handle('StartShowContentLicense', array($this))) {
1374             switch (common_config('license', 'type')) {
1375                 case 'private':
1376                     // TRANS: Content license displayed when license is set to 'private'.
1377                     // TRANS: %1$s is the site name.
1378                     $this->element('p', null, sprintf(
1379                         _('Content and data of %1$s are private and confidential.'),
1380                         common_config('site', 'name')
1381                     ));
1382                 // fall through
1383                 // no break
1384                 case 'allrightsreserved':
1385                     if (common_config('license', 'owner')) {
1386                         // TRANS: Content license displayed when license is set to 'allrightsreserved'.
1387                         // TRANS: %1$s is the copyright owner.
1388                         $this->element('p', null, sprintf(
1389                             _('Content and data copyright by %1$s. All rights reserved.'),
1390                             common_config('license', 'owner')
1391                         ));
1392                     } else {
1393                         // TRANS: Content license displayed when license is set to 'allrightsreserved' and no owner is set.
1394                         $this->element('p', null, _('Content and data copyright by contributors. All rights reserved.'));
1395                     }
1396                     break;
1397                 case 'cc': // fall through
1398                 default:
1399                     $this->elementStart('p');
1400
1401                     $image = common_config('license', 'image');
1402                     $sslimage = common_config('license', 'sslimage');
1403
1404                     if (GNUsocial::isHTTPS()) {
1405                         if (!empty($sslimage)) {
1406                             $url = $sslimage;
1407                         } elseif (preg_match('#^http://i.creativecommons.org/#', $image)) {
1408                             // CC support HTTPS on their images
1409                             $url = preg_replace('/^http/', 'https', $image, 1);
1410                         } else {
1411                             // Better to show mixed content than no content
1412                             $url = $image;
1413                         }
1414                     } else {
1415                         $url = $image;
1416                     }
1417
1418                     $this->element('img', array('id' => 'license_cc',
1419                         'src' => $url,
1420                         'alt' => common_config('license', 'title'),
1421                         'width' => '80',
1422                         'height' => '15'));
1423                     $this->text(' ');
1424                     // TRANS: license message in footer.
1425                     // TRANS: %1$s is the site name, %2$s is a link to the license URL, with a licence name set in configuration.
1426                     $notice = _('All %1$s content and data are available under the %2$s license.');
1427                     $link = sprintf(
1428                         '<a class="license" rel="external license" href="%1$s">%2$s</a>',
1429                         htmlspecialchars(common_config('license', 'url')),
1430                         htmlspecialchars(common_config('license', 'title'))
1431                     );
1432                     $this->raw(@sprintf(
1433                         htmlspecialchars($notice),
1434                         htmlspecialchars(common_config('site', 'name')),
1435                         $link
1436                     ));
1437                     $this->elementEnd('p');
1438                     break;
1439             }
1440
1441             Event::handle('EndShowContentLicense', array($this));
1442         }
1443     }
1444
1445     /**
1446      * Show javascript headers
1447      *
1448      * @return void
1449      */
1450     public function showScripts()
1451     {
1452         if (Event::handle('StartShowScripts', array($this))) {
1453             if (Event::handle('StartShowJQueryScripts', array($this))) {
1454                 $this->script('extlib/jquery.js');
1455                 $this->script('extlib/jquery.form.js');
1456                 $this->script('extlib/jquery-ui/jquery-ui.js');
1457                 $this->script('extlib/jquery.cookie.js');
1458
1459                 Event::handle('EndShowJQueryScripts', array($this));
1460             }
1461             if (Event::handle('StartShowStatusNetScripts', array($this))) {
1462                 $this->script('util.js');
1463                 $this->script('xbImportNode.js');
1464
1465                 // This route isn't available in single-user mode.
1466                 // Not sure why, but it causes errors here.
1467                 $this->inlineScript('var _peopletagAC = "' .
1468                     common_local_url('peopletagautocomplete') . '";');
1469                 $this->showScriptMessages();
1470                 $this->showScriptVariables();
1471                 // Anti-framing code to avoid clickjacking attacks in older browsers.
1472                 // This will show a blank page if the page is being framed, which is
1473                 // consistent with the behavior of the 'X-Frame-Options: SAMEORIGIN'
1474                 // header, which prevents framing in newer browser.
1475                 if (common_config('javascript', 'bustframes')) {
1476                     $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 = ""; }; }');
1477                 }
1478                 Event::handle('EndShowStatusNetScripts', array($this));
1479             }
1480             Event::handle('EndShowScripts', array($this));
1481         }
1482     }
1483
1484     /**
1485      * Exports a map of localized text strings to JavaScript code.
1486      *
1487      * Plugins can add to what's exported by hooking the StartScriptMessages or EndScriptMessages
1488      * events and appending to the array. Try to avoid adding strings that won't be used, as
1489      * they'll be added to HTML output.
1490      */
1491     public function showScriptMessages()
1492     {
1493         $messages = [];
1494
1495         if (Event::handle('StartScriptMessages', array($this, &$messages))) {
1496             // Common messages needed for timeline views etc...
1497
1498             // TRANS: Localized tooltip for '...' expansion button on overlong remote messages.
1499             $messages['showmore_tooltip'] = _m('TOOLTIP', 'Show more');
1500             $messages['popup_close_button'] = _m('TOOLTIP', 'Close popup');
1501
1502             $messages = array_merge($messages, $this->getScriptMessages());
1503
1504             Event::handle('EndScriptMessages', array($this, &$messages));
1505         }
1506
1507         if (!empty($messages)) {
1508             $this->inlineScript('SN.messages=' . json_encode($messages));
1509         }
1510
1511         return $messages;
1512     }
1513
1514     /**
1515      * If the action will need localizable text strings, export them here like so:
1516      *
1517      * return array('pool_deepend' => _('Deep end'),
1518      *              'pool_shallow' => _('Shallow end'));
1519      *
1520      * The exported map will be available via SN.msg() to JS code:
1521      *
1522      *   $('#pool').html('<div class="deepend"></div><div class="shallow"></div>');
1523      *   $('#pool .deepend').text(SN.msg('pool_deepend'));
1524      *   $('#pool .shallow').text(SN.msg('pool_shallow'));
1525      *
1526      * Exports a map of localized text strings to JavaScript code.
1527      *
1528      * Plugins can add to what's exported on any action by hooking the StartScriptMessages or
1529      * EndScriptMessages events and appending to the array. Try to avoid adding strings that won't
1530      * be used, as they'll be added to HTML output.
1531      */
1532     public function getScriptMessages()
1533     {
1534         return [];
1535     }
1536
1537     protected function showScriptVariables()
1538     {
1539         $vars = [];
1540
1541         if (Event::handle('StartScriptVariables', array($this, &$vars))) {
1542             $vars['urlNewNotice'] = common_local_url('newnotice');
1543             $vars['xhrTimeout'] = ini_get('max_execution_time') * 1000;   // milliseconds
1544             Event::handle('EndScriptVariables', array($this, &$vars));
1545         }
1546
1547         $this->inlineScript('SN.V = ' . json_encode($vars) . ';');
1548
1549         return $vars;
1550     }
1551
1552     /**
1553      * Show anonymous message.
1554      *
1555      * SHOULD overload
1556      *
1557      * @return void
1558      */
1559     public function showAnonymousMessage()
1560     {
1561         // needs to be defined by the class
1562     }
1563
1564     /**
1565      * This is a cheap hack to avoid a bug in DB_DataObject
1566      * where '' is non-type-aware compared to 0, which means it
1567      * will always be true for values like false and 0 too...
1568      *
1569      * Upstream bug is::
1570      * https://pear.php.net/bugs/bug.php?id=20291
1571      */
1572     public function booleanintstring($key, $def = false)
1573     {
1574         return $this->boolean($key, $def) ? '1' : '0';
1575     }
1576
1577     /**
1578      * Integer value of an argument
1579      *
1580      * @param string $key query key we're interested in
1581      * @param string $defValue optional default value (default null)
1582      * @param string $maxValue optional max value (default null)
1583      * @param string $minValue optional min value (default null)
1584      *
1585      * @return integer integer value
1586      */
1587     public function int($key, $defValue = null, $maxValue = null, $minValue = null)
1588     {
1589         $arg = intval($this->arg($key));
1590
1591         if (!is_numeric($this->arg($key)) || $arg != $this->arg($key)) {
1592             return $defValue;
1593         }
1594
1595         if (!is_null($maxValue)) {
1596             $arg = min($arg, $maxValue);
1597         }
1598
1599         if (!is_null($minValue)) {
1600             $arg = max($arg, $minValue);
1601         }
1602
1603         return $arg;
1604     }
1605
1606     /**
1607      * Returns the current URL
1608      *
1609      * @return string current URL
1610      */
1611     public function selfUrl()
1612     {
1613         list($action, $args) = $this->returnToArgs();
1614         return common_local_url($action, $args);
1615     }
1616
1617     /**
1618      * Generate pagination links
1619      *
1620      * @param boolean $have_before is there something before?
1621      * @param boolean $have_after is there something after?
1622      * @param integer $page current page
1623      * @param string $action current action
1624      * @param array $args rest of query arguments
1625      *
1626      * @return void
1627      */
1628     // XXX: The messages in this pagination method only tailor to navigating
1629     //      notices. In other lists, "Previous"/"Next" type navigation is
1630     //      desirable, but not available.
1631     /**
1632      * Returns arguments sufficient for re-constructing URL
1633      *
1634      * @return array two elements: action, other args
1635      */
1636     public function returnToArgs()
1637     {
1638         $action = $this->getActionName();
1639         $args = $this->args;
1640         unset($args['action']);
1641         if (common_config('site', 'fancy')) {
1642             unset($args['p']);
1643         }
1644         if (array_key_exists('submit', $args)) {
1645             unset($args['submit']);
1646         }
1647         foreach (array_keys($_COOKIE) as $cookie) {
1648             unset($args[$cookie]);
1649         }
1650         return array($action, $args);
1651     }
1652
1653     /**
1654      * Generate a menu item
1655      *
1656      * @param string $url menu URL
1657      * @param string $text menu name
1658      * @param string $title title attribute, null by default
1659      * @param boolean $is_selected current menu item, false by default
1660      * @param string $id element id, null by default
1661      *
1662      * @return void
1663      */
1664     public function menuItem($url, $text, $title = null, $is_selected = false, $id = null, $class = null)
1665     {
1666         // Added @id to li for some control.
1667         // XXX: We might want to move this to htmloutputter.php
1668         $lattrs = [];
1669         $classes = [];
1670         if ($class !== null) {
1671             $classes[] = trim($class);
1672         }
1673         if ($is_selected) {
1674             $classes[] = 'current';
1675         }
1676
1677         if (!empty($classes)) {
1678             $lattrs['class'] = implode(' ', $classes);
1679         }
1680
1681         if (!is_null($id)) {
1682             $lattrs['id'] = $id;
1683         }
1684
1685         $this->elementStart('li', $lattrs);
1686         $attrs['href'] = $url;
1687         if ($title) {
1688             $attrs['title'] = $title;
1689         }
1690         $this->element('a', $attrs, $text);
1691         $this->elementEnd('li');
1692     }
1693
1694     /**
1695      * Check the session token.
1696      *
1697      * Checks that the current form has the correct session token,
1698      * and throw an exception if it does not.
1699      *
1700      * @return void
1701      */
1702     // XXX: Finding this type of check with the same message about 50 times.
1703     //      Possible to refactor?
1704
1705     public function pagination($have_before, $have_after, $page, $action, $args = null)
1706     {
1707         // Does a little before-after block for next/prev page
1708         if ($have_before || $have_after) {
1709             $this->elementStart('ul', array('class' => 'nav',
1710                 'id' => 'pagination'));
1711         }
1712         if ($have_before) {
1713             $pargs = array('page' => $page - 1);
1714             $this->elementStart('li', array('class' => 'nav_prev'));
1715             $this->element(
1716                 'a',
1717                 array('href' => common_local_url($action, $args, $pargs),
1718                     'rel' => 'prev'),
1719                 // TRANS: Pagination message to go to a page displaying information more in the
1720                 // TRANS: present than the currently displayed information.
1721                 _('After')
1722             );
1723             $this->elementEnd('li');
1724         }
1725         if ($have_after) {
1726             $pargs = array('page' => $page + 1);
1727             $this->elementStart('li', array('class' => 'nav_next'));
1728             $this->element(
1729                 'a',
1730                 array('href' => common_local_url($action, $args, $pargs),
1731                     'rel' => 'next'),
1732                 // TRANS: Pagination message to go to a page displaying information more in the
1733                 // TRANS: past than the currently displayed information.
1734                 _('Before')
1735             );
1736             $this->elementEnd('li');
1737         }
1738         if ($have_before || $have_after) {
1739             $this->elementEnd('ul');
1740         }
1741     }
1742
1743
1744     public function checkSessionToken()
1745     {
1746         // CSRF protection
1747         $token = $this->trimmed('token');
1748         if (empty($token) || $token != common_session_token()) {
1749             // TRANS: Client error text when there is a problem with the session token.
1750             $this->clientError(_('There was a problem with your session token.'));
1751         }
1752     }
1753 }