aboutsummaryrefslogtreecommitdiff
path: root/engine/lib/actions.php
diff options
context:
space:
mode:
Diffstat (limited to 'engine/lib/actions.php')
-rw-r--r--engine/lib/actions.php368
1 files changed, 236 insertions, 132 deletions
diff --git a/engine/lib/actions.php b/engine/lib/actions.php
index 407b99f87..8047914ac 100644
--- a/engine/lib/actions.php
+++ b/engine/lib/actions.php
@@ -2,21 +2,23 @@
/**
* Elgg Actions
*
- * Actions are the primary controllers (The C in MVC) in Elgg. They are
- * registered by {@link register_elgg_action()} and are called either by URL
- * http://elggsite.org/action/action_name or {@link action($action_name}. For
- * URLs, a rewrite rule in .htaccess passes the action name to
- * engine/handlers/action_handler.php, which dispatches the action.
+ * Actions are one of the primary controllers (The C in MVC) in Elgg. They are
+ * registered by {@link register_elgg_action()} and are called by URL
+ * http://elggsite.org/action/action_name. For URLs, a rewrite rule in
+ * .htaccess passes the action name to engine/handlers/action_handler.php,
+ * which dispatches the request for the action.
*
- * An action name should be registered to exactly one file in the system, usually under
- * the actions/ directory.
+ * An action name must be registered to a file in the system. Core actions are
+ * found in /actions/ and plugin actions are usually under /mod/<plugin>/actions/.
+ * It is recommended that actions be namespaced to avoid collisions.
*
* All actions require security tokens. Using the {@elgg_view input/form} view
- * will automatically add tokens as hidden inputs. To manually add hidden inputs,
- * use the {@elgg_view input/securitytoken} view.
+ * will automatically add tokens as hidden inputs as will the elgg_view_form()
+ * function. To manually add hidden inputs, use the {@elgg_view input/securitytoken} view.
*
* To include security tokens for actions called via GET, use
- * {@link elgg_add_security_tokens_to_url()}.
+ * {@link elgg_add_security_tokens_to_url()} or specify is_action as true when
+ * using {@lgg_view output/url}.
*
* Action tokens can be manually generated by using {@link generate_action_token()}.
*
@@ -31,30 +33,30 @@
*/
/**
-* Perform an action.
-*
-* This function executes the action with name $action as
-* registered by {@link elgg_register_action()}.
-*
-* The plugin hook action, $action_name will be emitted before
-* the action is executed. If a handler returns false, it will
-* prevent the action from being called.
-*
-* @note If an action isn't registered in the system or is registered
-* to an unavailable file the user will be forwarded to the site front
-* page and an error will be emitted via {@link register_error()}.
-*
-* @warning All actions require {@link http://docs.elgg.org/Actions/Tokens Action Tokens}.
-* @warning Most plugin shouldn't call this manually.
-*
-* @param string $action The requested action
-* @param string $forwarder Optionally, the location to forward to
-*
-* @link http://docs.elgg.org/Actions
-* @see elgg_register_action()
-*
-* @return void
-*/
+ * Perform an action.
+ *
+ * This function executes the action with name $action as registered
+ * by {@link elgg_register_action()}.
+ *
+ * The plugin hook 'action', $action_name will be triggered before the action
+ * is executed. If a handler returns false, it will prevent the action script
+ * from being called.
+ *
+ * @note If an action isn't registered in the system or is registered
+ * to an unavailable file the user will be forwarded to the site front
+ * page and an error will be emitted via {@link register_error()}.
+ *
+ * @warning All actions require {@link http://docs.elgg.org/Actions/Tokens Action Tokens}.
+ *
+ * @param string $action The requested action
+ * @param string $forwarder Optionally, the location to forward to
+ *
+ * @link http://docs.elgg.org/Actions
+ * @see elgg_register_action()
+ *
+ * @return void
+ * @access private
+ */
function action($action, $forwarder = "") {
global $CONFIG;
@@ -63,65 +65,50 @@ function action($action, $forwarder = "") {
// @todo REMOVE THESE ONCE #1509 IS IN PLACE.
// Allow users to disable plugins without a token in order to
// remove plugins that are incompatible.
- // Login and logout are for convenience.
+ // Logout for convenience.
// file/download (see #2010)
$exceptions = array(
'admin/plugins/disable',
'logout',
- 'login',
'file/download',
);
if (!in_array($action, $exceptions)) {
- // All actions require a token.
- action_gatekeeper();
+ action_gatekeeper($action);
}
$forwarder = str_replace(elgg_get_site_url(), "", $forwarder);
$forwarder = str_replace("http://", "", $forwarder);
$forwarder = str_replace("@", "", $forwarder);
-
if (substr($forwarder, 0, 1) == "/") {
$forwarder = substr($forwarder, 1);
}
- if (isset($CONFIG->actions[$action])) {
- if (elgg_is_admin_logged_in() || ($CONFIG->actions[$action]['access'] !== 'admin')) {
- if (elgg_is_logged_in() || ($CONFIG->actions[$action]['access'] === 'public')) {
-
- // Trigger action event
- // @todo This is only called before the primary action is called.
- $event_result = true;
- $event_result = elgg_trigger_plugin_hook('action', $action, null, $event_result);
-
- // Include action
- // Event_result being false doesn't produce an error
- // since i assume this will be handled in the hook itself.
- // @todo make this better!
- if ($event_result) {
- if (!include($CONFIG->actions[$action]['file'])) {
- register_error(elgg_echo('actionnotfound', array($action)));
- }
- }
- } else {
- register_error(elgg_echo('actionloggedout'));
+ if (!isset($CONFIG->actions[$action])) {
+ register_error(elgg_echo('actionundefined', array($action)));
+ } elseif (!elgg_is_admin_logged_in() && ($CONFIG->actions[$action]['access'] === 'admin')) {
+ register_error(elgg_echo('actionunauthorized'));
+ } elseif (!elgg_is_logged_in() && ($CONFIG->actions[$action]['access'] !== 'public')) {
+ register_error(elgg_echo('actionloggedout'));
+ } else {
+ // Returning falsy doesn't produce an error
+ // We assume this will be handled in the hook itself.
+ if (elgg_trigger_plugin_hook('action', $action, null, true)) {
+ if (!include($CONFIG->actions[$action]['file'])) {
+ register_error(elgg_echo('actionnotfound', array($action)));
}
- } else {
- register_error(elgg_echo('actionunauthorized'));
}
- } else {
- register_error(elgg_echo('actionundefined', array($action)));
}
+ $forwarder = empty($forwarder) ? REFERER : $forwarder;
forward($forwarder);
}
/**
* Registers an action.
*
- * Actions are registered to a single file in the system and are executed
- * either by the URL http://elggsite.org/action/action_name or by calling
- * {@link action()}.
+ * Actions are registered to a script in the system and are executed
+ * either by the URL http://elggsite.org/action/action_name/.
*
* $filename must be the full path of the file to register, or a path relative
* to the core actions/ dir.
@@ -133,8 +120,7 @@ function action($action, $forwarder = "") {
*
* @tip Put action files under the actions/<plugin_name> directory of your plugin.
*
- * @tip You don't need to include engine/start.php, call {@link gatekeeper()},
- * or call {@link admin_gatekeeper()}.
+ * @tip You don't need to include engine/start.php in your action files.
*
* @internal Actions are saved in $CONFIG->actions as an array in the form:
* <code>
@@ -147,12 +133,13 @@ function action($action, $forwarder = "") {
* @param string $action The name of the action (eg "register", "account/settings/save")
* @param string $filename Optionally, the filename where this action is located. If not specified,
* will assume the action is in elgg/actions/<action>.php
- * @param string $access Who is allowed to execute this action: admin, public, or logged_in.
+ * @param string $access Who is allowed to execute this action: public, logged_in, admin.
+ * (default: logged_in)
*
* @see action()
* @see http://docs.elgg.org/Actions
*
- * @return true
+ * @return bool
*/
function elgg_register_action($action, $filename = "", $access = 'logged_in') {
global $CONFIG;
@@ -182,16 +169,51 @@ function elgg_register_action($action, $filename = "", $access = 'logged_in') {
}
/**
+ * Unregisters an action
+ *
+ * @param string $action Action name
+ * @return bool
+ * @since 1.8.1
+ */
+function elgg_unregister_action($action) {
+ global $CONFIG;
+
+ if (isset($CONFIG->actions[$action])) {
+ unset($CONFIG->actions[$action]);
+ return true;
+ } else {
+ return false;
+ }
+}
+
+/**
+ * Is the token timestamp within acceptable range?
+ *
+ * @param int $ts timestamp from the CSRF token
+ *
+ * @return bool
+ */
+function _elgg_validate_token_timestamp($ts) {
+ $action_token_timeout = elgg_get_config('action_token_timeout');
+ // default is 2 hours
+ $timeout = ($action_token_timeout !== null) ? $action_token_timeout : 2;
+
+ $hour = 60 * 60;
+ $timeout = $timeout * $hour;
+ $now = time();
+
+ // Validate time to ensure its not crazy
+ return ($timeout == 0 || ($ts > $now - $timeout) && ($ts < $now + $timeout));
+}
+
+/**
* Validate an action token.
*
- * Calls to actions will automatically validate tokens.
- * If tokens are not present or invalid, the action will be
- * denied and the user will be redirected to the front page.
+ * Calls to actions will automatically validate tokens. If tokens are not
+ * present or invalid, the action will be denied and the user will be redirected.
*
* Plugin authors should never have to manually validate action tokens.
*
- * @access private
- *
* @param bool $visibleerrors Emit {@link register_error()} errors on failure?
* @param mixed $token The token to test against. Default: $_REQUEST['__elgg_token']
* @param mixed $ts The time stamp to test against. Default: $_REQUEST['__elgg_ts']
@@ -199,10 +221,9 @@ function elgg_register_action($action, $filename = "", $access = 'logged_in') {
* @return bool
* @see generate_action_token()
* @link http://docs.elgg.org/Actions/Tokens
+ * @access private
*/
function validate_action_token($visibleerrors = TRUE, $token = NULL, $ts = NULL) {
- global $CONFIG;
-
if (!$token) {
$token = get_input('__elgg_token');
}
@@ -211,29 +232,18 @@ function validate_action_token($visibleerrors = TRUE, $token = NULL, $ts = NULL)
$ts = get_input('__elgg_ts');
}
- if (!isset($CONFIG->action_token_timeout)) {
- // default to 2 hours
- $timeout = 2;
- } else {
- $timeout = $CONFIG->action_token_timeout;
- }
-
$session_id = session_id();
if (($token) && ($ts) && ($session_id)) {
// generate token, check with input and forward if invalid
- $generated_token = generate_action_token($ts);
+ $required_token = generate_action_token($ts);
// Validate token
- if ($token == $generated_token) {
- $hour = 60 * 60;
- $timeout = $timeout * $hour;
- $now = time();
-
- // Validate time to ensure its not crazy
- if ($timeout == 0 || ($ts > $now - $timeout) && ($ts < $now + $timeout)) {
+ if ($token == $required_token) {
+
+ if (_elgg_validate_token_timestamp($ts)) {
// We have already got this far, so unless anything
- // else says something to the contry we assume we're ok
+ // else says something to the contrary we assume we're ok
$returnval = true;
$returnval = elgg_trigger_plugin_hook('action_gatekeeper:permissions:check', 'all', array(
@@ -247,33 +257,75 @@ function validate_action_token($visibleerrors = TRUE, $token = NULL, $ts = NULL)
register_error(elgg_echo('actiongatekeeper:pluginprevents'));
}
} else if ($visibleerrors) {
- register_error(elgg_echo('actiongatekeeper:timeerror'));
+ // this is necessary because of #5133
+ if (elgg_is_xhr()) {
+ register_error(elgg_echo('js:security:token_refresh_failed', array(elgg_get_site_url())));
+ } else {
+ register_error(elgg_echo('actiongatekeeper:timeerror'));
+ }
}
} else if ($visibleerrors) {
- register_error(elgg_echo('actiongatekeeper:tokeninvalid'));
+ // this is necessary because of #5133
+ if (elgg_is_xhr()) {
+ register_error(elgg_echo('js:security:token_refresh_failed', array(elgg_get_site_url())));
+ } else {
+ register_error(elgg_echo('actiongatekeeper:tokeninvalid'));
+ }
+ }
+ } else {
+ if (! empty($_SERVER['CONTENT_LENGTH']) && empty($_POST)) {
+ // The size of $_POST or uploaded file has exceed the size limit
+ $error_msg = elgg_trigger_plugin_hook('action_gatekeeper:upload_exceeded_msg', 'all', array(
+ 'post_size' => $_SERVER['CONTENT_LENGTH'],
+ 'visible_errors' => $visibleerrors,
+ ), elgg_echo('actiongatekeeper:uploadexceeded'));
+ } else {
+ $error_msg = elgg_echo('actiongatekeeper:missingfields');
+ }
+ if ($visibleerrors) {
+ register_error($error_msg);
}
- } else if ($visibleerrors) {
- register_error(elgg_echo('actiongatekeeper:missingfields'));
}
return FALSE;
}
/**
-* Validates the presence of action tokens.
-*
-* This function is called for all actions. If action tokens are missing,
-* the user will be forwarded to the site front page and an error emitted.
-*
-* This function verifies form input for security features (like a generated token), and forwards
-* the page if they are invalid.
-*
-* @access private
-* @return mixed True if valid, or redirects to front page and exists.
-*/
-function action_gatekeeper() {
- if (validate_action_token()) {
- return TRUE;
+ * Validates the presence of action tokens.
+ *
+ * This function is called for all actions. If action tokens are missing,
+ * the user will be forwarded to the site front page and an error emitted.
+ *
+ * This function verifies form input for security features (like a generated token),
+ * and forwards if they are invalid.
+ *
+ * @param string $action The action being performed
+ *
+ * @return mixed True if valid or redirects.
+ * @access private
+ */
+function action_gatekeeper($action) {
+ if ($action === 'login') {
+ if (validate_action_token(false)) {
+ return true;
+ }
+
+ $token = get_input('__elgg_token');
+ $ts = (int)get_input('__elgg_ts');
+ if ($token && _elgg_validate_token_timestamp($ts)) {
+ // The tokens are present and the time looks valid: this is probably a mismatch due to the
+ // login form being on a different domain.
+ register_error(elgg_echo('actiongatekeeper:crosssitelogin'));
+
+
+ forward('login', 'csrf');
+ }
+
+ // let the validator send an appropriate msg
+ validate_action_token();
+
+ } elseif (validate_action_token()) {
+ return true;
}
forward(REFERER, 'csrf');
@@ -296,6 +348,7 @@ function action_gatekeeper() {
* @example actions/manual_tokens.php
*
* @return string|false
+ * @access private
*/
function generate_action_token($timestamp) {
$site_secret = get_site_secret();
@@ -311,16 +364,19 @@ function generate_action_token($timestamp) {
}
/**
- * Initialise the site secret hash.
+ * Initialise the site secret (32 bytes: "z" to indicate format + 186-bit key in Base64 URL).
*
* Used during installation and saves as a datalist.
*
+ * Note: Old secrets were hex encoded.
+ *
* @return mixed The site secret hash or false
* @access private
* @todo Move to better file.
*/
function init_site_secret() {
- $secret = md5(rand() . microtime());
+ $secret = 'z' . ElggCrypto::getRandomString(31);
+
if (datalist_set('__site_secret__', $secret)) {
return $secret;
}
@@ -347,7 +403,27 @@ function get_site_secret() {
}
/**
- * Check if an action is registered and its file exists.
+ * Get the strength of the site secret
+ *
+ * @return string "strong", "moderate", or "weak"
+ * @access private
+ */
+function _elgg_get_site_secret_strength() {
+ $secret = get_site_secret();
+ if ($secret[0] !== 'z') {
+ $rand_max = getrandmax();
+ if ($rand_max < pow(2, 16)) {
+ return 'weak';
+ }
+ if ($rand_max < pow(2, 32)) {
+ return 'moderate';
+ }
+ }
+ return 'strong';
+}
+
+/**
+ * Check if an action is registered and its script exists.
*
* @param string $action Action name
*
@@ -361,25 +437,15 @@ function elgg_action_exists($action) {
}
/**
- * Initialize some ajaxy actions features
- */
-function actions_init() {
- elgg_register_action('security/refreshtoken', '', 'public');
-
- elgg_register_simplecache_view('js/languages/en');
-
- elgg_register_plugin_hook_handler('action', 'all', 'ajax_action_hook');
- elgg_register_plugin_hook_handler('forward', 'all', 'ajax_forward_hook');
-}
-
-/**
* Checks whether the request was requested via ajax
*
* @return bool whether page was requested via ajax
+ * @since 1.8.0
*/
function elgg_is_xhr() {
return isset($_SERVER['HTTP_X_REQUESTED_WITH'])
- && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest';
+ && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest' ||
+ get_input('X-Requested-With') === 'XMLHttpRequest';
}
/**
@@ -403,10 +469,21 @@ function elgg_is_xhr() {
* @param string $type
* @param string $reason
* @param array $params
- *
+ * @return void
+ * @access private
*/
function ajax_forward_hook($hook, $type, $reason, $params) {
if (elgg_is_xhr()) {
+ // always pass the full structure to avoid boilerplate JS code.
+ $params = array(
+ 'output' => '',
+ 'status' => 0,
+ 'system_messages' => array(
+ 'error' => array(),
+ 'success' => array()
+ )
+ );
+
//grab any data echo'd in the action
$output = ob_get_clean();
@@ -419,15 +496,27 @@ function ajax_forward_hook($hook, $type, $reason, $params) {
}
//Grab any system messages so we can inject them via ajax too
- $params['system_messages'] = system_messages(NULL, "");
+ $system_messages = system_messages(NULL, "");
+
+ if (isset($system_messages['success'])) {
+ $params['system_messages']['success'] = $system_messages['success'];
+ }
- if (isset($params['system_messages']['error'])) {
+ if (isset($system_messages['error'])) {
+ $params['system_messages']['error'] = $system_messages['error'];
$params['status'] = -1;
+ }
+
+ // Check the requester can accept JSON responses, if not fall back to
+ // returning JSON in a plain-text response. Some libraries request
+ // JSON in an invisible iframe which they then read from the iframe,
+ // however some browsers will not accept the JSON MIME type.
+ if (stripos($_SERVER['HTTP_ACCEPT'], 'application/json') === FALSE) {
+ header("Content-type: text/plain");
} else {
- $params['status'] = 0;
+ header("Content-type: application/json");
}
- header("Content-type: application/json");
echo json_encode($params);
exit;
}
@@ -435,6 +524,8 @@ function ajax_forward_hook($hook, $type, $reason, $params) {
/**
* Buffer all output echo'd directly in the action for inclusion in the returned JSON.
+ * @return void
+ * @access private
*/
function ajax_action_hook() {
if (elgg_is_xhr()) {
@@ -442,4 +533,17 @@ function ajax_action_hook() {
}
}
+/**
+ * Initialize some ajaxy actions features
+ * @access private
+ */
+function actions_init() {
+ elgg_register_action('security/refreshtoken', '', 'public');
+
+ elgg_register_simplecache_view('js/languages/en');
+
+ elgg_register_plugin_hook_handler('action', 'all', 'ajax_action_hook');
+ elgg_register_plugin_hook_handler('forward', 'all', 'ajax_forward_hook');
+}
+
elgg_register_event_handler('init', 'system', 'actions_init');