gnu-social/lib/action/action.php

1738 lines
53 KiB
PHP
Raw Normal View History

<?php
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
/**
* Base class for all actions (~views)
*
* @category Action
* @package GNUsocial
* @author Evan Prodromou <evan@status.net>
* @author Sarven Capadisli <csarven@status.net>
* @copyright 2008 StatusNet, Inc.
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/
defined('GNUSOCIAL') || die();
/**
* Base class for all actions
*
* This is the base class for all actions in the package. An action is
* more or less a "view" in an MVC framework.
*
* Actions are responsible for extracting and validating parameters; using
* model classes to read and write to the database; and doing ouput.
*
* @category Output
* @copyright 2008 StatusNet, Inc.
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*
* @see HTMLOutputter
*/
class Action extends HTMLOutputter // lawsuit
{
// This should be protected/private in the future
public $args = [];
// Action properties, set per-class
protected $action = false;
protected $ajax = false;
protected $menus = true;
protected $needLogin = false;
protected $redirectAfterLogin = false;
protected $needPost = false; // implies canPost if true
protected $canPost = false; // can this action handle POST method?
// The currently scoped profile (normally Profile::current; from $this->auth_user for API)
protected $scoped = null;
// Related to front-end user representation
protected $format = null;
protected $error = null;
protected $msg = null;
/**
* Constructor
*
* Just wraps the HTMLOutputter constructor.
*
* @param string $output URI to output to, default = stdout
* @param boolean $indent Whether to indent output, default true
*
* @see XMLOutputter::__construct
* @see HTMLOutputter::__construct
*/
public function __construct($output = 'php://output', $indent = null)
{
parent::__construct($output, $indent);
}
public static function run(array $args = [], $output = 'php://output', $indent = null)
{
$class = get_called_class();
$action = new $class($output, $indent);
set_exception_handler(array($action, 'handleError'));
$action->execute($args);
return $action;
}
public function getInfo()
{
return $this->msg;
}
public function handleError($e)
{
if ($e instanceof ClientException) {
$this->clientError($e->getMessage(), $e->getCode());
} elseif ($e instanceof ServerException) {
$this->serverError($e->getMessage(), $e->getCode());
} else {
// If it wasn't specified more closely which kind of exception it was
$this->serverError($e->getMessage(), 500);
}
}
/**
* Client error
*
* @param string $msg error message to display
* @param integer $code http error code, 400 by default
* @param string $format error format (json, xml, text) for ApiAction
*
* @return void
* @throws ClientException always
*/
public function clientError($msg, $code = 400, $format = null)
{
// $format is currently only relevant for an ApiAction anyway
if ($format === null) {
$format = $this->format;
}
common_debug("User error '{$code}' on '{$this->action}': {$msg}", __FILE__);
if (!array_key_exists($code, ClientErrorAction::$status)) {
$code = 400;
}
$status_string = ClientErrorAction::$status[$code];
switch ($format) {
case 'xml':
header("HTTP/1.1 {$code} {$status_string}");
$this->initDocument('xml');
$this->elementStart('hash');
$this->element('error', null, $msg);
$this->element('request', null, $_SERVER['REQUEST_URI']);
$this->elementEnd('hash');
$this->endDocument('xml');
break;
case 'json':
if (!isset($this->callback)) {
header("HTTP/1.1 {$code} {$status_string}");
}
$this->initDocument('json');
$error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
print(json_encode($error_array));
$this->endDocument('json');
break;
case 'text':
header("HTTP/1.1 {$code} {$status_string}");
header('Content-Type: text/plain; charset=utf-8');
echo $msg;
break;
default:
common_log(LOG_ERR, 'Handled clientError (' . _ve($code) . ') but cannot output into desired format (' . _ve($this->format) . '): ' . _ve($msg));
$action = new ClientErrorAction($msg, $code);
$action->execute();
}
exit((int)$code);
}
public function execute(array $args = [])
{
// checkMirror stuff
if (common_config('db', 'mirror') && $this->isReadOnly($args)) {
if (is_array(common_config('db', 'mirror'))) {
// "load balancing", ha ha
$arr = common_config('db', 'mirror');
$k = array_rand($arr);
$mirror = $arr[$k];
} else {
$mirror = common_config('db', 'mirror');
}
// everyone else uses the mirror
common_config_set('db', 'database', $mirror);
}
if (Event::handle('StartActionExecute', array($this, &$args))) {
$prepared = $this->prepare($args);
if ($prepared) {
$this->handle();
} else {
common_debug('Prepare failed for Action.');
}
Cron plugin added and now default queue handler Generally the Cron plugin will run if there's still execution time for 1 second since starting the Action processing. If you want to change this (such as disabling, 0 seconds, or maybe running bigger chunks, for like 4 seconds) you can do this, where 'n' is time in seconds. addPlugin('Cron', array('secs_per_action', n)); Add 'rel_to_pageload'=>false to the array if you want to run the queue for a certain amount of seconds _despite_ maybe already having run that long in the previous parts of Action processing. Perhaps you want to run the cron script remotely, using a machine capable of background processing (or locally, to avoid running daemon processes), simply do an HTTP GET request to the route /main/cron of your GNU social. Setting secs_per_action to 0 in the plugin config will imply that you run all your queue handling by calling /main/cron (which runs as long as it can). /main/cron will output "0" if it has finished processing, "1" if it should be called again to complete processing (because it ran out of time due to PHP's max_execution_time INI setting). The Cron plugin also runs events as close to hourly, daily and weekly as you get, based on the opportunistic method of running whenever a user visits the site. This means of course that the cron events should be as fast as possible, not only to avoid delaying page load for users but also to minimize the risk of running into PHP's max_execution_time. One suggestion is to only use the events to add new queue items for later processing. These events are called CronHourly, CronDaily, CronWeekly - however there is no guarantee that all events will execute, so some kind of failsafe, transaction-ish method must be implemented in the future.
2013-11-19 13:13:33 +00:00
$this->flush();
Event::handle('EndActionExecute', array($this));
}
}
/**
* Return true if read only.
*
* MAY override
*
* @param array $args other arguments
*
* @return boolean is read only action?
*/
public function isReadOnly($args)
{
return false;
}
2009-01-22 04:22:32 +00:00
/**
* For initializing members of the class.
*
* @param array $args misc. arguments
2009-01-22 04:22:32 +00:00
*
* @return boolean true
* @throws ClientException
2009-01-22 04:22:32 +00:00
*/
protected function prepare(array $args = [])
{
if ($this->needPost && !$this->isPost()) {
// TRANS: Client error. POST is a HTTP command. It should not be translated.
$this->clientError(_('This method requires a POST.'), 405);
}
// needPost, of course, overrides canPost if true
if (!$this->canPost) {
$this->canPost = $this->needPost;
}
$this->args = common_copy_args($args);
2011-03-16 22:53:46 +00:00
// This could be set with get_called_action and then
// chop off 'Action' from the class name. In lower case.
$this->action = strtolower($this->trimmed('action'));
if ($this->ajax || $this->boolean('ajax')) {
// check with GNUsocial::isAjax()
GNUsocial::setAjax(true);
2011-03-16 22:53:46 +00:00
}
if ($this->needLogin) {
$this->checkLogin(); // if not logged in, this redirs/excepts
}
if ($this->redirectAfterLogin) {
common_set_returnto($this->selfUrl());
}
$this->updateScopedProfile();
return true;
}
/**
* Check if the current request is a POST
*
* @return boolean true if POST; otherwise false.
*/
public function isPost()
{
return ($_SERVER['REQUEST_METHOD'] == 'POST');
}
// Must be run _after_ prepare
/**
* Returns trimmed query argument or default value if not found
*
* @param string $key requested argument
* @param string $def default value to return if $key is not provided
*
* @return boolean is read only action?
*/
public function trimmed($key, $def = null)
{
$arg = $this->arg($key, $def);
return is_string($arg) ? trim($arg) : $arg;
}
2009-01-22 04:22:32 +00:00
/**
* Returns query argument or default value if not found
2009-01-22 04:22:32 +00:00
*
* @param string $key requested argument
* @param string $def default value to return if $key is not provided
*
* @return boolean is read only action?
2009-01-22 04:22:32 +00:00
*/
public function arg($key, $def = null)
{
if (array_key_exists($key, $this->args)) {
return $this->args[$key];
} else {
return $def;
2009-03-20 12:59:03 +00:00
}
}
/**
* Boolean understands english (yes, no, true, false)
*
* @param string $key query key we're interested in
* @param string $def default value
*
* @return boolean interprets yes/no strings as boolean
*/
public function boolean($key, $def = false)
{
$arg = strtolower($this->trimmed($key));
if (is_null($arg)) {
return $def;
} elseif (in_array($arg, array('true', 'yes', '1', 'on'))) {
return true;
} elseif (in_array($arg, array('false', 'no', '0'))) {
return false;
2014-07-06 11:55:18 +01:00
} else {
return $def;
2014-07-06 11:55:18 +01:00
}
}
/**
* If not logged in, take appropriate action (redir or exception)
*
* @param boolean $redir Redirect to login if not logged in
*
* @return boolean true if logged in (never returns if not)
* @throws ClientException
*/
public function checkLogin($redir = true)
{
if (common_logged_in()) {
return true;
}
if ($redir == true) {
common_set_returnto($_SERVER['REQUEST_URI']);
common_redirect(common_local_url('login'));
}
// TRANS: Error message displayed when trying to perform an action that requires a logged in user.
$this->clientError(_('Not logged in.'), 403);
}
public function updateScopedProfile()
{
$this->scoped = Profile::current();
return $this->scoped;
}
2009-01-22 04:22:32 +00:00
/**
* Handler method
2009-01-22 04:22:32 +00:00
*/
protected function handle()
{
header('Vary: Accept-Encoding,Cookie');
$lm = $this->lastModified();
$etag = $this->etag();
if ($etag) {
header('ETag: ' . $etag);
}
if ($lm) {
header('Last-Modified: ' . date(DATE_RFC1123, $lm));
if ($this->isCacheable()) {
header('Expires: ' . gmdate('D, d M Y H:i:s', 0) . ' GMT');
header("Cache-Control: private, must-revalidate, max-age=0");
header("Pragma:");
}
}
$checked = false;
if ($etag) {
$if_none_match = (array_key_exists('HTTP_IF_NONE_MATCH', $_SERVER)) ?
$_SERVER['HTTP_IF_NONE_MATCH'] : null;
if ($if_none_match) {
// If this check fails, ignore the if-modified-since below.
$checked = true;
if ($this->_hasEtag($etag, $if_none_match)) {
header('HTTP/1.1 304 Not Modified');
// Better way to do this?
exit(0);
}
}
}
if (!$checked && $lm && array_key_exists('HTTP_IF_MODIFIED_SINCE', $_SERVER)) {
$if_modified_since = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
$ims = strtotime($if_modified_since);
if ($lm <= $ims) {
header('HTTP/1.1 304 Not Modified');
// Better way to do this?
exit(0);
}
}
}
2009-01-22 04:22:32 +00:00
/**
* Return last modified, if applicable.
2009-01-22 04:22:32 +00:00
*
* MAY override
*
* @return string last modified http header
2009-01-22 04:22:32 +00:00
*/
public function lastModified()
{
// For comparison with If-Last-Modified
// If not applicable, return null
return null;
}
2009-01-22 04:22:32 +00:00
/**
* Return etag, if applicable.
2009-01-22 04:22:32 +00:00
*
* MAY override
2009-01-22 04:22:32 +00:00
*
* @return string etag http header
2009-01-22 04:22:32 +00:00
*/
public function etag()
{
return null;
}
/**
* Is this action cacheable?
*
* If the action returns a last-modified
*
* @return boolean is read only action?
*/
public function isCacheable()
{
return true;
}
/**
* Has etag? (private)
*
* @param string $etag etag http header
* @param string $if_none_match ifNoneMatch http header
*
* @return boolean
*/
public function _hasEtag($etag, $if_none_match)
{
$etags = explode(',', $if_none_match);
return in_array($etag, $etags) || in_array('*', $etags);
}
/**
* Server error
*
* @param string $msg error message to display
* @param integer $code http error code, 500 by default
*
* @param string $format
* @return void
*/
public function serverError($msg, $code = 500, $format = null)
{
if ($format === null) {
$format = $this->format;
}
common_debug("Server error '{$code}' on '{$this->action}': {$msg}", __FILE__);
if (!array_key_exists($code, ServerErrorAction::$status)) {
$code = 500;
}
$status_string = ServerErrorAction::$status[$code];
switch ($format) {
case 'xml':
header("HTTP/1.1 {$code} {$status_string}");
$this->initDocument('xml');
$this->elementStart('hash');
$this->element('error', null, $msg);
$this->element('request', null, $_SERVER['REQUEST_URI']);
$this->elementEnd('hash');
$this->endDocument('xml');
break;
case 'json':
if (!isset($this->callback)) {
header("HTTP/1.1 {$code} {$status_string}");
}
$this->initDocument('json');
$error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
print(json_encode($error_array));
$this->endDocument('json');
break;
default:
common_log(LOG_ERR, 'Handled serverError (' . _ve($code) . ') but cannot output into desired format (' . _ve($this->format) . '): ' . _ve($msg));
$action = new ServerErrorAction($msg, $code);
$action->execute();
}
exit((int)$code);
}
public function getScoped()
{
return ($this->scoped instanceof Profile) ? $this->scoped : null;
}
public function isAction(array $names)
{
foreach ($names as $class) {
// PHP is case insensitive, and we have stuff like ApiUpperCaseAction,
// but we at least make a point out of wanting to do stuff case-sensitive.
$class = ucfirst($class) . 'Action';
if ($this instanceof $class) {
return true;
}
}
return false;
}
/**
* Show page, a template method.
*
* @return void
* @throws ClientException
* @throws ReflectionException
* @throws ServerException
*/
public function showPage()
{
if (GNUsocial::isAjax()) {
self::showAjax();
return;
}
if (Event::handle('StartShowHTML', array($this))) {
$this->startHTML();
$this->flush();
Event::handle('EndShowHTML', array($this));
}
if (Event::handle('StartShowHead', array($this))) {
$this->showHead();
$this->flush();
Event::handle('EndShowHead', array($this));
}
if (Event::handle('StartShowBody', array($this))) {
$this->showBody();
Event::handle('EndShowBody', array($this));
}
if (Event::handle('StartEndHTML', array($this))) {
$this->endHTML();
Event::handle('EndEndHTML', array($this));
}
}
public function showAjax()
{
$this->startHTML('text/xml;charset=utf-8');
$this->elementStart('head');
// TRANS: Title for conversation page.
$this->element('title', null, $this->title());
$this->elementEnd('head');
$this->elementStart('body');
if ($this->getError()) {
$this->element('p', array('id' => 'error'), $this->getError());
} else {
$this->showContent();
}
$this->elementEnd('body');
$this->endHTML();
}
/**
* Returns the page title
*
* SHOULD overload
*
* @return string page title
*/
public function title()
{
// TRANS: Page title for a page without a title set.
return _('Untitled page');
}
public function getError()
{
return $this->error;
}
/**
* Show content.
*
* MUST overload (unless there's not a notice)
*
* @return void
*/
protected function showContent()
{
}
public function endHTML()
{
global $_startCpuTime;
if (isset($_startCpuTime)) {
$end_cpu_time = hrtime(true);
$diff = round(($end_cpu_time - $_startCpuTime) / 1000000);
$this->raw("<!-- ${diff}ms -->");
}
parent::endHTML();
}
/**
* Show head, a template method.
*
* @return void
*/
public function showHead()
{
// XXX: attributes (profile?)
$this->elementStart('head');
if (Event::handle('StartShowHeadElements', array($this))) {
if (Event::handle('StartShowHeadTitle', array($this))) {
$this->showTitle();
Event::handle('EndShowHeadTitle', array($this));
}
$this->showShortcutIcon();
$this->showStylesheets();
$this->showOpenSearch();
$this->showFeeds();
$this->showDescription();
$this->extraHead();
Event::handle('EndShowHeadElements', array($this));
}
$this->elementEnd('head');
}
/**
* Show title, a template method.
*
* @return void
*/
public function showTitle()
{
$this->element(
'title',
null,
// TRANS: Page title. %1$s is the title, %2$s is the site name.
sprintf(
_('%1$s - %2$s'),
$this->title(),
common_config('site', 'name')
)
);
}
/**
* Show themed shortcut icon
*
* @return void
*/
public function showShortcutIcon()
{
if (is_readable(INSTALLDIR . '/theme/' . common_config('site', 'theme') . '/favicon.ico')) {
$this->element('link', array('rel' => 'shortcut icon',
'href' => Theme::path('favicon.ico')));
} else {
// favicon.ico should be HTTPS if the rest of the page is
$this->element('link', array('rel' => 'shortcut icon',
'href' => common_path('favicon.ico', GNUsocial::isHTTPS())));
}
if (common_config('site', 'mobile')) {
if (is_readable(INSTALLDIR . '/theme/' . common_config('site', 'theme') . '/apple-touch-icon.png')) {
$this->element('link', array('rel' => 'apple-touch-icon',
'href' => Theme::path('apple-touch-icon.png')));
} else {
$this->element('link', array('rel' => 'apple-touch-icon',
'href' => common_path('apple-touch-icon.png')));
}
}
}
2009-01-22 04:22:32 +00:00
/**
* Show stylesheets
*
* @return void
2009-01-22 04:22:32 +00:00
*/
public function showStylesheets()
{
if (Event::handle('StartShowStyles', array($this))) {
// Use old name for StatusNet for compatibility on events
if (Event::handle('StartShowStylesheets', array($this))) {
$this->primaryCssLink(null, 'screen, projection, tv, print');
Event::handle('EndShowStylesheets', array($this));
}
$this->cssLink('js/extlib/jquery-ui/css/smoothness/jquery-ui.css');
2011-03-10 16:28:08 +00:00
if (Event::handle('StartShowUAStyles', array($this))) {
Event::handle('EndShowUAStyles', array($this));
}
Event::handle('EndShowStyles', array($this));
if (common_config('custom_css', 'enabled')) {
$css = common_config('custom_css', 'css');
if (Event::handle('StartShowCustomCss', array($this, &$css))) {
if (trim($css) != '') {
$this->style($css);
}
Event::handle('EndShowCustomCss', array($this));
}
}
}
}
public function primaryCssLink($mainTheme = null, $media = null)
{
$theme = new Theme($mainTheme);
// Some themes may have external stylesheets
foreach ($theme->getExternals() as $url) {
$this->cssLink($url, $mainTheme, $media);
}
// If the currently-selected theme has dependencies on other themes,
// we'll need to load their display.css files as well in order.
$baseThemes = $theme->getDeps();
foreach ($baseThemes as $baseTheme) {
$this->cssLink('css/display.css', $baseTheme, $media);
}
$this->cssLink('css/display.css', $mainTheme, $media);
// Additional styles for RTL languages
if (is_rtl(common_language())) {
if (file_exists(Theme::file('css/rtl.css'))) {
$this->cssLink('css/rtl.css', $mainTheme, $media);
}
}
}
2009-01-22 04:22:32 +00:00
/**
* Show OpenSearch headers
*
* @return void
2009-01-22 04:22:32 +00:00
*/
public function showOpenSearch()
{
2009-01-22 04:22:32 +00:00
$this->element('link', array('rel' => 'search',
'type' => 'application/opensearchdescription+xml',
'href' => common_local_url('opensearch', array('type' => 'people')),
'title' => common_config('site', 'name') . ' People Search'));
$this->element('link', array('rel' => 'search', 'type' => 'application/opensearchdescription+xml',
'href' => common_local_url('opensearch', array('type' => 'notice')),
'title' => common_config('site', 'name') . ' Notice Search'));
}
2009-01-22 04:22:32 +00:00
/**
* Show feed headers
*
* MAY overload
*
* @return void
2009-01-22 04:22:32 +00:00
*/
public function showFeeds()
{
foreach ($this->getFeeds() as $feed) {
$this->element('link', array('rel' => $feed->rel(),
'href' => $feed->url,
'type' => $feed->mimeType(),
'title' => $feed->title));
}
}
/**
* An array of feeds for this action.
*
* Returns an array of potential feeds for this action.
*
* @return array Feed object to show in head and links
*/
public function getFeeds()
{
return [];
}
2009-01-22 04:22:32 +00:00
/**
* Show description.
*
* SHOULD overload
*
* @return void
2009-01-22 04:22:32 +00:00
*/
public function showDescription()
{
// does nothing by default
}
2009-01-22 04:22:32 +00:00
/**
* Show extra stuff in <head>.
*
* MAY overload
*
* @return void
2009-01-22 04:22:32 +00:00
*/
public function extraHead()
{
// does nothing by default
}
2009-01-22 04:22:32 +00:00
/**
* Show body.
*
* Calls template methods
*
* @return void
* @throws ServerException
* @throws ReflectionException
2009-01-22 04:22:32 +00:00
*/
public function showBody()
{
$params = array('id' => $this->getActionName());
if ($this->scoped instanceof Profile) {
$params['class'] = 'user_in';
}
$this->elementStart('body', $params);
$this->elementStart('div', array('id' => 'wrap'));
if (Event::handle('StartShowHeader', array($this))) {
$this->showHeader();
2011-01-21 19:23:02 +00:00
$this->flush();
Event::handle('EndShowHeader', array($this));
}
$this->showCore();
2011-01-21 19:23:02 +00:00
$this->flush();
if (Event::handle('StartShowFooter', array($this))) {
$this->showFooter();
2011-01-21 19:23:02 +00:00
$this->flush();
Event::handle('EndShowFooter', array($this));
}
2009-01-16 15:25:22 +00:00
$this->elementEnd('div');
$this->showScripts();
$this->elementEnd('body');
}
public function getActionName()
{
return $this->action;
}
2009-01-22 04:22:32 +00:00
/**
* Show header of the page.
*
* Calls template methods
*
* @return void
* @throws ServerException
2009-01-22 04:22:32 +00:00
*/
public function showHeader()
{
$this->elementStart('div', array('id' => 'header'));
$this->showLogo();
$this->showPrimaryNav();
if (Event::handle('StartShowSiteNotice', array($this))) {
$this->showSiteNotice();
Event::handle('EndShowSiteNotice', array($this));
}
2011-03-03 03:09:11 +00:00
$this->elementEnd('div');
}
2009-01-22 04:22:32 +00:00
/**
* Show configured logo.
*
* @return void
* @throws ServerException
2009-01-22 04:22:32 +00:00
*/
public function showLogo()
{
$this->elementStart('address', array('id' => 'site_contact', 'class' => 'h-card'));
if (Event::handle('StartAddressData', array($this))) {
if (common_config('singleuser', 'enabled')) {
$user = User::singleUser();
$url = common_local_url(
'showstream',
array('nickname' => $user->nickname)
);
} elseif (common_logged_in()) {
$cur = common_current_user();
$url = common_local_url('all', array('nickname' => $cur->nickname));
} else {
$url = common_local_url('public');
}
$this->elementStart('a', array('class' => 'home bookmark',
'href' => $url));
if (GNUsocial::isHTTPS()) {
$logoUrl = common_config('site', 'ssllogo');
if (empty($logoUrl)) {
// if logo is an uploaded file, try to fall back to HTTPS file URL
$httpUrl = common_config('site', 'logo');
if (!empty($httpUrl)) {
try {
$f = File::getByUrl($httpUrl);
if (!empty($f->filename)) {
// this will handle the HTTPS case
$logoUrl = File::url($f->filename);
}
} catch (NoResultException $e) {
// no match
}
}
}
} else {
$logoUrl = common_config('site', 'logo');
}
if (empty($logoUrl) && file_exists(Theme::file('logo.png'))) {
// This should handle the HTTPS case internally
$logoUrl = Theme::path('logo.png');
}
if (!empty($logoUrl)) {
$this->element('img', array('class' => 'logo u-photo p-name',
'src' => $logoUrl,
'alt' => common_config('site', 'name')));
}
$this->elementEnd('a');
Event::handle('EndAddressData', array($this));
2009-01-14 06:34:22 +00:00
}
$this->elementEnd('address');
}
2009-01-22 04:22:32 +00:00
/**
* Show primary navigation.
*
* @return void
2009-01-22 04:22:32 +00:00
*/
public function showPrimaryNav()
{
2011-03-07 17:42:51 +00:00
$this->elementStart('div', array('id' => 'site_nav_global_primary'));
2011-08-25 22:59:27 +01:00
$user = common_current_user();
if (!empty($user) || !common_config('site', 'private')) {
$form = new SearchForm($this);
$form->show();
}
$pn = new PrimaryNav($this);
$pn->show();
2011-03-07 17:42:51 +00:00
$this->elementEnd('div');
}
2009-02-04 13:49:42 +00:00
2009-01-22 04:22:32 +00:00
/**
* Show site notice.
*
* @return void
2009-01-22 04:22:32 +00:00
*/
public function showSiteNotice()
2009-01-15 22:25:26 +00:00
{
// Revist. Should probably do an hAtom pattern here
$text = common_config('site', 'notice');
if ($text) {
$this->elementStart('div', array('id' => 'site_notice',
'class' => 'system_notice'));
$this->raw($text);
$this->elementEnd('div');
}
2009-01-15 22:25:26 +00:00
}
2009-01-22 04:22:32 +00:00
/**
* Show core.
*
* Shows local navigation, content block and aside.
*
* @return void
* @throws ReflectionException
2009-01-22 04:22:32 +00:00
*/
public function showCore()
{
2009-01-15 21:34:33 +00:00
$this->elementStart('div', array('id' => 'core'));
$this->elementStart('div', array('id' => 'aside_primary_wrapper'));
$this->elementStart('div', array('id' => 'content_wrapper'));
$this->elementStart('div', array('id' => 'site_nav_local_views_wrapper'));
2009-02-20 23:47:24 +00:00
if (Event::handle('StartShowLocalNavBlock', array($this))) {
$this->showLocalNavBlock();
2011-01-21 19:23:02 +00:00
$this->flush();
2009-02-20 23:47:24 +00:00
Event::handle('EndShowLocalNavBlock', array($this));
}
if (Event::handle('StartShowContentBlock', array($this))) {
$this->showContentBlock();
2011-01-21 19:23:02 +00:00
$this->flush();
Event::handle('EndShowContentBlock', array($this));
}
2009-10-01 13:27:02 +01:00
if (Event::handle('StartShowAside', array($this))) {
$this->showAside();
2011-01-21 19:23:02 +00:00
$this->flush();
2009-10-01 13:27:02 +01:00
Event::handle('EndShowAside', array($this));
}
$this->elementEnd('div');
$this->elementEnd('div');
$this->elementEnd('div');
$this->elementEnd('div');
}
2009-01-22 04:22:32 +00:00
/**
* Show local navigation block.
*
* @return void
2009-01-22 04:22:32 +00:00
*/
public function showLocalNavBlock()
{
2011-01-14 20:53:02 +00:00
// Need to have this ID for CSS; I'm too lazy to add it to
// all menus
$this->elementStart('div', array('id' => 'site_nav_local_views'));
2011-03-14 15:58:19 +00:00
// Cheat cheat cheat!
$this->showLocalNav();
2011-01-14 20:53:02 +00:00
$this->elementEnd('div');
}
2009-01-22 04:22:32 +00:00
/**
* Show local navigation.
*
* SHOULD overload
*
* @return void
2009-01-22 04:22:32 +00:00
*/
public function showLocalNav()
{
$nav = new DefaultLocalNav($this);
$nav->show();
}
2009-01-22 04:22:32 +00:00
/**
* Show content block.
*
* @return void
* @throws ReflectionException
2009-01-22 04:22:32 +00:00
*/
public function showContentBlock()
{
$this->elementStart('div', array('id' => 'content'));
2011-03-03 03:09:11 +00:00
if (common_logged_in()) {
if (Event::handle('StartShowNoticeForm', array($this))) {
$this->showNoticeForm();
Event::handle('EndShowNoticeForm', array($this));
}
}
if (Event::handle('StartShowPageTitle', array($this))) {
$this->showPageTitle();
Event::handle('EndShowPageTitle', array($this));
}
$this->showPageNoticeBlock();
2009-01-15 22:17:48 +00:00
$this->elementStart('div', array('id' => 'content_inner'));
// show the actual content (forms, lists, whatever)
$this->showContent();
$this->elementEnd('div');
2009-01-15 22:17:48 +00:00
$this->elementEnd('div');
}
/**
* Show notice form.
*
* MAY overload if no notice form needed... or direct message box????
*
* @return void
*/
public function showNoticeForm()
{
// TRANS: Tab on the notice form.
$tabs = array('status' => array('title' => _m('TAB', 'Status'),
'href' => common_local_url('newnotice')));
$this->elementStart('div', 'input_forms');
$this->element('label', array('for' => 'input_form_nav'), _m('TAB', 'Share your:'));
if (Event::handle('StartShowEntryForms', array(&$tabs))) {
$this->elementStart('ul', array('class' => 'nav',
'id' => 'input_form_nav'));
foreach ($tabs as $tag => $data) {
$tag = htmlspecialchars($tag);
$attrs = array('id' => 'input_form_nav_' . $tag,
'class' => 'input_form_nav_tab');
if ($tag == 'status') {
$attrs['class'] .= ' current';
}
$this->elementStart('li', $attrs);
$this->element(
'a',
array('onclick' => 'return SN.U.switchInputFormTab("' . $tag . '");',
'href' => $data['href']),
$data['title']
);
$this->elementEnd('li');
}
$this->elementEnd('ul');
foreach ($tabs as $tag => $data) {
$attrs = array('class' => 'input_form',
'id' => 'input_form_' . $tag);
if ($tag == 'status') {
$attrs['class'] .= ' current';
}
$this->elementStart('div', $attrs);
$form = null;
if (Event::handle('StartMakeEntryForm', array($tag, $this, &$form))) {
if ($tag == 'status') {
$options = $this->noticeFormOptions();
$form = new NoticeForm($this, $options);
}
Event::handle('EndMakeEntryForm', array($tag, $this, $form));
}
if (!empty($form)) {
$form->show();
}
$this->elementEnd('div');
}
}
$this->elementEnd('div');
}
public function noticeFormOptions()
{
return [];
}
2009-01-22 04:22:32 +00:00
/**
* Show page title.
*
* @return void
2009-01-22 04:22:32 +00:00
*/
public function showPageTitle()
2009-01-22 04:22:32 +00:00
{
$this->element('h1', null, $this->title());
}
2009-01-22 04:22:32 +00:00
/**
* Show page notice block.
*
* Only show the block if a subclassed action has overrided
* Action::showPageNotice(), or an event handler is registered for
* the StartShowPageNotice event, in which case we assume the
* 'page_notice' definition list is desired. This is to prevent
* empty 'page_notice' definition lists from being output everywhere.
*
* @return void
* @throws ReflectionException
2009-01-22 04:22:32 +00:00
*/
public function showPageNoticeBlock()
{
$rmethod = new ReflectionMethod($this, 'showPageNotice');
$dclass = $rmethod->getDeclaringClass()->getName();
if ($dclass != 'Action' || Event::hasHandler('StartShowPageNotice')) {
2011-01-14 20:36:06 +00:00
$this->elementStart('div', array('id' => 'page_notice',
'class' => 'system_notice'));
if (Event::handle('StartShowPageNotice', array($this))) {
$this->showPageNotice();
Event::handle('EndShowPageNotice', array($this));
}
2011-01-14 20:36:06 +00:00
$this->elementEnd('div');
2009-05-15 22:11:28 +01:00
}
2009-01-22 04:22:32 +00:00
}
2009-01-22 04:22:32 +00:00
/**
* Show page notice.
*
* SHOULD overload (unless there's not a notice)
*
* @return void
2009-01-22 04:22:32 +00:00
*/
public function showPageNotice()
{
}
2009-01-22 04:22:32 +00:00
/**
* Show Aside.
*
* @return void
* @throws ReflectionException
2009-01-22 04:22:32 +00:00
*/
public function showAside()
{
2009-01-15 22:00:48 +00:00
$this->elementStart('div', array('id' => 'aside_primary',
'class' => 'aside'));
2011-03-14 20:18:03 +00:00
$this->showProfileBlock();
2011-03-09 18:38:44 +00:00
if (Event::handle('StartShowObjectNavBlock', array($this))) {
$this->showObjectNavBlock();
Event::handle('EndShowObjectNavBlock', array($this));
}
if (Event::handle('StartShowSections', array($this))) {
$this->showSections();
Event::handle('EndShowSections', array($this));
}
if (Event::handle('StartShowExportData', array($this))) {
$this->showExportData();
Event::handle('EndShowExportData', array($this));
}
2009-01-15 22:00:48 +00:00
$this->elementEnd('div');
}
2009-01-22 04:22:32 +00:00
/**
* If there's a logged-in user, show a bit of login context
2009-01-22 04:22:32 +00:00
*
* @return void
* @throws Exception
2009-01-22 04:22:32 +00:00
*/
public function showProfileBlock()
{
if (common_logged_in()) {
$block = new DefaultProfileBlock($this);
$block->show();
}
}
/**
* Show menu for an object (group, profile)
*
* This block will only show if a subclass has overridden
* the showObjectNav() method.
*
* @return void
* @throws ReflectionException
*/
public function showObjectNavBlock()
{
$rmethod = new ReflectionMethod($this, 'showObjectNav');
$dclass = $rmethod->getDeclaringClass()->getName();
if ($dclass != 'Action') {
// Need to have this ID for CSS; I'm too lazy to add it to
// all menus
$this->elementStart('div', array('id' => 'site_nav_object',
'class' => 'section'));
$this->showObjectNav();
$this->elementEnd('div');
}
}
/**
* Show object navigation.
*
* If there are things to do with this object, show it here.
*
* @return void
*/
public function showObjectNav()
{
/* Nothing here. */
}
2009-01-22 04:22:32 +00:00
/**
* Show sections.
*
* SHOULD overload
*
* @return void
2009-01-22 04:22:32 +00:00
*/
public function showSections()
2009-01-22 04:22:32 +00:00
{
// for each section, show it
}
/**
* Show export data feeds.
*
* @return void
*/
public function showExportData()
{
$feeds = $this->getFeeds();
if (!empty($feeds)) {
$fl = new FeedList($this, $feeds);
$fl->show();
}
}
2009-01-22 04:22:32 +00:00
/**
* Show footer.
*
* @return void
2009-01-22 04:22:32 +00:00
*/
public function showFooter()
{
$this->elementStart('div', array('id' => 'footer'));
if (Event::handle('StartShowInsideFooter', array($this))) {
$this->showSecondaryNav();
$this->showLicenses();
Event::handle('EndShowInsideFooter', array($this));
}
$this->elementEnd('div');
}
2009-01-22 04:22:32 +00:00
/**
* Show secondary navigation.
*
* @return void
2009-01-22 04:22:32 +00:00
*/
public function showSecondaryNav()
{
$sn = new SecondaryNav($this);
$sn->show();
}
2009-01-22 04:22:32 +00:00
/**
* Show licenses.
*
* @return void
2009-01-22 04:22:32 +00:00
*/
public function showLicenses()
{
$this->showGNUsocialLicense();
$this->showContentLicense();
}
2009-01-22 04:22:32 +00:00
/**
* Show GNU social license.
2009-01-22 04:22:32 +00:00
*
* @return void
2009-01-22 04:22:32 +00:00
*/
public function showGNUsocialLicense()
{
if (common_config('site', 'broughtby')) {
// TRANS: First sentence of the GNU social site license. Used if 'broughtby' is set.
// TRANS: Text between [] is a link description, text between () is the link itself.
// TRANS: Make sure there is no whitespace between "]" and "(".
// TRANS: "%%site.broughtby%%" is the value of the variable site.broughtby
2010-08-09 23:40:04 +01:00
$instr = _('**%%site.name%%** is a social network, courtesy of [%%site.broughtby%%](%%site.broughtbyurl%%).');
} else {
// TRANS: First sentence of the GNU social site license. Used if 'broughtby' is not set.
2010-08-09 23:40:04 +01:00
$instr = _('**%%site.name%%** is a social network.');
}
$instr .= ' ';
// TRANS: Second sentence of the GNU social site license. Mentions the GNU social source code license.
// TRANS: Make sure there is no whitespace between "]" and "(".
// TRANS: [%1$s](%2$s) is a link description followed by the link itself
// TRANS: %3$s is the version of GNU social that is being used.
$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);
$output = common_markup_to_html($instr);
$this->raw($output);
// do it
}
2009-01-22 04:22:32 +00:00
/**
* Show content license.
*
* @return void
2009-01-22 04:22:32 +00:00
*/
public function showContentLicense()
{
if (Event::handle('StartShowContentLicense', array($this))) {
switch (common_config('license', 'type')) {
case 'private':
// TRANS: Content license displayed when license is set to 'private'.
// TRANS: %1$s is the site name.
$this->element('p', null, sprintf(
_('Content and data of %1$s are private and confidential.'),
common_config('site', 'name')
));
// fall through
// no break
case 'allrightsreserved':
if (common_config('license', 'owner')) {
// TRANS: Content license displayed when license is set to 'allrightsreserved'.
// TRANS: %1$s is the copyright owner.
$this->element('p', null, sprintf(
_('Content and data copyright by %1$s. All rights reserved.'),
common_config('license', 'owner')
));
} else {
// TRANS: Content license displayed when license is set to 'allrightsreserved' and no owner is set.
$this->element('p', null, _('Content and data copyright by contributors. All rights reserved.'));
}
break;
case 'cc': // fall through
default:
$this->elementStart('p');
$image = common_config('license', 'image');
$sslimage = common_config('license', 'sslimage');
if (GNUsocial::isHTTPS()) {
if (!empty($sslimage)) {
$url = $sslimage;
} elseif (preg_match('#^http://i.creativecommons.org/#', $image)) {
// CC support HTTPS on their images
$url = preg_replace('/^http/', 'https', $image, 1);
} else {
// Better to show mixed content than no content
$url = $image;
}
} else {
$url = $image;
}
$this->element('img', array('id' => 'license_cc',
'src' => $url,
'alt' => common_config('license', 'title'),
'width' => '80',
'height' => '15'));
$this->text(' ');
// TRANS: license message in footer.
// TRANS: %1$s is the site name, %2$s is a link to the license URL, with a licence name set in configuration.
$notice = _('All %1$s content and data are available under the %2$s license.');
$link = sprintf(
'<a class="license" rel="external license" href="%1$s">%2$s</a>',
htmlspecialchars(common_config('license', 'url')),
htmlspecialchars(common_config('license', 'title'))
);
$this->raw(@sprintf(
htmlspecialchars($notice),
htmlspecialchars(common_config('site', 'name')),
$link
));
$this->elementEnd('p');
break;
}
Event::handle('EndShowContentLicense', array($this));
}
}
2009-01-22 04:22:32 +00:00
/**
* Show javascript headers
*
* @return void
2009-01-22 04:22:32 +00:00
*/
public function showScripts()
{
if (Event::handle('StartShowScripts', array($this))) {
if (Event::handle('StartShowJQueryScripts', array($this))) {
$this->script('extlib/jquery.js');
$this->script('extlib/jquery.form.js');
$this->script('extlib/jquery-ui/jquery-ui.js');
$this->script('extlib/jquery.cookie.js');
Event::handle('EndShowJQueryScripts', array($this));
}
if (Event::handle('StartShowStatusNetScripts', array($this))) {
$this->script('util.js');
$this->script('xbImportNode.js');
// This route isn't available in single-user mode.
// Not sure why, but it causes errors here.
$this->inlineScript('var _peopletagAC = "' .
common_local_url('peopletagautocomplete') . '";');
$this->showScriptMessages();
$this->showScriptVariables();
// Anti-framing code to avoid clickjacking attacks in older browsers.
// This will show a blank page if the page is being framed, which is
// consistent with the behavior of the 'X-Frame-Options: SAMEORIGIN'
// header, which prevents framing in newer browser.
if (common_config('javascript', 'bustframes')) {
$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 = ""; }; }');
}
Event::handle('EndShowStatusNetScripts', array($this));
}
Event::handle('EndShowScripts', array($this));
}
}
2009-01-22 04:22:32 +00:00
/**
* Exports a map of localized text strings to JavaScript code.
2009-01-22 04:22:32 +00:00
*
* Plugins can add to what's exported by hooking the StartScriptMessages or EndScriptMessages
* events and appending to the array. Try to avoid adding strings that won't be used, as
* they'll be added to HTML output.
2009-01-22 04:22:32 +00:00
*/
public function showScriptMessages()
{
$messages = [];
if (Event::handle('StartScriptMessages', array($this, &$messages))) {
// Common messages needed for timeline views etc...
2010-08-31 05:54:33 +01:00
// TRANS: Localized tooltip for '...' expansion button on overlong remote messages.
$messages['showmore_tooltip'] = _m('TOOLTIP', 'Show more');
$messages['popup_close_button'] = _m('TOOLTIP', 'Close popup');
2010-08-31 05:54:33 +01:00
$messages = array_merge($messages, $this->getScriptMessages());
2010-08-31 05:54:33 +01:00
Event::handle('EndScriptMessages', array($this, &$messages));
2010-08-31 05:54:33 +01:00
}
if (!empty($messages)) {
$this->inlineScript('SN.messages=' . json_encode($messages));
}
2010-08-31 05:54:33 +01:00
return $messages;
}
2010-08-31 05:33:56 +01:00
/**
* If the action will need localizable text strings, export them here like so:
2010-08-31 05:33:56 +01:00
*
* return array('pool_deepend' => _('Deep end'),
* 'pool_shallow' => _('Shallow end'));
2010-08-31 05:33:56 +01:00
*
* The exported map will be available via SN.msg() to JS code:
2010-08-31 05:33:56 +01:00
*
* $('#pool').html('<div class="deepend"></div><div class="shallow"></div>');
* $('#pool .deepend').text(SN.msg('pool_deepend'));
* $('#pool .shallow').text(SN.msg('pool_shallow'));
*
* Exports a map of localized text strings to JavaScript code.
*
* Plugins can add to what's exported on any action by hooking the StartScriptMessages or
* EndScriptMessages events and appending to the array. Try to avoid adding strings that won't
* be used, as they'll be added to HTML output.
2010-08-31 05:33:56 +01:00
*/
public function getScriptMessages()
2010-08-31 05:33:56 +01:00
{
return [];
}
protected function showScriptVariables()
{
$vars = [];
if (Event::handle('StartScriptVariables', array($this, &$vars))) {
$vars['urlNewNotice'] = common_local_url('newnotice');
$vars['xhrTimeout'] = ini_get('max_execution_time') * 1000; // milliseconds
Event::handle('EndScriptVariables', array($this, &$vars));
}
$this->inlineScript('SN.V = ' . json_encode($vars) . ';');
return $vars;
}
2009-01-22 04:22:32 +00:00
/**
* Show anonymous message.
2009-01-22 04:22:32 +00:00
*
* SHOULD overload
2009-01-22 04:22:32 +00:00
*
* @return void
2009-01-22 04:22:32 +00:00
*/
public function showAnonymousMessage()
{
// needs to be defined by the class
}
2009-12-12 20:00:09 +00:00
/**
* Integer value of an argument
*
* @param string $key query key we're interested in
2009-12-12 20:00:09 +00:00
* @param string $defValue optional default value (default null)
* @param string $maxValue optional max value (default null)
* @param string $minValue optional min value (default null)
*
* @return integer integer value
*/
public function int($key, $defValue = null, $maxValue = null, $minValue = null)
2009-12-12 20:00:09 +00:00
{
$arg = intval($this->arg($key));
2009-12-12 20:00:09 +00:00
if (!is_numeric($this->arg($key)) || $arg != $this->arg($key)) {
2009-12-12 20:00:09 +00:00
return $defValue;
}
if (!is_null($maxValue)) {
$arg = min($arg, $maxValue);
}
if (!is_null($minValue)) {
$arg = max($arg, $minValue);
}
return $arg;
}
2009-01-22 04:22:32 +00:00
/**
* Returns the current URL
*
* @return string current URL
*/
public function selfUrl()
{
list($action, $args) = $this->returnToArgs();
return common_local_url($action, $args);
}
/**
* Generate pagination links
*
* @param boolean $have_before is there something before?
* @param boolean $have_after is there something after?
* @param integer $page current page
* @param string $action current action
* @param array $args rest of query arguments
*
* @return void
*/
// XXX: The messages in this pagination method only tailor to navigating
// notices. In other lists, "Previous"/"Next" type navigation is
// desirable, but not available.
/**
* Returns arguments sufficient for re-constructing URL
*
* @return array two elements: action, other args
*/
public function returnToArgs()
{
$action = $this->getActionName();
$args = $this->args;
unset($args['action']);
if (common_config('site', 'fancy')) {
unset($args['p']);
}
2009-04-07 16:40:13 +01:00
if (array_key_exists('submit', $args)) {
unset($args['submit']);
}
foreach (array_keys($_COOKIE) as $cookie) {
unset($args[$cookie]);
}
return array($action, $args);
}
2009-01-22 04:22:32 +00:00
/**
* Generate a menu item
*
* @param string $url menu URL
* @param string $text menu name
* @param string $title title attribute, null by default
2009-01-22 04:22:32 +00:00
* @param boolean $is_selected current menu item, false by default
* @param string $id element id, null by default
2009-01-22 04:22:32 +00:00
*
* @return void
2009-01-22 04:22:32 +00:00
*/
public function menuItem($url, $text, $title = null, $is_selected = false, $id = null, $class = null)
{
2009-01-22 04:22:32 +00:00
// Added @id to li for some control.
// XXX: We might want to move this to htmloutputter.php
$lattrs = [];
$classes = [];
if ($class !== null) {
$classes[] = trim($class);
}
if ($is_selected) {
$classes[] = 'current';
}
2011-07-06 16:18:53 +01:00
if (!empty($classes)) {
$lattrs['class'] = implode(' ', $classes);
}
2011-07-06 16:18:53 +01:00
if (!is_null($id)) {
$lattrs['id'] = $id;
}
$this->elementStart('li', $lattrs);
$attrs['href'] = $url;
if ($title) {
$attrs['title'] = $title;
}
$this->element('a', $attrs, $text);
$this->elementEnd('li');
}
2009-01-22 04:22:32 +00:00
/**
* Check the session token.
2009-01-22 04:22:32 +00:00
*
* Checks that the current form has the correct session token,
* and throw an exception if it does not.
2009-01-22 04:22:32 +00:00
*
* @return void
2009-01-22 04:22:32 +00:00
*/
// XXX: Finding this type of check with the same message about 50 times.
// Possible to refactor?
public function pagination($have_before, $have_after, $page, $action, $args = null)
{
2009-01-22 04:22:32 +00:00
// Does a little before-after block for next/prev page
if ($have_before || $have_after) {
2011-01-14 20:53:02 +00:00
$this->elementStart('ul', array('class' => 'nav',
'id' => 'pagination'));
}
if ($have_before) {
$pargs = array('page' => $page - 1);
$this->elementStart('li', array('class' => 'nav_prev'));
$this->element(
'a',
array('href' => common_local_url($action, $args, $pargs),
'rel' => 'prev'),
// TRANS: Pagination message to go to a page displaying information more in the
// TRANS: present than the currently displayed information.
_('After')
);
$this->elementEnd('li');
}
if ($have_after) {
$pargs = array('page' => $page + 1);
$this->elementStart('li', array('class' => 'nav_next'));
$this->element(
'a',
array('href' => common_local_url($action, $args, $pargs),
'rel' => 'next'),
// TRANS: Pagination message to go to a page displaying information more in the
// TRANS: past than the currently displayed information.
_('Before')
);
$this->elementEnd('li');
}
if ($have_before || $have_after) {
$this->elementEnd('ul');
}
}
public function checkSessionToken()
{
// CSRF protection
$token = $this->trimmed('token');
if (empty($token) || $token != common_session_token()) {
// TRANS: Client error text when there is a problem with the session token.
$this->clientError(_('There was a problem with your session token.'));
}
}
}