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.php414
1 files changed, 262 insertions, 152 deletions
diff --git a/engine/lib/actions.php b/engine/lib/actions.php
index fef6004cc..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. The 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, rewrite a 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 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 regiser_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 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,99 +65,83 @@ 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($CONFIG->url, "", $forwarder);
+ $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 ((isadminloggedin()) || (!$CONFIG->actions[$action]['admin'])) {
- if ($CONFIG->actions[$action]['public'] || get_loggedin_userid()) {
-
- // Trigger action event
- // @todo This is only called before the primary action is called.
- $event_result = true;
- $event_result = 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(sprintf(elgg_echo('actionnotfound'), $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(sprintf(elgg_echo('actionundefined'), $action));
}
- forward($CONFIG->url . $forwarder);
+ $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/.
*
- * $file must be the full path of the file to register, or a path relative
+ * $filename must be the full path of the file to register, or a path relative
* to the core actions/ dir.
*
* Actions should be namedspaced for your plugin. Example:
* <code>
- * register_action('myplugin/save_settings', ...);
+ * elgg_register_action('myplugin/save_settings', ...);
* </code>
*
- * @tip Put action files under the actions/ directory of your plugin.
+ * @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>
* array(
* 'file' => '/location/to/file.php',
- * 'public' => BOOL If false, user must be logged in.
- * 'admin' => BOOL If true, user must be admin (implies plugin = false)
+ * 'access' => 'public', 'logged_in', or 'admin'
* )
* </code>
*
- * @param string $action The name of the action (eg "register", "account/settings/save")
- * @param boolean $public Can this action be accessed by people not logged into the system?
- * @param string $filename Optionally, the filename where this action is located
- * @param boolean $admin_only Whether this action is only available to admin users.
+ * @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: public, logged_in, admin.
+ * (default: logged_in)
*
* @see action()
* @see http://docs.elgg.org/Actions
*
- * @return true
+ * @return bool
*/
-function register_action($action, $public = false, $filename = "", $admin_only = false) {
+function elgg_register_action($action, $filename = "", $access = 'logged_in') {
global $CONFIG;
// plugins are encouraged to call actions with a trailing / to prevent 301
@@ -177,23 +163,57 @@ function register_action($action, $public = false, $filename = "", $admin_only =
$CONFIG->actions[$action] = array(
'file' => $filename,
- 'public' => $public,
- 'admin' => $admin_only
+ 'access' => $access,
);
return true;
}
/**
+ * 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']
@@ -201,6 +221,7 @@ function register_action($action, $public = false, $filename = "", $admin_only =
* @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) {
if (!$token) {
@@ -215,20 +236,17 @@ function validate_action_token($visibleerrors = TRUE, $token = NULL, $ts = NULL)
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;
- $now = time();
-
- // Validate time to ensure its not crazy
- if (($ts > $now - $hour) && ($ts < $now + $hour)) {
+ 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 = trigger_plugin_hook('action_gatekeeper:permissions:check', 'all', array(
+ $returnval = elgg_trigger_plugin_hook('action_gatekeeper:permissions:check', 'all', array(
'token' => $token,
'time' => $ts
), $returnval);
@@ -239,37 +257,78 @@ 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();
- exit;
+ forward(REFERER, 'csrf');
}
/**
@@ -289,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();
@@ -304,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;
}
@@ -340,45 +403,54 @@ 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
*
- * @return BOOL
- * @since 1.8
+ * @return bool
+ * @since 1.8.0
*/
-function elgg_action_exist($action) {
+function elgg_action_exists($action) {
global $CONFIG;
return (isset($CONFIG->actions[$action]) && file_exists($CONFIG->actions[$action]['file']));
}
/**
- * Initialize some ajaxy actions features
- */
-function actions_init()
-{
- register_action('security/refreshtoken', TRUE);
-
- elgg_view_register_simplecache('js/languages/en');
-
- register_plugin_hook('action', 'all', 'ajax_action_hook');
- register_plugin_hook('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';
+ return isset($_SERVER['HTTP_X_REQUESTED_WITH'])
+ && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest' ||
+ get_input('X-Requested-With') === 'XMLHttpRequest';
}
/**
* Catch calls to forward() in ajax request and force an exit.
- *
+ *
* Forces response is json of the following form:
* <pre>
* {
@@ -392,18 +464,29 @@ function elgg_is_xhr() {
* }
* </pre>
* where "system_messages" is all message registers at the point of forwarding
- *
+ *
* @param string $hook
- * @param string $type
+ * @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();
-
+
//Avoid double-encoding in case data is json
$json = json_decode($output);
if (isset($json)) {
@@ -411,17 +494,29 @@ function ajax_forward_hook($hook, $type, $reason, $params) {
} else {
$params['output'] = $output;
}
-
+
//Grab any system messages so we can inject them via ajax too
- $params['system_messages'] = system_messages(NULL, "");
-
- if (isset($params['system_messages']['errors'])) {
+ $system_messages = system_messages(NULL, "");
+
+ if (isset($system_messages['success'])) {
+ $params['system_messages']['success'] = $system_messages['success'];
+ }
+
+ 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;
}
@@ -429,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()) {
@@ -436,4 +533,17 @@ function ajax_action_hook() {
}
}
-register_elgg_event_handler('init', 'system', 'actions_init'); \ No newline at end of file
+/**
+ * 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');