Elgg  Version 2.2
 All Classes Namespaces Files Functions Variables Pages
ActionsService.php
Go to the documentation of this file.
1 <?php
2 namespace Elgg;
4 
17 
27  private $actions = array();
28 
33  private $currentAction = null;
34 
38  private static $access_levels = ['public', 'logged_in', 'admin'];
39 
44  public function execute($action, $forwarder = "") {
45  $action = rtrim($action, '/');
46  $this->currentAction = $action;
47 
48  // @todo REMOVE THESE ONCE #1509 IS IN PLACE.
49  // Allow users to disable plugins without a token in order to
50  // remove plugins that are incompatible.
51  // Login and logout are for convenience.
52  // file/download (see #2010)
53  $exceptions = array(
54  'admin/plugins/disable',
55  'logout',
56  'file/download',
57  );
58 
59  if (!in_array($action, $exceptions)) {
60  // All actions require a token.
61  $this->gatekeeper($action);
62  }
63 
64  $forwarder = str_replace(_elgg_services()->config->getSiteUrl(), "", $forwarder);
65  $forwarder = str_replace("http://", "", $forwarder);
66  $forwarder = str_replace("@", "", $forwarder);
67  if (substr($forwarder, 0, 1) == "/") {
68  $forwarder = substr($forwarder, 1);
69  }
70 
78  $forward = function ($error_key = '') use ($action, $forwarder) {
79  if ($error_key) {
80  $msg = _elgg_services()->translator->translate($error_key, [$action]);
81  _elgg_services()->systemMessages->addErrorMessage($msg);
82  }
83 
84  $forwarder = empty($forwarder) ? REFERER : $forwarder;
85  forward($forwarder);
86  };
87 
88  if (!isset($this->actions[$action])) {
89  $forward('actionundefined');
90  }
91 
92  $user = _elgg_services()->session->getLoggedInUser();
93 
94  // access checks
95  switch ($this->actions[$action]['access']) {
96  case 'public':
97  break;
98  case 'logged_in':
99  if (!$user) {
100  $forward('actionloggedout');
101  }
102  break;
103  default:
104  // admin or misspelling
105  if (!$user->isAdmin()) {
106  $forward('actionunauthorized');
107  }
108  }
109 
110  // To quietly cancel the file, return a falsey value in the "action" hook.
111  if (!_elgg_services()->hooks->trigger('action', $action, null, true)) {
112  $forward();
113  }
114 
115  $file = $this->actions[$action]['file'];
116 
117  if (!is_file($file) || !is_readable($file)) {
118  $forward('actionnotfound');
119  }
120 
121  self::includeFile($file);
122  $forward();
123  }
124 
131  protected static function includeFile($file) {
132  include $file;
133  }
134 
139  public function register($action, $filename = "", $access = 'logged_in') {
140  // plugins are encouraged to call actions with a trailing / to prevent 301
141  // redirects but we store the actions without it
142  $action = rtrim($action, '/');
143 
144  if (empty($filename)) {
145  $path = __DIR__ . '/../../../actions';
146  $filename = realpath("$path/$action.php");
147  }
148 
149  if (!in_array($access, self::$access_levels)) {
150  _elgg_services()->logger->error("Unrecognized value '$access' for \$access in " . __METHOD__);
151  $access = 'admin';
152  }
153 
154  $this->actions[$action] = array(
155  'file' => $filename,
156  'access' => $access,
157  );
158  return true;
159  }
160 
165  public function unregister($action) {
166  if (isset($this->actions[$action])) {
167  unset($this->actions[$action]);
168  return true;
169  } else {
170  return false;
171  }
172  }
173 
178  public function validateActionToken($visible_errors = true, $token = null, $ts = null) {
179  if (!$token) {
180  $token = get_input('__elgg_token');
181  }
182 
183  if (!$ts) {
184  $ts = get_input('__elgg_ts');
185  }
186 
187  $session_id = _elgg_services()->session->getId();
188 
189  if (($token) && ($ts) && ($session_id)) {
190  if ($this->validateTokenOwnership($token, $ts)) {
191  if ($this->validateTokenTimestamp($ts)) {
192  // We have already got this far, so unless anything
193  // else says something to the contrary we assume we're ok
194  $returnval = _elgg_services()->hooks->trigger('action_gatekeeper:permissions:check', 'all', array(
195  'token' => $token,
196  'time' => $ts
197  ), true);
198 
199  if ($returnval) {
200  return true;
201  } else if ($visible_errors) {
202  register_error(_elgg_services()->translator->translate('actiongatekeeper:pluginprevents'));
203  }
204  } else if ($visible_errors) {
205  // this is necessary because of #5133
206  if (elgg_is_xhr()) {
207  register_error(_elgg_services()->translator->translate('js:security:token_refresh_failed', array(_elgg_services()->config->getSiteUrl())));
208  } else {
209  register_error(_elgg_services()->translator->translate('actiongatekeeper:timeerror'));
210  }
211  }
212  } else if ($visible_errors) {
213  // this is necessary because of #5133
214  if (elgg_is_xhr()) {
215  register_error(_elgg_services()->translator->translate('js:security:token_refresh_failed', array(_elgg_services()->config->getSiteUrl())));
216  } else {
217  register_error(_elgg_services()->translator->translate('actiongatekeeper:tokeninvalid'));
218  }
219  }
220  } else {
221  $req = _elgg_services()->request;
222  $length = $req->server->get('CONTENT_LENGTH');
223  $post_count = count($req->request);
224  if ($length && $post_count < 1) {
225  // The size of $_POST or uploaded file has exceed the size limit
226  $error_msg = _elgg_services()->hooks->trigger('action_gatekeeper:upload_exceeded_msg', 'all', array(
227  'post_size' => $length,
228  'visible_errors' => $visible_errors,
229  ), _elgg_services()->translator->translate('actiongatekeeper:uploadexceeded'));
230  } else {
231  $error_msg = _elgg_services()->translator->translate('actiongatekeeper:missingfields');
232  }
233  if ($visible_errors) {
234  register_error($error_msg);
235  }
236  }
237 
238  return false;
239  }
240 
248  protected function validateTokenTimestamp($ts) {
249  $timeout = $this->getActionTokenTimeout();
250  $now = time();
251  return ($timeout == 0 || ($ts > $now - $timeout) && ($ts < $now + $timeout));
252  }
253 
260  public function getActionTokenTimeout() {
261  if (($timeout = _elgg_services()->config->get('action_token_timeout')) === null) {
262  // default to 2 hours
263  $timeout = 2;
264  }
265  $hour = 60 * 60;
266  return (int)((float)$timeout * $hour);
267  }
268 
273  public function gatekeeper($action) {
274  if ($action === 'login') {
275  if ($this->validateActionToken(false)) {
276  return true;
277  }
278 
279  $token = get_input('__elgg_token');
280  $ts = (int)get_input('__elgg_ts');
281  if ($token && $this->validateTokenTimestamp($ts)) {
282  // The tokens are present and the time looks valid: this is probably a mismatch due to the
283  // login form being on a different domain.
284  register_error(_elgg_services()->translator->translate('actiongatekeeper:crosssitelogin'));
285 
286  forward('login', 'csrf');
287  }
288 
289  // let the validator send an appropriate msg
290  $this->validateActionToken();
291 
292  } else if ($this->validateActionToken()) {
293  return true;
294  }
295 
296  forward(REFERER, 'csrf');
297  }
298 
309  public function validateTokenOwnership($token, $timestamp, $session_token = '') {
310  $required_token = $this->generateActionToken($timestamp, $session_token);
311 
312  return _elgg_services()->crypto->areEqual($token, $required_token);
313  }
314 
326  public function generateActionToken($timestamp, $session_token = '') {
327  if (!$session_token) {
328  $session_token = elgg_get_session()->get('__elgg_session');
329  if (!$session_token) {
330  return false;
331  }
332  }
333 
334  return _elgg_services()->crypto->getHmac([(int)$timestamp, $session_token], 'md5')
335  ->getToken();
336  }
337 
342  public function exists($action) {
343  return (isset($this->actions[$action]) && file_exists($this->actions[$action]['file']));
344  }
345 
350  public function ajaxForwardHook($hook, $reason, $return, $params) {
351  if (!elgg_is_xhr()) {
352  return;
353  }
354 
355  // grab any data echo'd in the action
356  $output = ob_get_clean();
357 
358  if ($reason == 'walled_garden') {
359  $reason = '403';
360  }
361  $http_codes = array(
362  '400' => 'Bad Request',
363  '401' => 'Unauthorized',
364  '403' => 'Forbidden',
365  '404' => 'Not Found',
366  '407' => 'Proxy Authentication Required',
367  '500' => 'Internal Server Error',
368  '503' => 'Service Unavailable',
369  );
370 
371  $ajax_api = _elgg_services()->ajax;
372  if ($ajax_api->isReady()) {
373  if (isset($http_codes[$reason])) {
374  $ajax_api->respondWithError($http_codes[$reason], $reason);
375  } else {
376  $ajax_api->respondFromOutput($output, "action:{$this->currentAction}");
377  }
378  exit;
379  }
380 
381  // legacy XHR behavior
382  if (isset($http_codes[$reason])) {
383  header("HTTP/1.1 $reason {$http_codes[$reason]}", true);
384  }
385 
386  // always pass the full structure to avoid boilerplate JS code.
387  $params = array_merge($params, array(
388  'output' => '',
389  'status' => 0,
390  'system_messages' => array(
391  'error' => array(),
392  'success' => array()
393  )
394  ));
395 
396  $params['output'] = $ajax_api->decodeJson($output);
397 
398  //Grab any system messages so we can inject them via ajax too
399  $system_messages = _elgg_services()->systemMessages->dumpRegister();
400 
401  if (isset($system_messages['success'])) {
402  $params['system_messages']['success'] = $system_messages['success'];
403  }
404 
405  if (isset($system_messages['error'])) {
406  $params['system_messages']['error'] = $system_messages['error'];
407  $params['status'] = -1;
408  }
409 
410  $context = array('action' => $this->currentAction);
411  $params = _elgg_services()->hooks->trigger('output', 'ajax', $context, $params);
412 
413  // Check the requester can accept JSON responses, if not fall back to
414  // returning JSON in a plain-text response. Some libraries request
415  // JSON in an invisible iframe which they then read from the iframe,
416  // however some browsers will not accept the JSON MIME type.
417  $http_accept = _elgg_services()->request->server->get('HTTP_ACCEPT');
418  if (stripos($http_accept, 'application/json') === false) {
419  header("Content-type: text/plain;charset=utf-8");
420  } else {
421  header("Content-type: application/json;charset=utf-8");
422  }
423 
424  echo json_encode($params);
425  exit;
426  }
427 
432  public function ajaxActionHook() {
433  if (elgg_is_xhr()) {
434  ob_start();
435  }
436  }
437 
443  public function getAllActions() {
444  return $this->actions;
445  }
446 }
447 
$action
Definition: full.php:125
elgg_is_xhr()
Checks whether the request was requested via ajax.
Definition: actions.php:226
$context
Definition: add.php:11
validateTokenOwnership($token, $timestamp, $session_token= '')
Was the given token generated for the session defined by session_token?
if(!array_key_exists($filename, $text_files)) $file
execute($action, $forwarder="")
static includeFile($file)
Include an action file with isolated scope.
elgg_get_session()
Gets Elgg's session object.
Definition: sessions.php:23
$path
Definition: details.php:88
$return
Definition: opendd.php:15
$timestamp
Definition: date.php:34
register_error($error)
Display an error on next page load.
Definition: elgglib.php:452
$actions
Definition: user_hover.php:12
$params
Definition: login.php:72
getAllActions()
Get all actions.
get_input($variable, $default=null, $filter_result=true)
Get some input from variables passed submitted through GET or POST.
Definition: input.php:27
const REFERER
Definition: elgglib.php:2029
$forward
Definition: delete.php:35
JSON endpoint response.
Definition: AjaxResponse.php:9
$user
Definition: ban.php:13
validateActionToken($visible_errors=true, $token=null, $ts=null)
_elgg_services(\Elgg\Di\ServiceProvider $services=null)
Get the global service provider.
Definition: autoloader.php:17
$token
forward($location="", $reason= 'system')
Forward to $location.
Definition: elgglib.php:93
validateTokenTimestamp($ts)
Is the token timestamp within acceptable range?
$filename
ajaxForwardHook($hook, $reason, $return, $params)
exit
Definition: autoloader.php:34
$output
Definition: item.php:10
$access
Definition: save.php:15
generateActionToken($timestamp, $session_token= '')
Generate a token from a session token (specifying the user), the timestamp, and the site key...