Elgg  Version 2.3
ActionsService.php
Go to the documentation of this file.
1 <?php
2 namespace Elgg;
3 
7 
20 
22 
26  private $config;
27 
31  private $session;
32 
36  private $crypto;
37 
47  private $actions = array();
48 
53  private $currentAction = null;
54 
58  private static $access_levels = ['public', 'logged_in', 'admin'];
59 
67  public function __construct(Config $config, ElggSession $session, ElggCrypto $crypto) {
68  $this->config = $config;
69  $this->session = $session;
70  $this->crypto = $crypto;
71  }
72 
84  public function execute($action, $forwarder = "") {
85  $action = rtrim($action, '/');
86  $this->currentAction = $action;
87 
88  // @todo REMOVE THESE ONCE #1509 IS IN PLACE.
89  // Allow users to disable plugins without a token in order to
90  // remove plugins that are incompatible.
91  // Login and logout are for convenience.
92  // file/download (see #2010)
93  $exceptions = array(
94  'admin/plugins/disable',
95  'logout',
96  'file/download',
97  );
98 
99  if (!in_array($action, $exceptions)) {
100  // All actions require a token.
101  $pass = $this->gatekeeper($action);
102  if (!$pass) {
103  return;
104  }
105  }
106 
107  $forwarder = str_replace($this->config->getSiteUrl(), "", $forwarder);
108  $forwarder = str_replace("http://", "", $forwarder);
109  $forwarder = str_replace("@", "", $forwarder);
110  if (substr($forwarder, 0, 1) == "/") {
111  $forwarder = substr($forwarder, 1);
112  }
113 
114  $ob_started = false;
115 
123  $forward = function ($error_key = '', $status_code = ELGG_HTTP_OK) use ($action, $forwarder, &$ob_started) {
124  if ($error_key) {
125  if ($ob_started) {
126  ob_end_clean();
127  }
128  $msg = _elgg_services()->translator->translate($error_key, [$action]);
129  _elgg_services()->systemMessages->addErrorMessage($msg);
130  $response = new \Elgg\Http\ErrorResponse($msg, $status_code);
131  } else {
132  $content = ob_get_clean();
133  $response = new \Elgg\Http\OkResponse($content, $status_code);
134  }
135 
136  $forwarder = empty($forwarder) ? REFERER : $forwarder;
137  $response->setForwardURL($forwarder);
138  return $response;
139  };
140 
141  if (!isset($this->actions[$action])) {
142  return $forward('actionundefined', ELGG_HTTP_NOT_IMPLEMENTED);
143  }
144 
145  $user = $this->session->getLoggedInUser();
146 
147  // access checks
148  switch ($this->actions[$action]['access']) {
149  case 'public':
150  break;
151  case 'logged_in':
152  if (!$user) {
153  return $forward('actionloggedout', ELGG_HTTP_FORBIDDEN);
154  }
155  break;
156  default:
157  // admin or misspelling
158  if (!$user || !$user->isAdmin()) {
159  return $forward('actionunauthorized', ELGG_HTTP_FORBIDDEN);
160  }
161  }
162 
163  ob_start();
164 
165  // To quietly cancel the file, return a falsey value in the "action" hook.
166  if (!_elgg_services()->hooks->trigger('action', $action, null, true)) {
167  return $forward('', ELGG_HTTP_OK);
168  }
169 
170  $file = $this->actions[$action]['file'];
171 
172  if (!is_file($file) || !is_readable($file)) {
173  return $forward('actionnotfound', ELGG_HTTP_NOT_IMPLEMENTED);
174  }
175 
176  $result = Includer::includeFile($file);
177  if ($result instanceof ResponseBuilder) {
178  ob_end_clean();
179  return $result;
180  }
181 
182  return $forward('', ELGG_HTTP_OK);
183  }
184 
189  public function register($action, $filename = "", $access = 'logged_in') {
190  // plugins are encouraged to call actions with a trailing / to prevent 301
191  // redirects but we store the actions without it
192  $action = rtrim($action, '/');
193 
194  if (empty($filename)) {
195  $path = __DIR__ . '/../../../actions';
196  $filename = realpath("$path/$action.php");
197  }
198 
199  if (!in_array($access, self::$access_levels)) {
200  _elgg_services()->logger->error("Unrecognized value '$access' for \$access in " . __METHOD__);
201  $access = 'admin';
202  }
203 
204  $this->actions[$action] = array(
205  'file' => $filename,
206  'access' => $access,
207  );
208  return true;
209  }
210 
215  public function unregister($action) {
216  if (isset($this->actions[$action])) {
217  unset($this->actions[$action]);
218  return true;
219  } else {
220  return false;
221  }
222  }
223 
228  public function validateActionToken($visible_errors = true, $token = null, $ts = null) {
229  if (!$token) {
230  $token = get_input('__elgg_token');
231  }
232 
233  if (!$ts) {
234  $ts = get_input('__elgg_ts');
235  }
236 
237  $session_id = $this->session->getId();
238 
239  if (($token) && ($ts) && ($session_id)) {
240  if ($this->validateTokenOwnership($token, $ts)) {
241  if ($this->validateTokenTimestamp($ts)) {
242  // We have already got this far, so unless anything
243  // else says something to the contrary we assume we're ok
244  $returnval = _elgg_services()->hooks->trigger('action_gatekeeper:permissions:check', 'all', array(
245  'token' => $token,
246  'time' => $ts
247  ), true);
248 
249  if ($returnval) {
250  return true;
251  } else if ($visible_errors) {
252  register_error(_elgg_services()->translator->translate('actiongatekeeper:pluginprevents'));
253  }
254  } else if ($visible_errors) {
255  // this is necessary because of #5133
256  if (elgg_is_xhr()) {
257  register_error(_elgg_services()->translator->translate(
258  'js:security:token_refresh_failed',
259  array($this->config->getSiteUrl()
260  )));
261  } else {
262  register_error(_elgg_services()->translator->translate('actiongatekeeper:timeerror'));
263  }
264  }
265  } else if ($visible_errors) {
266  // this is necessary because of #5133
267  if (elgg_is_xhr()) {
268  register_error(_elgg_services()->translator->translate('js:security:token_refresh_failed', array($this->config->getSiteUrl())));
269  } else {
270  register_error(_elgg_services()->translator->translate('actiongatekeeper:tokeninvalid'));
271  }
272  }
273  } else {
274  $req = _elgg_services()->request;
275  $length = $req->server->get('CONTENT_LENGTH');
276  $post_count = count($req->request);
277  if ($length && $post_count < 1) {
278  // The size of $_POST or uploaded file has exceed the size limit
279  $error_msg = _elgg_services()->hooks->trigger('action_gatekeeper:upload_exceeded_msg', 'all', array(
280  'post_size' => $length,
281  'visible_errors' => $visible_errors,
282  ), _elgg_services()->translator->translate('actiongatekeeper:uploadexceeded'));
283  } else {
284  $error_msg = _elgg_services()->translator->translate('actiongatekeeper:missingfields');
285  }
286  if ($visible_errors) {
287  register_error($error_msg);
288  }
289  }
290 
291  return false;
292  }
293 
301  protected function validateTokenTimestamp($ts) {
302  $timeout = $this->getActionTokenTimeout();
303  $now = $this->getCurrentTime()->getTimestamp();
304  return ($timeout == 0 || ($ts > $now - $timeout) && ($ts < $now + $timeout));
305  }
306 
313  public function getActionTokenTimeout() {
314  if (($timeout = $this->config->get('action_token_timeout')) === null) {
315  // default to 2 hours
316  $timeout = 2;
317  }
318  $hour = 60 * 60;
319  return (int)((float)$timeout * $hour);
320  }
321 
327  public function gatekeeper($action) {
328  if ($action === 'login') {
329  if ($this->validateActionToken(false)) {
330  return true;
331  }
332 
333  $token = get_input('__elgg_token');
334  $ts = (int)get_input('__elgg_ts');
335  if ($token && $this->validateTokenTimestamp($ts)) {
336  // The tokens are present and the time looks valid: this is probably a mismatch due to the
337  // login form being on a different domain.
338  register_error(_elgg_services()->translator->translate('actiongatekeeper:crosssitelogin'));
339  _elgg_services()->responseFactory->redirect('login', 'csrf');
340  return false;
341  }
342  }
343 
344  if ($this->validateActionToken()) {
345  return true;
346  }
347 
348  _elgg_services()->responseFactory->redirect(REFERER, 'csrf');
349  return false;
350  }
351 
362  public function validateTokenOwnership($token, $timestamp, $session_token = '') {
363  $required_token = $this->generateActionToken($timestamp, $session_token);
364 
365  return _elgg_services()->crypto->areEqual($token, $required_token);
366  }
367 
379  public function generateActionToken($timestamp, $session_token = '') {
380  if (!$session_token) {
381  $session_token = elgg_get_session()->get('__elgg_session');
382  if (!$session_token) {
383  return false;
384  }
385  }
386 
387  return _elgg_services()->crypto->getHmac([(int)$timestamp, $session_token], 'md5')
388  ->getToken();
389  }
390 
395  public function exists($action) {
396  return (isset($this->actions[$action]) && file_exists($this->actions[$action]['file']));
397  }
398 
404  public function ajaxForwardHook($hook, $reason, $forward_url, $params) {
405  if (!elgg_is_xhr()) {
406  return;
407  }
408 
409  // grab any data echo'd in the action
410  $output = ob_get_clean();
411 
412  if ($reason == 'walled_garden' || $reason == 'csrf') {
413  $reason = '403';
414  }
415 
416  $status_code = (int) $reason;
417  if ($status_code < 100 || ($status_code > 299 && $status_code < 400) || $status_code > 599) {
418  // We only want to preserve OK and error codes
419  // Redirect responses should be converted to OK responses as this is an XHR request
420  $status_code = ELGG_HTTP_OK;
421  }
422 
423  $response = elgg_ok_response($output, '', $forward_url, $status_code);
424 
425  $headers = $response->getHeaders();
426  $headers['Content-Type'] = 'application/json; charset=UTF-8';
427  $response->setHeaders($headers);
428 
429  _elgg_services()->responseFactory->respond($response);
430  exit;
431  }
432 
438  public function ajaxActionHook() {
439  if (elgg_is_xhr()) {
440  ob_start();
441  }
442  }
443 
449  public function getAllActions() {
450  return $this->actions;
451  }
452 
459  public function handleTokenRefreshRequest() {
460  if (!elgg_is_xhr()) {
461  return false;
462  }
463 
464  // the page's session_token might have expired (not matching __elgg_session in the session), but
465  // we still allow it to be given to validate the tokens in the page.
466  $session_token = get_input('session_token', null, false);
467  $pairs = (array)get_input('pairs', array(), false);
468  $valid_tokens = (object)array();
469  foreach ($pairs as $pair) {
470  list($ts, $token) = explode(',', $pair, 2);
471  if ($this->validateTokenOwnership($token, $ts, $session_token)) {
472  $valid_tokens->{$token} = true;
473  }
474  }
475 
476  $ts = $this->getCurrentTime()->getTimestamp();
477  $token = $this->generateActionToken($ts);
478  $data = array(
479  'token' => array(
480  '__elgg_ts' => $ts,
481  '__elgg_token' => $token,
482  'logged_in' => $this->session->isLoggedIn(),
483  ),
484  'valid_tokens' => $valid_tokens,
485  'session_token' => $this->session->get('__elgg_session'),
486  'user_guid' => $this->session->getLoggedInUserGuid(),
487  );
488 
489  elgg_set_http_header("Content-Type: application/json;charset=utf-8");
490  return elgg_ok_response($data);
491  }
492 }
493 
const ELGG_HTTP_FORBIDDEN
Definition: elgglib.php:2153
const ELGG_HTTP_NOT_IMPLEMENTED
Definition: elgglib.php:2178
HTTP response builder interface.
$action
Definition: full.php:133
elgg_is_xhr()
Checks whether the request was requested via ajax.
Definition: actions.php:237
validateTokenOwnership($token, $timestamp, $session_token= '')
Was the given token generated for the session defined by session_token?
if(!$entity->delete()) $forward_url
Definition: delete.php:37
if(!array_key_exists($filename, $text_files)) $file
elgg_set_http_header($header, $replace=true)
Set a response HTTP header.
Definition: elgglib.php:114
execute($action, $forwarder="")
Executes an action If called from action() redirect will be issued by the response factory If called ...
elgg_get_session()
Gets Elgg&#39;s session object.
Definition: sessions.php:23
$headers
Definition: default.php:14
$data
Definition: opendd.php:13
$path
Definition: details.php:88
Access to configuration values.
Definition: Config.php:11
getCurrentTime($modifier= '')
Get the (cloned) time.
Definition: TimeUsing.php:26
$length
Definition: excerpt.php:14
$timestamp
Definition: date.php:33
$actions
Definition: user_hover.php:12
$params
Definition: login.php:72
getAllActions()
Get all actions.
Save menu items.
ajaxForwardHook($hook, $reason, $forward_url, $params)
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:2123
$forward
Definition: delete.php:35
$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
elgg_ok_response($content= '', $message= '', $forward_url=null, $status_code=ELGG_HTTP_OK)
Prepares a successful response to be returned by a page or an action handler.
validateTokenTimestamp($ts)
Is the token timestamp within acceptable range?
elgg register_error
Wrapper function for system_messages.
Definition: elgglib.js:399
__construct(Config $config, ElggSession $session, ElggCrypto $crypto)
Constructor.
$content
Set robots.txt action.
Definition: set_robots.php:6
const ELGG_HTTP_OK
Definition: elgglib.php:2131
handleTokenRefreshRequest()
Send an updated CSRF token, provided the page&#39;s current tokens were not fake.
$filename
exit
Definition: autoloader.php:34
trait TimeUsing
Adds methods for setting the current time (for testing)
Definition: TimeUsing.php:11
$output
Definition: item.php:10
$session
Definition: login.php:9
http free of to any person obtaining a copy of this software and associated documentation to deal in the Software without including without limitation the rights to use
Definition: MIT-LICENSE.txt:5
gatekeeper()
Alias of elgg_gatekeeper()
Definition: pagehandler.php:73
$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...