3 * StatusNet, the distributed open-source microblogging tool
5 * Base class for all actions (~views)
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.
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.
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/>.
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/
31 if (!defined('GNUSOCIAL')) {
36 * Base class for all actions
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.
41 * Actions are responsible for extracting and validating parameters; using
42 * model classes to read and write to the database; and doing ouput.
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/
53 class Action extends HTMLOutputter // lawsuit
55 // This should be protected/private in the future
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?
66 // The currently scoped profile (normally Profile::current; from $this->auth_user for API)
67 protected $scoped = null;
69 // Related to front-end user representation
70 protected $format = null;
71 protected $error = null;
72 protected $msg = null;
77 * Just wraps the HTMLOutputter constructor.
79 * @param string $output URI to output to, default = stdout
80 * @param boolean $indent Whether to indent output, default true
82 * @see XMLOutputter::__construct
83 * @see HTMLOutputter::__construct
85 public function __construct($output = 'php://output', $indent = null)
87 parent::__construct($output, $indent);
90 public static function run(array $args = [], $output = 'php://output', $indent = null)
92 $class = get_called_class();
93 $action = new $class($output, $indent);
94 set_exception_handler(array($action, 'handleError'));
95 $action->execute($args);
99 public function getInfo()
104 public function handleError($e)
106 if ($e instanceof ClientException) {
107 $this->clientError($e->getMessage(), $e->getCode());
108 } elseif ($e instanceof ServerException) {
109 $this->serverError($e->getMessage(), $e->getCode());
111 // If it wasn't specified more closely which kind of exception it was
112 $this->serverError($e->getMessage(), 500);
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
124 * @throws ClientException always
126 public function clientError($msg, $code = 400, $format = null)
128 // $format is currently only relevant for an ApiAction anyway
129 if ($format === null) {
130 $format = $this->format;
133 common_debug("User error '{$code}' on '{$this->action}': {$msg}", __FILE__);
135 if (!array_key_exists($code, ClientErrorAction::$status)) {
139 $status_string = ClientErrorAction::$status[$code];
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');
152 if (!isset($this->callback)) {
153 header("HTTP/1.1 {$code} {$status_string}");
155 $this->initDocument('json');
156 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
157 print(json_encode($error_array));
158 $this->endDocument('json');
161 header("HTTP/1.1 {$code} {$status_string}");
162 header('Content-Type: text/plain; charset=utf-8');
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);
173 public function execute(array $args = [])
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);
183 $mirror = common_config('db', 'mirror');
186 // everyone else uses the mirror
187 common_config_set('db', 'database', $mirror);
190 if (Event::handle('StartActionExecute', array($this, &$args))) {
191 $prepared = $this->prepare($args);
195 common_debug('Prepare failed for Action.');
199 Event::handle('EndActionExecute', array($this));
204 * Return true if read only.
208 * @param array $args other arguments
210 * @return boolean is read only action?
212 public function isReadOnly($args)
218 * For initializing members of the class.
220 * @param array $args misc. arguments
222 * @return boolean true
223 * @throws ClientException
225 protected function prepare(array $args = [])
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);
232 // needPost, of course, overrides canPost if true
233 if (!$this->canPost) {
234 $this->canPost = $this->needPost;
237 $this->args = common_copy_args($args);
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'));
243 if ($this->ajax || $this->boolean('ajax')) {
244 // check with GNUsocial::isAjax()
245 GNUsocial::setAjax(true);
248 if ($this->needLogin) {
249 $this->checkLogin(); // if not logged in, this redirs/excepts
252 $this->updateScopedProfile();
258 * Check if the current request is a POST
260 * @return boolean true if POST; otherwise false.
263 public function isPost()
265 return ($_SERVER['REQUEST_METHOD'] == 'POST');
268 // Must be run _after_ prepare
271 * Returns trimmed query argument or default value if not found
273 * @param string $key requested argument
274 * @param string $def default value to return if $key is not provided
276 * @return boolean is read only action?
278 public function trimmed($key, $def = null)
280 $arg = $this->arg($key, $def);
281 return is_string($arg) ? trim($arg) : $arg;
285 * Returns query argument or default value if not found
287 * @param string $key requested argument
288 * @param string $def default value to return if $key is not provided
290 * @return boolean is read only action?
292 public function arg($key, $def = null)
294 if (array_key_exists($key, $this->args)) {
295 return $this->args[$key];
302 * Boolean understands english (yes, no, true, false)
304 * @param string $key query key we're interested in
305 * @param string $def default value
307 * @return boolean interprets yes/no strings as boolean
309 public function boolean($key, $def = false)
311 $arg = strtolower($this->trimmed($key));
315 } elseif (in_array($arg, array('true', 'yes', '1', 'on'))) {
317 } elseif (in_array($arg, array('false', 'no', '0'))) {
325 * If not logged in, take appropriate action (redir or exception)
327 * @param boolean $redir Redirect to login if not logged in
329 * @return boolean true if logged in (never returns if not)
330 * @throws ClientException
332 public function checkLogin($redir = true)
334 if (common_logged_in()) {
338 if ($redir == true) {
339 common_set_returnto($_SERVER['REQUEST_URI']);
340 common_redirect(common_local_url('login'));
343 // TRANS: Error message displayed when trying to perform an action that requires a logged in user.
344 $this->clientError(_('Not logged in.'), 403);
347 public function updateScopedProfile()
349 $this->scoped = Profile::current();
350 return $this->scoped;
356 protected function handle()
358 header('Vary: Accept-Encoding,Cookie');
360 $lm = $this->lastModified();
361 $etag = $this->etag();
364 header('ETag: ' . $etag);
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");
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.
383 if ($this->_hasEtag($etag, $if_none_match)) {
384 header('HTTP/1.1 304 Not Modified');
385 // Better way to do this?
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);
395 header('HTTP/1.1 304 Not Modified');
396 // Better way to do this?
403 * Return last modified, if applicable.
407 * @return string last modified http header
409 public function lastModified()
411 // For comparison with If-Last-Modified
412 // If not applicable, return null
417 * Return etag, if applicable.
421 * @return string etag http header
423 public function etag()
429 * Is this action cacheable?
431 * If the action returns a last-modified
433 * @return boolean is read only action?
435 public function isCacheable()
441 * Has etag? (private)
443 * @param string $etag etag http header
444 * @param string $if_none_match ifNoneMatch http header
448 public function _hasEtag($etag, $if_none_match)
450 $etags = explode(',', $if_none_match);
451 return in_array($etag, $etags) || in_array('*', $etags);
457 * @param string $msg error message to display
458 * @param integer $code http error code, 500 by default
460 * @param string $format
463 public function serverError($msg, $code = 500, $format = null)
465 if ($format === null) {
466 $format = $this->format;
469 common_debug("Server error '{$code}' on '{$this->action}': {$msg}", __FILE__);
471 if (!array_key_exists($code, ServerErrorAction::$status)) {
475 $status_string = ServerErrorAction::$status[$code];
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');
488 if (!isset($this->callback)) {
489 header("HTTP/1.1 {$code} {$status_string}");
491 $this->initDocument('json');
492 $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
493 print(json_encode($error_array));
494 $this->endDocument('json');
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);
505 public function getScoped()
507 return ($this->scoped instanceof Profile) ? $this->scoped : null;
510 public function isAction(array $names)
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) {
524 * Show page, a template method.
527 * @throws ClientException
528 * @throws ReflectionException
529 * @throws ServerException
531 public function showPage()
533 if (GNUsocial::isAjax()) {
537 if (Event::handle('StartShowHTML', array($this))) {
540 Event::handle('EndShowHTML', array($this));
542 if (Event::handle('StartShowHead', array($this))) {
545 Event::handle('EndShowHead', array($this));
547 if (Event::handle('StartShowBody', array($this))) {
549 Event::handle('EndShowBody', array($this));
551 if (Event::handle('StartEndHTML', array($this))) {
553 Event::handle('EndEndHTML', array($this));
557 public function showAjax()
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());
568 $this->showContent();
570 $this->elementEnd('body');
575 * Returns the page title
579 * @return string page title
582 public function title()
584 // TRANS: Page title for a page without a title set.
585 return _('Untitled page');
588 public function getError()
596 * MUST overload (unless there's not a notice)
600 protected function showContent()
604 public function endHTML()
608 if (isset($_startTime)) {
609 $endTime = microtime(true);
610 $diff = round(($endTime - $_startTime) * 1000);
611 $this->raw("<!-- ${diff}ms -->");
618 * Show head, a template method.
622 public function showHead()
624 // XXX: attributes (profile?)
625 $this->elementStart('head');
626 if (Event::handle('StartShowHeadElements', array($this))) {
627 if (Event::handle('StartShowHeadTitle', array($this))) {
629 Event::handle('EndShowHeadTitle', array($this));
631 $this->showShortcutIcon();
632 $this->showStylesheets();
633 $this->showOpenSearch();
635 $this->showDescription();
637 Event::handle('EndShowHeadElements', array($this));
639 $this->elementEnd('head');
643 * Show title, a template method.
647 public function showTitle()
652 // TRANS: Page title. %1$s is the title, %2$s is the site name.
656 common_config('site', 'name')
662 * Show themed shortcut icon
666 public function showShortcutIcon()
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')));
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())));
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')));
682 $this->element('link', array('rel' => 'apple-touch-icon',
683 'href' => common_path('apple-touch-icon.png')));
693 public function showStylesheets()
695 if (Event::handle('StartShowStyles', array($this))) {
697 // Use old name for StatusNet for compatibility on events
699 if (Event::handle('StartShowStylesheets', array($this))) {
700 $this->primaryCssLink(null, 'screen, projection, tv, print');
701 Event::handle('EndShowStylesheets', array($this));
704 $this->cssLink('js/extlib/jquery-ui/css/smoothness/jquery-ui.css');
706 if (Event::handle('StartShowUAStyles', array($this))) {
707 Event::handle('EndShowUAStyles', array($this));
710 Event::handle('EndShowStyles', array($this));
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) != '') {
718 Event::handle('EndShowCustomCss', array($this));
724 public function primaryCssLink($mainTheme = null, $media = null)
726 $theme = new Theme($mainTheme);
728 // Some themes may have external stylesheets
729 foreach ($theme->getExternals() as $url) {
730 $this->cssLink($url, $mainTheme, $media);
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);
739 $this->cssLink('css/display.css', $mainTheme, $media);
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);
750 * Show OpenSearch headers
754 public function showOpenSearch()
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'));
772 public function showFeeds()
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));
783 * An array of feeds for this action.
785 * Returns an array of potential feeds for this action.
787 * @return array Feed object to show in head and links
789 public function getFeeds()
801 public function showDescription()
803 // does nothing by default
807 * Show extra stuff in <head>.
813 public function extraHead()
815 // does nothing by default
821 * Calls template methods
824 * @throws ServerException
825 * @throws ReflectionException
827 public function showBody()
829 $params = array('id' => $this->getActionName());
830 if ($this->scoped instanceof Profile) {
831 $params['class'] = 'user_in';
833 $this->elementStart('body', $params);
834 $this->elementStart('div', array('id' => 'wrap'));
835 if (Event::handle('StartShowHeader', array($this))) {
838 Event::handle('EndShowHeader', array($this));
842 if (Event::handle('StartShowFooter', array($this))) {
845 Event::handle('EndShowFooter', array($this));
847 $this->elementEnd('div');
848 $this->showScripts();
849 $this->elementEnd('body');
852 public function getActionName()
854 return $this->action;
858 * Show header of the page.
860 * Calls template methods
863 * @throws ServerException
865 public function showHeader()
867 $this->elementStart('div', array('id' => 'header'));
869 $this->showPrimaryNav();
870 if (Event::handle('StartShowSiteNotice', array($this))) {
871 $this->showSiteNotice();
873 Event::handle('EndShowSiteNotice', array($this));
876 $this->elementEnd('div');
880 * Show configured logo.
883 * @throws ServerException
885 public function showLogo()
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(
893 array('nickname' => $user->nickname)
895 } elseif (common_logged_in()) {
896 $cur = common_current_user();
897 $url = common_local_url('all', array('nickname' => $cur->nickname));
899 $url = common_local_url('public');
902 $this->elementStart('a', array('class' => 'home bookmark',
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)) {
912 $f = File::getByUrl($httpUrl);
913 if (!empty($f->filename)) {
914 // this will handle the HTTPS case
915 $logoUrl = File::url($f->filename);
917 } catch (NoResultException $e) {
923 $logoUrl = common_config('site', 'logo');
926 if (empty($logoUrl) && file_exists(Theme::file('logo.png'))) {
927 // This should handle the HTTPS case internally
928 $logoUrl = Theme::path('logo.png');
931 if (!empty($logoUrl)) {
932 $this->element('img', array('class' => 'logo u-photo p-name',
934 'alt' => common_config('site', 'name')));
937 $this->elementEnd('a');
939 Event::handle('EndAddressData', array($this));
941 $this->elementEnd('address');
945 * Show primary navigation.
949 public function showPrimaryNav()
951 $this->elementStart('div', array('id' => 'site_nav_global_primary'));
953 $user = common_current_user();
955 if (!empty($user) || !common_config('site', 'private')) {
956 $form = new SearchForm($this);
960 $pn = new PrimaryNav($this);
962 $this->elementEnd('div');
970 public function showSiteNotice()
972 // Revist. Should probably do an hAtom pattern here
973 $text = common_config('site', 'notice');
975 $this->elementStart('div', array('id' => 'site_notice',
976 'class' => 'system_notice'));
978 $this->elementEnd('div');
985 * Shows local navigation, content block and aside.
988 * @throws ReflectionException
990 public function showCore()
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();
999 Event::handle('EndShowLocalNavBlock', array($this));
1001 if (Event::handle('StartShowContentBlock', array($this))) {
1002 $this->showContentBlock();
1004 Event::handle('EndShowContentBlock', array($this));
1006 if (Event::handle('StartShowAside', array($this))) {
1009 Event::handle('EndShowAside', array($this));
1011 $this->elementEnd('div');
1012 $this->elementEnd('div');
1013 $this->elementEnd('div');
1014 $this->elementEnd('div');
1018 * Show local navigation block.
1022 public function showLocalNavBlock()
1024 // Need to have this ID for CSS; I'm too lazy to add it to
1026 $this->elementStart('div', array('id' => 'site_nav_local_views'));
1027 // Cheat cheat cheat!
1028 $this->showLocalNav();
1029 $this->elementEnd('div');
1033 * Show local navigation.
1039 public function showLocalNav()
1041 $nav = new DefaultLocalNav($this);
1046 * Show content block.
1049 * @throws ReflectionException
1051 public function showContentBlock()
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));
1060 if (Event::handle('StartShowPageTitle', array($this))) {
1061 $this->showPageTitle();
1062 Event::handle('EndShowPageTitle', array($this));
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');
1075 * MAY overload if no notice form needed... or direct message box????
1079 public function showNoticeForm()
1081 // TRANS: Tab on the notice form.
1082 $tabs = array('status' => array('title' => _m('TAB', 'Status'),
1083 'href' => common_local_url('newnotice')));
1085 $this->elementStart('div', 'input_forms');
1087 $this->element('label', array('for' => 'input_form_nav'), _m('TAB', 'Share your:'));
1089 if (Event::handle('StartShowEntryForms', array(&$tabs))) {
1090 $this->elementStart('ul', array('class' => 'nav',
1091 'id' => 'input_form_nav'));
1093 foreach ($tabs as $tag => $data) {
1094 $tag = htmlspecialchars($tag);
1095 $attrs = array('id' => 'input_form_nav_' . $tag,
1096 'class' => 'input_form_nav_tab');
1098 if ($tag == 'status') {
1099 $attrs['class'] .= ' current';
1101 $this->elementStart('li', $attrs);
1105 array('onclick' => 'return SN.U.switchInputFormTab("' . $tag . '");',
1106 'href' => $data['href']),
1109 $this->elementEnd('li');
1112 $this->elementEnd('ul');
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';
1121 $this->elementStart('div', $attrs);
1125 if (Event::handle('StartMakeEntryForm', array($tag, $this, &$form))) {
1126 if ($tag == 'status') {
1127 $options = $this->noticeFormOptions();
1128 $form = new NoticeForm($this, $options);
1130 Event::handle('EndMakeEntryForm', array($tag, $this, $form));
1133 if (!empty($form)) {
1137 $this->elementEnd('div');
1141 $this->elementEnd('div');
1144 public function noticeFormOptions()
1154 public function showPageTitle()
1156 $this->element('h1', null, $this->title());
1160 * Show page notice block.
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.
1169 * @throws ReflectionException
1171 public function showPageNoticeBlock()
1173 $rmethod = new ReflectionMethod($this, 'showPageNotice');
1174 $dclass = $rmethod->getDeclaringClass()->getName();
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));
1183 $this->elementEnd('div');
1190 * SHOULD overload (unless there's not a notice)
1194 public function showPageNotice()
1202 * @throws ReflectionException
1204 public function showAside()
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));
1213 if (Event::handle('StartShowSections', array($this))) {
1214 $this->showSections();
1215 Event::handle('EndShowSections', array($this));
1217 if (Event::handle('StartShowExportData', array($this))) {
1218 $this->showExportData();
1219 Event::handle('EndShowExportData', array($this));
1221 $this->elementEnd('div');
1225 * If there's a logged-in user, show a bit of login context
1230 public function showProfileBlock()
1232 if (common_logged_in()) {
1233 $block = new DefaultProfileBlock($this);
1239 * Show menu for an object (group, profile)
1241 * This block will only show if a subclass has overridden
1242 * the showObjectNav() method.
1245 * @throws ReflectionException
1247 public function showObjectNavBlock()
1249 $rmethod = new ReflectionMethod($this, 'showObjectNav');
1250 $dclass = $rmethod->getDeclaringClass()->getName();
1252 if ($dclass != 'Action') {
1253 // Need to have this ID for CSS; I'm too lazy to add it to
1255 $this->elementStart('div', array('id' => 'site_nav_object',
1256 'class' => 'section'));
1257 $this->showObjectNav();
1258 $this->elementEnd('div');
1263 * Show object navigation.
1265 * If there are things to do with this object, show it here.
1269 public function showObjectNav()
1281 public function showSections()
1283 // for each section, show it
1287 * Show export data feeds.
1291 public function showExportData()
1293 $feeds = $this->getFeeds();
1294 if (!empty($feeds)) {
1295 $fl = new FeedList($this, $feeds);
1305 public function showFooter()
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));
1313 $this->elementEnd('div');
1317 * Show secondary navigation.
1321 public function showSecondaryNav()
1323 $sn = new SecondaryNav($this);
1332 public function showLicenses()
1334 $this->showGNUsocialLicense();
1335 $this->showContentLicense();
1339 * Show GNU social license.
1343 public function showGNUsocialLicense()
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%%).');
1352 // TRANS: First sentence of the GNU social site license. Used if 'broughtby' is not set.
1353 $instr = _('**%%site.name%%** is a social network.');
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);
1367 * Show content license.
1371 public function showContentLicense()
1373 if (Event::handle('StartShowContentLicense', array($this))) {
1374 switch (common_config('license', 'type')) {
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')
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')
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.'));
1397 case 'cc': // fall through
1399 $this->elementStart('p');
1401 $image = common_config('license', 'image');
1402 $sslimage = common_config('license', 'sslimage');
1404 if (GNUsocial::isHTTPS()) {
1405 if (!empty($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);
1411 // Better to show mixed content than no content
1418 $this->element('img', array('id' => 'license_cc',
1420 'alt' => common_config('license', 'title'),
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.');
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'))
1432 $this->raw(@sprintf(
1433 htmlspecialchars($notice),
1434 htmlspecialchars(common_config('site', 'name')),
1437 $this->elementEnd('p');
1441 Event::handle('EndShowContentLicense', array($this));
1446 * Show javascript headers
1450 public function showScripts()
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');
1459 Event::handle('EndShowJQueryScripts', array($this));
1461 if (Event::handle('StartShowStatusNetScripts', array($this))) {
1462 $this->script('util.js');
1463 $this->script('xbImportNode.js');
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 = ""; }; }');
1478 Event::handle('EndShowStatusNetScripts', array($this));
1480 Event::handle('EndShowScripts', array($this));
1485 * Exports a map of localized text strings to JavaScript code.
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.
1491 public function showScriptMessages()
1495 if (Event::handle('StartScriptMessages', array($this, &$messages))) {
1496 // Common messages needed for timeline views etc...
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');
1502 $messages = array_merge($messages, $this->getScriptMessages());
1504 Event::handle('EndScriptMessages', array($this, &$messages));
1507 if (!empty($messages)) {
1508 $this->inlineScript('SN.messages=' . json_encode($messages));
1515 * If the action will need localizable text strings, export them here like so:
1517 * return array('pool_deepend' => _('Deep end'),
1518 * 'pool_shallow' => _('Shallow end'));
1520 * The exported map will be available via SN.msg() to JS code:
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'));
1526 * Exports a map of localized text strings to JavaScript code.
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.
1532 public function getScriptMessages()
1537 protected function showScriptVariables()
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));
1547 $this->inlineScript('SN.V = ' . json_encode($vars) . ';');
1553 * Show anonymous message.
1559 public function showAnonymousMessage()
1561 // needs to be defined by the class
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...
1570 * https://pear.php.net/bugs/bug.php?id=20291
1572 public function booleanintstring($key, $def = false)
1574 return $this->boolean($key, $def) ? '1' : '0';
1578 * Integer value of an argument
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)
1585 * @return integer integer value
1587 public function int($key, $defValue = null, $maxValue = null, $minValue = null)
1589 $arg = intval($this->arg($key));
1591 if (!is_numeric($this->arg($key)) || $arg != $this->arg($key)) {
1595 if (!is_null($maxValue)) {
1596 $arg = min($arg, $maxValue);
1599 if (!is_null($minValue)) {
1600 $arg = max($arg, $minValue);
1607 * Returns the current URL
1609 * @return string current URL
1611 public function selfUrl()
1613 list($action, $args) = $this->returnToArgs();
1614 return common_local_url($action, $args);
1618 * Generate pagination links
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
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.
1632 * Returns arguments sufficient for re-constructing URL
1634 * @return array two elements: action, other args
1636 public function returnToArgs()
1638 $action = $this->getActionName();
1639 $args = $this->args;
1640 unset($args['action']);
1641 if (common_config('site', 'fancy')) {
1644 if (array_key_exists('submit', $args)) {
1645 unset($args['submit']);
1647 foreach (array_keys($_COOKIE) as $cookie) {
1648 unset($args[$cookie]);
1650 return array($action, $args);
1654 * Generate a menu item
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
1664 public function menuItem($url, $text, $title = null, $is_selected = false, $id = null, $class = null)
1666 // Added @id to li for some control.
1667 // XXX: We might want to move this to htmloutputter.php
1670 if ($class !== null) {
1671 $classes[] = trim($class);
1674 $classes[] = 'current';
1677 if (!empty($classes)) {
1678 $lattrs['class'] = implode(' ', $classes);
1681 if (!is_null($id)) {
1682 $lattrs['id'] = $id;
1685 $this->elementStart('li', $lattrs);
1686 $attrs['href'] = $url;
1688 $attrs['title'] = $title;
1690 $this->element('a', $attrs, $text);
1691 $this->elementEnd('li');
1695 * Check the session token.
1697 * Checks that the current form has the correct session token,
1698 * and throw an exception if it does not.
1702 // XXX: Finding this type of check with the same message about 50 times.
1703 // Possible to refactor?
1705 public function pagination($have_before, $have_after, $page, $action, $args = null)
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'));
1713 $pargs = array('page' => $page - 1);
1714 $this->elementStart('li', array('class' => 'nav_prev'));
1717 array('href' => common_local_url($action, $args, $pargs),
1719 // TRANS: Pagination message to go to a page displaying information more in the
1720 // TRANS: present than the currently displayed information.
1723 $this->elementEnd('li');
1726 $pargs = array('page' => $page + 1);
1727 $this->elementStart('li', array('class' => 'nav_next'));
1730 array('href' => common_local_url($action, $args, $pargs),
1732 // TRANS: Pagination message to go to a page displaying information more in the
1733 // TRANS: past than the currently displayed information.
1736 $this->elementEnd('li');
1738 if ($have_before || $have_after) {
1739 $this->elementEnd('ul');
1744 public function checkSessionToken()
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.'));