Checks if the types of returned expressions are compatible with the documented types.
1 | <?php |
||
2 | namespace Elgg; |
||
3 | |||
4 | use Elgg\Http\ResponseBuilder; |
||
5 | use ElggCrypto; |
||
6 | use ElggSession; |
||
7 | |||
8 | /** |
||
9 | * WARNING: API IN FLUX. DO NOT USE DIRECTLY. |
||
10 | * |
||
11 | * Use the elgg_* versions instead. |
||
12 | * |
||
13 | * @access private |
||
14 | * |
||
15 | * @package Elgg.Core |
||
16 | * @subpackage Actions |
||
17 | * @since 1.9.0 |
||
18 | */ |
||
19 | class ActionsService { |
||
20 | |||
21 | use \Elgg\TimeUsing; |
||
22 | |||
23 | /** |
||
24 | * @var Config |
||
25 | */ |
||
26 | private $config; |
||
27 | |||
28 | /** |
||
29 | * @var ElggSession |
||
30 | */ |
||
31 | private $session; |
||
32 | |||
33 | /** |
||
34 | * @var ElggCrypto |
||
35 | */ |
||
36 | private $crypto; |
||
37 | |||
38 | /** |
||
39 | * Registered actions storage |
||
40 | * |
||
41 | * Each element has keys: |
||
42 | * "file" => filename |
||
43 | * "access" => access level |
||
44 | * |
||
45 | * @var array |
||
46 | */ |
||
47 | private $actions = []; |
||
48 | |||
49 | /** |
||
50 | * The current action being processed |
||
51 | * @var string |
||
52 | */ |
||
53 | private $currentAction = null; |
||
54 | |||
55 | /** |
||
56 | * @var string[] |
||
57 | */ |
||
58 | private static $access_levels = ['public', 'logged_in', 'admin']; |
||
59 | |||
60 | /** |
||
61 | * Constructor |
||
62 | * |
||
63 | * @param Config $config Config |
||
64 | * @param ElggSession $session Session |
||
65 | * @param ElggCrypto $crypto Crypto service |
||
66 | */ |
||
67 | 75 | public function __construct(Config $config, ElggSession $session, ElggCrypto $crypto) { |
|
68 | 75 | $this->config = $config; |
|
69 | 75 | $this->session = $session; |
|
70 | 75 | $this->crypto = $crypto; |
|
71 | 75 | } |
|
72 | |||
73 | /** |
||
74 | * Executes an action |
||
75 | * If called from action() redirect will be issued by the response factory |
||
76 | * If called as /action page handler response will be handled by \Elgg\Router |
||
77 | * |
||
78 | * @param string $action Action name |
||
79 | * @param string $forwarder URL to forward to after completion |
||
80 | * @return ResponseBuilder|null |
||
81 | * @see action() |
||
82 | * @access private |
||
83 | */ |
||
84 | 66 | public function execute($action, $forwarder = "") { |
|
85 | 66 | $action = rtrim($action, '/'); |
|
86 | 66 | $this->currentAction = $action; |
|
87 | |||
88 | // Logout is for convenience. |
||
89 | $exceptions = [ |
||
90 | 66 | 'logout', |
|
91 | ]; |
||
92 | |||
93 | 66 | if (!in_array($action, $exceptions)) { |
|
94 | // All actions require a token. |
||
95 | 66 | $pass = $this->gatekeeper($action); |
|
96 | 66 | if (!$pass) { |
|
97 | 1 | return; |
|
98 | } |
||
99 | } |
||
100 | |||
101 | 65 | $forwarder = str_replace($this->config->wwwroot, "", $forwarder); |
|
102 | 65 | $forwarder = str_replace("http://", "", $forwarder); |
|
103 | 65 | $forwarder = str_replace("@", "", $forwarder); |
|
104 | 65 | if (substr($forwarder, 0, 1) == "/") { |
|
105 | 1 | $forwarder = substr($forwarder, 1); |
|
106 | } |
||
107 | |||
108 | 65 | $ob_started = false; |
|
109 | |||
110 | /** |
||
111 | * Prepare action response |
||
112 | * |
||
113 | * @param string $error_key Error message key |
||
114 | * @param int $status_code HTTP status code |
||
115 | * @return ResponseBuilder |
||
116 | */ |
||
117 | 65 | $forward = function ($error_key = '', $status_code = ELGG_HTTP_OK) use ($action, $forwarder, &$ob_started) { |
|
118 | 23 | if ($error_key) { |
|
119 | 9 | if ($ob_started) { |
|
120 | ob_end_clean(); |
||
121 | } |
||
122 | 9 | $msg = _elgg_services()->translator->translate($error_key, [$action]); |
|
123 | 9 | _elgg_services()->systemMessages->addErrorMessage($msg); |
|
124 | 9 | $response = new \Elgg\Http\ErrorResponse($msg, $status_code); |
|
125 | } else { |
||
126 | 14 | $content = ob_get_clean(); |
|
127 | 14 | $response = new \Elgg\Http\OkResponse($content, $status_code); |
|
128 | } |
||
129 | |||
130 | 23 | $forwarder = empty($forwarder) ? REFERER : $forwarder; |
|
131 | 23 | $response->setForwardURL($forwarder); |
|
132 | 23 | return $response; |
|
133 | 65 | }; |
|
134 | |||
135 | 65 | if (!isset($this->actions[$action])) { |
|
136 | 5 | return $forward('actionundefined', ELGG_HTTP_NOT_IMPLEMENTED); |
|
137 | } |
||
138 | |||
139 | 60 | $user = $this->session->getLoggedInUser(); |
|
140 | |||
141 | // access checks |
||
142 | 60 | switch ($this->actions[$action]['access']) { |
|
143 | case 'public': |
||
144 | 35 | break; |
|
145 | case 'logged_in': |
||
146 | 21 | if (!$user) { |
|
147 | 1 | return $forward('actionloggedout', ELGG_HTTP_FORBIDDEN); |
|
148 | } |
||
149 | 20 | break; |
|
150 | default: |
||
151 | // admin or misspelling |
||
152 | 4 | if (!$user || !$user->isAdmin()) { |
|
153 | 1 | return $forward('actionunauthorized', ELGG_HTTP_FORBIDDEN); |
|
154 | } |
||
155 | } |
||
156 | |||
157 | 58 | ob_start(); |
|
158 | |||
159 | // To quietly cancel the file, return a falsey value in the "action" hook. |
||
160 | 58 | if (!_elgg_services()->hooks->trigger('action', $action, null, true)) { |
|
161 | 1 | return $forward('', ELGG_HTTP_OK); |
|
162 | } |
||
163 | |||
164 | 57 | $file = $this->actions[$action]['file']; |
|
165 | |||
166 | 57 | if (!is_file($file) || !is_readable($file)) { |
|
167 | 2 | ob_end_clean(); |
|
168 | 2 | return $forward('actionnotfound', ELGG_HTTP_NOT_IMPLEMENTED); |
|
169 | } |
||
170 | |||
171 | // set the maximum execution time for actions |
||
172 | 55 | $action_timeout = $this->config->action_time_limit; |
|
173 | 55 | if (isset($action_timeout)) { |
|
174 | set_time_limit($action_timeout); |
||
175 | } |
||
176 | |||
177 | 55 | $result = Includer::includeFile($file); |
|
178 | 55 | if ($result instanceof ResponseBuilder) { |
|
179 | 42 | ob_end_clean(); |
|
180 | 42 | return $result; |
|
181 | } |
||
182 | |||
183 | 13 | return $forward('', ELGG_HTTP_OK); |
|
184 | } |
||
185 | |||
186 | /** |
||
187 | * Registers an action |
||
188 | * |
||
189 | * @param string $action The name of the action (eg "register", "account/settings/save") |
||
190 | * @param string $filename Optionally, the filename where this action is located. If not specified, |
||
191 | * will assume the action is in elgg/actions/<action>.php |
||
192 | * @param string $access Who is allowed to execute this action: public, logged_in, admin. |
||
193 | * (default: logged_in) |
||
194 | * |
||
195 | * @return bool |
||
196 | * |
||
197 | * @see elgg_register_action() |
||
198 | * @access private |
||
199 | */ |
||
200 | 87 | public function register($action, $filename = "", $access = 'logged_in') { |
|
201 | // plugins are encouraged to call actions with a trailing / to prevent 301 |
||
202 | // redirects but we store the actions without it |
||
203 | 87 | $action = rtrim($action, '/'); |
|
204 | |||
205 | 87 | if (empty($filename)) { |
|
206 | 32 | $path = __DIR__ . '/../../../actions'; |
|
207 | 32 | $filename = realpath("$path/$action.php"); |
|
208 | } |
||
209 | |||
210 | 87 | if (!in_array($access, self::$access_levels)) { |
|
211 | 1 | _elgg_services()->logger->error("Unrecognized value '$access' for \$access in " . __METHOD__); |
|
212 | 1 | $access = 'admin'; |
|
213 | } |
||
214 | |||
215 | 87 | $this->actions[$action] = [ |
|
216 | 87 | 'file' => $filename, |
|
217 | 87 | 'access' => $access, |
|
218 | ]; |
||
219 | 87 | return true; |
|
220 | } |
||
221 | |||
222 | /** |
||
223 | * Unregisters an action |
||
224 | * |
||
225 | * @param string $action Action name |
||
226 | * |
||
227 | * @return bool |
||
228 | * |
||
229 | * @see elgg_unregister_action() |
||
230 | * @access private |
||
231 | */ |
||
232 | 1 | public function unregister($action) { |
|
233 | 1 | if (isset($this->actions[$action])) { |
|
234 | 1 | unset($this->actions[$action]); |
|
235 | 1 | return true; |
|
236 | } else { |
||
237 | 1 | return false; |
|
238 | } |
||
239 | } |
||
240 | |||
241 | /** |
||
242 | * Validate an action token. |
||
243 | * |
||
244 | * Calls to actions will automatically validate tokens. If tokens are not |
||
245 | * present or invalid, the action will be denied and the user will be redirected. |
||
246 | * |
||
247 | * Plugin authors should never have to manually validate action tokens. |
||
248 | * |
||
249 | * @param bool $visible_errors Emit {@link register_error()} errors on failure? |
||
250 | * @param mixed $token The token to test against. Default: $_REQUEST['__elgg_token'] |
||
251 | * @param mixed $ts The time stamp to test against. Default: $_REQUEST['__elgg_ts'] |
||
252 | * |
||
253 | * @return bool |
||
254 | * |
||
255 | * @see validate_action_token() |
||
256 | * @access private |
||
257 | */ |
||
258 | 70 | public function validateActionToken($visible_errors = true, $token = null, $ts = null) { |
|
259 | 70 | if (!$token) { |
|
260 | 67 | $token = get_input('__elgg_token'); |
|
261 | } |
||
262 | |||
263 | 70 | if (!$ts) { |
|
264 | 67 | $ts = get_input('__elgg_ts'); |
|
265 | } |
||
266 | |||
267 | 70 | $session_id = $this->session->getId(); |
|
268 | |||
269 | 70 | if (($token) && ($ts) && ($session_id)) { |
|
270 | 70 | if ($this->validateTokenOwnership($token, $ts)) { |
|
271 | 68 | if ($this->validateTokenTimestamp($ts)) { |
|
272 | // We have already got this far, so unless anything |
||
273 | // else says something to the contrary we assume we're ok |
||
274 | 67 | $returnval = _elgg_services()->hooks->trigger('action_gatekeeper:permissions:check', 'all', [ |
|
275 | 67 | 'token' => $token, |
|
276 | 67 | 'time' => $ts |
|
277 | 67 | ], true); |
|
278 | |||
279 | 67 | if ($returnval) { |
|
280 | 67 | return true; |
|
281 | } else if ($visible_errors) { |
||
282 | register_error(_elgg_services()->translator->translate('actiongatekeeper:pluginprevents')); |
||
283 | } |
||
284 | 1 | } else if ($visible_errors) { |
|
285 | // this is necessary because of #5133 |
||
286 | if (elgg_is_xhr()) { |
||
287 | register_error(_elgg_services()->translator->translate( |
||
288 | 'js:security:token_refresh_failed', |
||
289 | [$this->config->wwwroot] |
||
290 | )); |
||
291 | } else { |
||
292 | 1 | register_error(_elgg_services()->translator->translate('actiongatekeeper:timeerror')); |
|
293 | } |
||
294 | } |
||
295 | 3 | } else if ($visible_errors) { |
|
296 | // this is necessary because of #5133 |
||
297 | 1 | if (elgg_is_xhr()) { |
|
298 | register_error(_elgg_services()->translator->translate('js:security:token_refresh_failed', [$this->config->wwwroot])); |
||
299 | } else { |
||
300 | 4 | register_error(_elgg_services()->translator->translate('actiongatekeeper:tokeninvalid')); |
|
301 | } |
||
302 | } |
||
303 | } else { |
||
304 | 1 | $req = _elgg_services()->request; |
|
305 | 1 | $length = $req->server->get('CONTENT_LENGTH'); |
|
306 | 1 | $post_count = count($req->request); |
|
307 | 1 | if ($length && $post_count < 1) { |
|
308 | // The size of $_POST or uploaded file has exceed the size limit |
||
309 | $error_msg = _elgg_services()->hooks->trigger('action_gatekeeper:upload_exceeded_msg', 'all', [ |
||
310 | 'post_size' => $length, |
||
311 | 'visible_errors' => $visible_errors, |
||
312 | ], _elgg_services()->translator->translate('actiongatekeeper:uploadexceeded')); |
||
313 | } else { |
||
314 | 1 | $error_msg = _elgg_services()->translator->translate('actiongatekeeper:missingfields'); |
|
315 | } |
||
316 | 1 | if ($visible_errors) { |
|
317 | 1 | register_error($error_msg); |
|
318 | } |
||
319 | } |
||
320 | |||
321 | 5 | return false; |
|
322 | } |
||
323 | |||
324 | /** |
||
325 | * Is the token timestamp within acceptable range? |
||
326 | * |
||
327 | * @param int $ts timestamp from the CSRF token |
||
328 | * |
||
329 | * @return bool |
||
330 | */ |
||
331 | 68 | protected function validateTokenTimestamp($ts) { |
|
332 | 68 | $timeout = $this->getActionTokenTimeout(); |
|
333 | 68 | $now = $this->getCurrentTime()->getTimestamp(); |
|
334 | 68 | return ($timeout == 0 || ($ts > $now - $timeout) && ($ts < $now + $timeout)); |
|
335 | } |
||
336 | |||
337 | /** |
||
338 | * Returns the action token timeout in seconds |
||
339 | * |
||
340 | * @return int number of seconds that action token is valid |
||
341 | * |
||
342 | * @see ActionsService::validateActionToken |
||
343 | * @access private |
||
344 | * @since 1.9.0 |
||
345 | */ |
||
346 | 69 | public function getActionTokenTimeout() { |
|
347 | 69 | if (($timeout = $this->config->action_token_timeout) === null) { |
|
348 | // default to 2 hours |
||
349 | 69 | $timeout = 2; |
|
350 | } |
||
351 | 69 | $hour = 60 * 60; |
|
352 | 69 | return (int) ((float) $timeout * $hour); |
|
353 | } |
||
354 | |||
355 | /** |
||
356 | * Validates the presence of action tokens. |
||
357 | * |
||
358 | * This function is called for all actions. If action tokens are missing, |
||
359 | * the user will be forwarded to the site front page and an error emitted. |
||
360 | * |
||
361 | * This function verifies form input for security features (like a generated token), |
||
362 | * and forwards if they are invalid. |
||
363 | * |
||
364 | * @param string $action The action being performed |
||
365 | * |
||
366 | * @return bool |
||
367 | * |
||
368 | * @see action_gatekeeper() |
||
369 | * @access private |
||
370 | */ |
||
371 | 67 | public function gatekeeper($action) { |
|
372 | 67 | if ($action === 'login') { |
|
373 | 8 | if ($this->validateActionToken(false)) { |
|
374 | 8 | return true; |
|
375 | } |
||
376 | |||
377 | $token = get_input('__elgg_token'); |
||
378 | $ts = (int) get_input('__elgg_ts'); |
||
379 | if ($token && $this->validateTokenTimestamp($ts)) { |
||
380 | // The tokens are present and the time looks valid: this is probably a mismatch due to the |
||
381 | // login form being on a different domain. |
||
382 | register_error(_elgg_services()->translator->translate('actiongatekeeper:crosssitelogin')); |
||
383 | _elgg_services()->responseFactory->redirect('login', 'csrf'); |
||
384 | return false; |
||
385 | } |
||
386 | } |
||
387 | |||
388 | 59 | if ($this->validateActionToken()) { |
|
389 | 58 | return true; |
|
390 | } |
||
391 | |||
392 | 2 | _elgg_services()->responseFactory->redirect(REFERER, 'csrf'); |
|
393 | 2 | return false; |
|
394 | } |
||
395 | |||
396 | /** |
||
397 | * Was the given token generated for the session defined by session_token? |
||
398 | * |
||
399 | * @param string $token CSRF token |
||
400 | * @param int $timestamp Unix time |
||
401 | * @param string $session_token Session-specific token |
||
402 | * |
||
403 | * @return bool |
||
404 | * @access private |
||
405 | */ |
||
406 | 71 | public function validateTokenOwnership($token, $timestamp, $session_token = '') { |
|
407 | 71 | $required_token = $this->generateActionToken($timestamp, $session_token); |
|
408 | |||
409 | 71 | return _elgg_services()->crypto->areEqual($token, $required_token); |
|
410 | } |
||
411 | |||
412 | /** |
||
413 | * Generate a token from a session token (specifying the user), the timestamp, and the site key. |
||
414 | * |
||
415 | * @see generate_action_token() |
||
416 | * |
||
417 | * @param int $timestamp Unix timestamp |
||
418 | * @param string $session_token Session-specific token |
||
419 | * |
||
420 | * @return string |
||
421 | * @access private |
||
422 | */ |
||
423 | 71 | public function generateActionToken($timestamp, $session_token = '') { |
|
424 | 71 | if (!$session_token) { |
|
425 | 71 | $session_token = elgg_get_session()->get('__elgg_session'); |
|
426 | 71 | if (!$session_token) { |
|
427 | return false; |
||
0 ignored issues
–
show
Bug
Best Practice
introduced
by
Loading history...
|
|||
428 | } |
||
429 | } |
||
430 | |||
431 | 71 | return _elgg_services()->hmac->getHmac([(int) $timestamp, $session_token], 'md5') |
|
432 | 71 | ->getToken(); |
|
433 | } |
||
434 | |||
435 | /** |
||
436 | * Check if an action is registered and its script exists. |
||
437 | * |
||
438 | * @param string $action Action name |
||
439 | * |
||
440 | * @return bool |
||
441 | * |
||
442 | * @see elgg_action_exists() |
||
443 | * @access private |
||
444 | */ |
||
445 | 4 | public function exists($action) { |
|
446 | 4 | return (isset($this->actions[$action]) && file_exists($this->actions[$action]['file'])); |
|
447 | } |
||
448 | |||
449 | /** |
||
450 | * Get all actions |
||
451 | * |
||
452 | * @return array |
||
453 | */ |
||
454 | 4 | public function getAllActions() { |
|
455 | 4 | return $this->actions; |
|
456 | } |
||
457 | |||
458 | /** |
||
459 | * Send an updated CSRF token, provided the page's current tokens were not fake. |
||
460 | * |
||
461 | * @return ResponseBuilder |
||
462 | * @access private |
||
463 | */ |
||
464 | 1 | public function handleTokenRefreshRequest() { |
|
465 | 1 | if (!elgg_is_xhr()) { |
|
466 | return false; |
||
0 ignored issues
–
show
|
|||
467 | } |
||
468 | |||
469 | // the page's session_token might have expired (not matching __elgg_session in the session), but |
||
470 | // we still allow it to be given to validate the tokens in the page. |
||
471 | 1 | $session_token = get_input('session_token', null, false); |
|
472 | 1 | $pairs = (array) get_input('pairs', [], false); |
|
473 | 1 | $valid_tokens = (object) []; |
|
474 | 1 | foreach ($pairs as $pair) { |
|
475 | 1 | list($ts, $token) = explode(',', $pair, 2); |
|
476 | 1 | if ($this->validateTokenOwnership($token, $ts, $session_token)) { |
|
477 | 1 | $valid_tokens->{$token} = true; |
|
478 | } |
||
479 | } |
||
480 | |||
481 | 1 | $ts = $this->getCurrentTime()->getTimestamp(); |
|
482 | 1 | $token = $this->generateActionToken($ts); |
|
483 | $data = [ |
||
484 | 'token' => [ |
||
485 | 1 | '__elgg_ts' => $ts, |
|
486 | 1 | '__elgg_token' => $token, |
|
487 | 1 | 'logged_in' => $this->session->isLoggedIn(), |
|
488 | ], |
||
489 | 1 | 'valid_tokens' => $valid_tokens, |
|
490 | 1 | 'session_token' => $this->session->get('__elgg_session'), |
|
491 | 1 | 'user_guid' => $this->session->getLoggedInUserGuid(), |
|
492 | ]; |
||
493 | |||
494 | 1 | elgg_set_http_header("Content-Type: application/json;charset=utf-8"); |
|
495 | 1 | return elgg_ok_response($data); |
|
496 | } |
||
497 | } |
||
498 | |||
499 |