ActionsService.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. <?php
  2. namespace Elgg;
  3. /**
  4. * WARNING: API IN FLUX. DO NOT USE DIRECTLY.
  5. *
  6. * Use the elgg_* versions instead.
  7. *
  8. * @access private
  9. *
  10. * @package Elgg.Core
  11. * @subpackage Actions
  12. * @since 1.9.0
  13. */
  14. class ActionsService {
  15. /**
  16. * Registered actions storage
  17. * @var array
  18. */
  19. private $actions = array();
  20. /**
  21. * The current action being processed
  22. * @var string
  23. */
  24. private $currentAction = null;
  25. /**
  26. * @see action
  27. * @access private
  28. */
  29. public function execute($action, $forwarder = "") {
  30. $action = rtrim($action, '/');
  31. $this->currentAction = $action;
  32. // @todo REMOVE THESE ONCE #1509 IS IN PLACE.
  33. // Allow users to disable plugins without a token in order to
  34. // remove plugins that are incompatible.
  35. // Login and logout are for convenience.
  36. // file/download (see #2010)
  37. $exceptions = array(
  38. 'admin/plugins/disable',
  39. 'logout',
  40. 'file/download',
  41. );
  42. if (!in_array($action, $exceptions)) {
  43. // All actions require a token.
  44. action_gatekeeper($action);
  45. }
  46. $forwarder = str_replace(_elgg_services()->config->getSiteUrl(), "", $forwarder);
  47. $forwarder = str_replace("http://", "", $forwarder);
  48. $forwarder = str_replace("@", "", $forwarder);
  49. if (substr($forwarder, 0, 1) == "/") {
  50. $forwarder = substr($forwarder, 1);
  51. }
  52. if (!isset($this->actions[$action])) {
  53. register_error(_elgg_services()->translator->translate('actionundefined', array($action)));
  54. } elseif (!_elgg_services()->session->isAdminLoggedIn() && ($this->actions[$action]['access'] === 'admin')) {
  55. register_error(_elgg_services()->translator->translate('actionunauthorized'));
  56. } elseif (!_elgg_services()->session->isLoggedIn() && ($this->actions[$action]['access'] !== 'public')) {
  57. register_error(_elgg_services()->translator->translate('actionloggedout'));
  58. } else {
  59. // To quietly cancel the action file, return a falsey value in the "action" hook.
  60. if (_elgg_services()->hooks->trigger('action', $action, null, true)) {
  61. if (is_file($this->actions[$action]['file']) && is_readable($this->actions[$action]['file'])) {
  62. self::includeFile($this->actions[$action]['file']);
  63. } else {
  64. register_error(_elgg_services()->translator->translate('actionnotfound', array($action)));
  65. }
  66. }
  67. }
  68. $forwarder = empty($forwarder) ? REFERER : $forwarder;
  69. forward($forwarder);
  70. }
  71. /**
  72. * Include an action file with isolated scope
  73. *
  74. * @param string $file File to be interpreted by PHP
  75. * @return void
  76. */
  77. protected static function includeFile($file) {
  78. include $file;
  79. }
  80. /**
  81. * @see elgg_register_action
  82. * @access private
  83. */
  84. public function register($action, $filename = "", $access = 'logged_in') {
  85. // plugins are encouraged to call actions with a trailing / to prevent 301
  86. // redirects but we store the actions without it
  87. $action = rtrim($action, '/');
  88. if (empty($filename)) {
  89. $path = _elgg_services()->config->get('path');
  90. if ($path === null) {
  91. $path = "";
  92. }
  93. $filename = $path . "actions/" . $action . ".php";
  94. }
  95. $this->actions[$action] = array(
  96. 'file' => $filename,
  97. 'access' => $access,
  98. );
  99. return true;
  100. }
  101. /**
  102. * @see elgg_unregister_action
  103. * @access private
  104. */
  105. public function unregister($action) {
  106. if (isset($this->actions[$action])) {
  107. unset($this->actions[$action]);
  108. return true;
  109. } else {
  110. return false;
  111. }
  112. }
  113. /**
  114. * @see validate_action_token
  115. * @access private
  116. */
  117. public function validateActionToken($visible_errors = true, $token = null, $ts = null) {
  118. if (!$token) {
  119. $token = get_input('__elgg_token');
  120. }
  121. if (!$ts) {
  122. $ts = get_input('__elgg_ts');
  123. }
  124. $session_id = _elgg_services()->session->getId();
  125. if (($token) && ($ts) && ($session_id)) {
  126. // generate token, check with input and forward if invalid
  127. $required_token = $this->generateActionToken($ts);
  128. // Validate token
  129. $token_matches = _elgg_services()->crypto->areEqual($token, $required_token);
  130. if ($token_matches) {
  131. if ($this->validateTokenTimestamp($ts)) {
  132. // We have already got this far, so unless anything
  133. // else says something to the contrary we assume we're ok
  134. $returnval = _elgg_services()->hooks->trigger('action_gatekeeper:permissions:check', 'all', array(
  135. 'token' => $token,
  136. 'time' => $ts
  137. ), true);
  138. if ($returnval) {
  139. return true;
  140. } else if ($visible_errors) {
  141. register_error(_elgg_services()->translator->translate('actiongatekeeper:pluginprevents'));
  142. }
  143. } else if ($visible_errors) {
  144. // this is necessary because of #5133
  145. if (elgg_is_xhr()) {
  146. register_error(_elgg_services()->translator->translate('js:security:token_refresh_failed', array(_elgg_services()->config->getSiteUrl())));
  147. } else {
  148. register_error(_elgg_services()->translator->translate('actiongatekeeper:timeerror'));
  149. }
  150. }
  151. } else if ($visible_errors) {
  152. // this is necessary because of #5133
  153. if (elgg_is_xhr()) {
  154. register_error(_elgg_services()->translator->translate('js:security:token_refresh_failed', array(_elgg_services()->config->getSiteUrl())));
  155. } else {
  156. register_error(_elgg_services()->translator->translate('actiongatekeeper:tokeninvalid'));
  157. }
  158. }
  159. } else {
  160. $req = _elgg_services()->request;
  161. $length = $req->server->get('CONTENT_LENGTH');
  162. $post_count = count($req->request);
  163. if ($length && $post_count < 1) {
  164. // The size of $_POST or uploaded file has exceed the size limit
  165. $error_msg = _elgg_services()->hooks->trigger('action_gatekeeper:upload_exceeded_msg', 'all', array(
  166. 'post_size' => $length,
  167. 'visible_errors' => $visible_errors,
  168. ), _elgg_services()->translator->translate('actiongatekeeper:uploadexceeded'));
  169. } else {
  170. $error_msg = _elgg_services()->translator->translate('actiongatekeeper:missingfields');
  171. }
  172. if ($visible_errors) {
  173. register_error($error_msg);
  174. }
  175. }
  176. return false;
  177. }
  178. /**
  179. * Is the token timestamp within acceptable range?
  180. *
  181. * @param int $ts timestamp from the CSRF token
  182. *
  183. * @return bool
  184. */
  185. protected function validateTokenTimestamp($ts) {
  186. $timeout = $this->getActionTokenTimeout();
  187. $now = time();
  188. return ($timeout == 0 || ($ts > $now - $timeout) && ($ts < $now + $timeout));
  189. }
  190. /**
  191. * @see \Elgg\ActionsService::validateActionToken
  192. * @access private
  193. * @since 1.9.0
  194. * @return int number of seconds that action token is valid
  195. */
  196. public function getActionTokenTimeout() {
  197. if (($timeout = _elgg_services()->config->get('action_token_timeout')) === null) {
  198. // default to 2 hours
  199. $timeout = 2;
  200. }
  201. $hour = 60 * 60;
  202. return (int)((float)$timeout * $hour);
  203. }
  204. /**
  205. * @see action_gatekeeper
  206. * @access private
  207. */
  208. public function gatekeeper($action) {
  209. if ($action === 'login') {
  210. if ($this->validateActionToken(false)) {
  211. return true;
  212. }
  213. $token = get_input('__elgg_token');
  214. $ts = (int)get_input('__elgg_ts');
  215. if ($token && $this->validateTokenTimestamp($ts)) {
  216. // The tokens are present and the time looks valid: this is probably a mismatch due to the
  217. // login form being on a different domain.
  218. register_error(_elgg_services()->translator->translate('actiongatekeeper:crosssitelogin'));
  219. forward('login', 'csrf');
  220. }
  221. // let the validator send an appropriate msg
  222. $this->validateActionToken();
  223. } else if ($this->validateActionToken()) {
  224. return true;
  225. }
  226. forward(REFERER, 'csrf');
  227. }
  228. /**
  229. * @see generate_action_token
  230. * @access private
  231. */
  232. public function generateActionToken($timestamp) {
  233. $session_id = _elgg_services()->session->getId();
  234. if (!$session_id) {
  235. return false;
  236. }
  237. $session_token = _elgg_services()->session->get('__elgg_session');
  238. return _elgg_services()->crypto->getHmac([(int)$timestamp, $session_id, $session_token], 'md5')
  239. ->getToken();
  240. }
  241. /**
  242. * @see elgg_action_exists
  243. * @access private
  244. */
  245. public function exists($action) {
  246. return (isset($this->actions[$action]) && file_exists($this->actions[$action]['file']));
  247. }
  248. /**
  249. * @see ajax_forward_hook
  250. * @access private
  251. */
  252. public function ajaxForwardHook($hook, $reason, $return, $params) {
  253. if (elgg_is_xhr()) {
  254. // always pass the full structure to avoid boilerplate JS code.
  255. $params = array_merge($params, array(
  256. 'output' => '',
  257. 'status' => 0,
  258. 'system_messages' => array(
  259. 'error' => array(),
  260. 'success' => array()
  261. )
  262. ));
  263. //grab any data echo'd in the action
  264. $output = ob_get_clean();
  265. //Avoid double-encoding in case data is json
  266. $json = json_decode($output);
  267. if (isset($json)) {
  268. $params['output'] = $json;
  269. } else {
  270. $params['output'] = $output;
  271. }
  272. //Grab any system messages so we can inject them via ajax too
  273. $system_messages = _elgg_services()->systemMessages->dumpRegister();
  274. if (isset($system_messages['success'])) {
  275. $params['system_messages']['success'] = $system_messages['success'];
  276. }
  277. if (isset($system_messages['error'])) {
  278. $params['system_messages']['error'] = $system_messages['error'];
  279. $params['status'] = -1;
  280. }
  281. if ($reason == 'walled_garden') {
  282. $reason = '403';
  283. }
  284. $httpCodes = array(
  285. '400' => 'Bad Request',
  286. '401' => 'Unauthorized',
  287. '403' => 'Forbidden',
  288. '404' => 'Not Found',
  289. '407' => 'Proxy Authentication Required',
  290. '500' => 'Internal Server Error',
  291. '503' => 'Service Unavailable',
  292. );
  293. if (isset($httpCodes[$reason])) {
  294. header("HTTP/1.1 $reason {$httpCodes[$reason]}", true);
  295. }
  296. $context = array('action' => $this->currentAction);
  297. $params = _elgg_services()->hooks->trigger('output', 'ajax', $context, $params);
  298. // Check the requester can accept JSON responses, if not fall back to
  299. // returning JSON in a plain-text response. Some libraries request
  300. // JSON in an invisible iframe which they then read from the iframe,
  301. // however some browsers will not accept the JSON MIME type.
  302. $http_accept = _elgg_services()->request->server->get('HTTP_ACCEPT');
  303. if (stripos($http_accept, 'application/json') === false) {
  304. header("Content-type: text/plain;charset=utf-8");
  305. } else {
  306. header("Content-type: application/json;charset=utf-8");
  307. }
  308. echo json_encode($params);
  309. exit;
  310. }
  311. }
  312. /**
  313. * @see ajax_action_hook
  314. * @access private
  315. */
  316. public function ajaxActionHook() {
  317. if (elgg_is_xhr()) {
  318. ob_start();
  319. }
  320. }
  321. /**
  322. * Get all actions
  323. *
  324. * @return array
  325. */
  326. public function getAllActions() {
  327. return $this->actions;
  328. }
  329. }