PageRenderTime 48ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/classes/controller/rest.php

http://github.com/fuel/core
PHP | 525 lines | 320 code | 73 blank | 132 comment | 46 complexity | 8d8fc3e2d8c696a9737a470197df325f MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. /**
  3. * Fuel is a fast, lightweight, community driven PHP 5.4+ framework.
  4. *
  5. * @package Fuel
  6. * @version 1.9-dev
  7. * @author Fuel Development Team
  8. * @license MIT License
  9. * @copyright 2010 - 2019 Fuel Development Team
  10. * @link https://fuelphp.com
  11. */
  12. namespace Fuel\Core;
  13. abstract class Controller_Rest extends \Controller
  14. {
  15. /**
  16. * @var null|string Set this in a controller to use a default format
  17. */
  18. protected $rest_format = null;
  19. /**
  20. * @var array contains a list of method properties such as limit, log and level
  21. */
  22. protected $methods = array();
  23. /**
  24. * @var integer status code to return in case a not defined action is called
  25. */
  26. protected $no_method_status = 405;
  27. /**
  28. * @var integer status code to return in case the called action doesn't return data
  29. */
  30. protected $no_data_status = 204;
  31. /**
  32. * @var string authentication to be used for this controller
  33. */
  34. protected $auth = null;
  35. /**
  36. * @var string the detected response format
  37. */
  38. protected $format = null;
  39. /**
  40. * @var integer default response http status
  41. */
  42. protected $http_status = 200;
  43. /**
  44. * @var string xml basenode name
  45. */
  46. protected $xml_basenode = null;
  47. /**
  48. * @var array List all supported methods
  49. */
  50. protected $_supported_formats = array(
  51. 'xml' => 'application/xml',
  52. 'rawxml' => 'application/xml',
  53. 'json' => 'application/json',
  54. 'jsonp'=> 'text/javascript',
  55. 'serialized' => 'application/vnd.php.serialized',
  56. 'php' => 'text/plain',
  57. 'html' => 'text/html',
  58. 'csv' => 'application/csv',
  59. );
  60. public function before()
  61. {
  62. parent::before();
  63. // Some Methods cant have a body
  64. $this->request->body = null;
  65. // Which format should the data be returned in?
  66. $this->request->lang = $this->_detect_lang();
  67. $this->response = \Response::forge();
  68. }
  69. public function after($response)
  70. {
  71. // If the response is an array
  72. if (is_array($response))
  73. {
  74. // set the response
  75. $response = $this->response($response);
  76. }
  77. // If the response is a Response object, we will use their
  78. // instead of ours.
  79. if ( ! $response instanceof \Response)
  80. {
  81. $response = $this->response;
  82. }
  83. return parent::after($response);
  84. }
  85. /**
  86. * Router
  87. *
  88. * Requests are not made to methods directly The request will be for an "object".
  89. * this simply maps the object and method to the correct Controller method.
  90. *
  91. * @param string $resource
  92. * @param array $arguments
  93. * @return bool|mixed
  94. */
  95. public function router($resource, $arguments)
  96. {
  97. \Config::load('rest', true);
  98. // If no (or an invalid) format is given, auto detect the format
  99. if (is_null($this->format) or ! array_key_exists($this->format, $this->_supported_formats))
  100. {
  101. // auto-detect the format
  102. $this->format = array_key_exists(\Input::extension(), $this->_supported_formats) ? \Input::extension() : $this->_detect_format();
  103. }
  104. // Get the configured auth method if none is defined
  105. $this->auth === null and $this->auth = \Config::get('rest.auth');
  106. //Check method is authorized if required, and if we're authorized
  107. if ($this->auth == 'basic')
  108. {
  109. $valid_login = $this->_prepare_basic_auth();
  110. }
  111. elseif ($this->auth == 'digest')
  112. {
  113. $valid_login = $this->_prepare_digest_auth();
  114. }
  115. elseif (method_exists($this, $this->auth))
  116. {
  117. if (($valid_login = $this->{$this->auth}()) instanceOf \Response)
  118. {
  119. return $valid_login;
  120. }
  121. }
  122. else
  123. {
  124. $valid_login = false;
  125. }
  126. //If the request passes auth then execute as normal
  127. if(empty($this->auth) or $valid_login)
  128. {
  129. // If they call user, go to $this->post_user();
  130. $controller_method = strtolower(\Input::method()) . '_' . $resource;
  131. // Fall back to action_ if no rest method is provided
  132. if ( ! method_exists($this, $controller_method))
  133. {
  134. $controller_method = 'action_'.$resource;
  135. }
  136. // If method is not available, set status code to 404
  137. if (method_exists($this, $controller_method))
  138. {
  139. return call_fuel_func_array(array($this, $controller_method), $arguments);
  140. }
  141. else
  142. {
  143. $this->response->status = $this->no_method_status;
  144. return;
  145. }
  146. }
  147. else
  148. {
  149. $this->response(array('status'=> 0, 'error'=> 'Not Authorized'), 401);
  150. }
  151. }
  152. /**
  153. * Response
  154. *
  155. * Takes pure data and optionally a status code, then creates the response
  156. *
  157. * @param mixed
  158. * @param int
  159. * @return object Response instance
  160. */
  161. protected function response($data = array(), $http_status = null)
  162. {
  163. // set the correct response header
  164. if (method_exists('Format', 'to_'.$this->format))
  165. {
  166. $this->response->set_header('Content-Type', $this->_supported_formats[$this->format]);
  167. }
  168. // no data returned?
  169. if ((is_array($data) and empty($data)) or ($data == ''))
  170. {
  171. // override the http status with the NO CONTENT status
  172. $http_status = $this->no_data_status;
  173. }
  174. // make sure we have a valid return status
  175. $http_status or $http_status = $this->http_status;
  176. // If the format method exists, call and return the output in that format
  177. if (method_exists('Format', 'to_'.$this->format))
  178. {
  179. // Handle XML output
  180. if ($this->format === 'xml')
  181. {
  182. // Detect basenode
  183. $xml_basenode = $this->xml_basenode;
  184. $xml_basenode or $xml_basenode = \Config::get('rest.xml_basenode', 'xml');
  185. // Set the XML response
  186. $this->response->body(\Format::forge($data)->{'to_'.$this->format}(null, null, $xml_basenode));
  187. }
  188. else
  189. {
  190. // Set the formatted response
  191. $this->response->body(\Format::forge($data)->{'to_'.$this->format}());
  192. }
  193. }
  194. // Format not supported, but the output is an array or an object that can not be cast to string
  195. elseif (is_array($data) or (is_object($data) and ! method_exists($data, '__toString')))
  196. {
  197. if (\Fuel::$env == \Fuel::PRODUCTION)
  198. {
  199. // not acceptable in production
  200. if ($http_status == 200)
  201. { $http_status = 406;
  202. }
  203. $this->response->body('The requested REST method returned an array or object, which is not compatible with the output format "'.$this->format.'"');
  204. }
  205. else
  206. {
  207. // convert it to json so we can at least read it while we're developing
  208. $this->response->body('The requested REST method returned an array or object:<br /><br />'.\Format::forge($data)->to_json(null, true));
  209. }
  210. }
  211. // Format not supported, output directly
  212. else
  213. {
  214. $this->response->body($data);
  215. }
  216. // Set the reponse http status
  217. $http_status and $this->response->status = $http_status;
  218. return $this->response;
  219. }
  220. /**
  221. * Set the Response http status.
  222. *
  223. * @param integer $status response http status code
  224. * @return void
  225. */
  226. protected function http_status($status)
  227. {
  228. $this->http_status = $status;
  229. }
  230. /**
  231. * Detect format
  232. *
  233. * Detect which format should be used to output the data
  234. *
  235. * @return string
  236. */
  237. protected function _detect_format()
  238. {
  239. // A format has been passed as a named parameter in the route
  240. if ($this->param('format') and array_key_exists($this->param('format'), $this->_supported_formats))
  241. {
  242. return $this->param('format');
  243. }
  244. // A format has been passed as an argument in the URL and it is supported
  245. if (\Input::param('format') and array_key_exists(\Input::param('format'), $this->_supported_formats))
  246. {
  247. return \Input::param('format');
  248. }
  249. // Otherwise, check the HTTP_ACCEPT (if it exists and we are allowed)
  250. if ($acceptable = \Input::server('HTTP_ACCEPT') and \Config::get('rest.ignore_http_accept') !== true)
  251. {
  252. // If anything is accepted, and we have a default, return that
  253. if ($acceptable == '*/*' and ! empty($this->rest_format))
  254. {
  255. return $this->rest_format;
  256. }
  257. // Split the Accept header and build an array of quality scores for each format
  258. $fragments = new \CachingIterator(new \ArrayIterator(preg_split('/[,;]/', $acceptable)));
  259. $acceptable = array();
  260. $next_is_quality = false;
  261. foreach ($fragments as $fragment)
  262. {
  263. $quality = 1;
  264. // Skip the fragment if it is a quality score
  265. if ($next_is_quality)
  266. {
  267. $next_is_quality = false;
  268. continue;
  269. }
  270. // If next fragment exists and is a quality score, set the quality score
  271. elseif ($fragments->hasNext())
  272. {
  273. $next = $fragments->getInnerIterator()->current();
  274. if (strpos($next, 'q=') === 0)
  275. {
  276. list($key, $quality) = explode('=', $next);
  277. $next_is_quality = true;
  278. }
  279. }
  280. $acceptable[$fragment] = $quality;
  281. }
  282. // Sort the formats by score in descending order
  283. uasort($acceptable, function($a, $b)
  284. {
  285. $a = (float) $a;
  286. $b = (float) $b;
  287. return ($a > $b) ? -1 : 1;
  288. });
  289. // Check each of the acceptable formats against the supported formats
  290. $find = array('\*', '/');
  291. $replace = array('.*', '\/');
  292. foreach ($acceptable as $pattern => $quality)
  293. {
  294. // The Accept header can contain wildcards in the format
  295. $pattern = '/^' . str_replace($find, $replace, preg_quote($pattern)) . '$/';
  296. foreach ($this->_supported_formats as $format => $mime)
  297. {
  298. if (preg_match($pattern, $mime))
  299. {
  300. return $format;
  301. }
  302. }
  303. }
  304. } // End HTTP_ACCEPT checking
  305. // Well, none of that has worked! Let's see if the controller has a default
  306. if ( ! empty($this->rest_format))
  307. {
  308. return $this->rest_format;
  309. }
  310. // Just use the default format
  311. return \Config::get('rest.default_format');
  312. }
  313. /**
  314. * Detect language(s)
  315. *
  316. * What language do they want it in?
  317. *
  318. * @return null|array|string
  319. */
  320. protected function _detect_lang()
  321. {
  322. if (!$lang = \Input::server('HTTP_ACCEPT_LANGUAGE'))
  323. {
  324. return null;
  325. }
  326. // They might have sent a few, make it an array
  327. if (strpos($lang, ',') !== false)
  328. {
  329. $langs = explode(',', $lang);
  330. $return_langs = array();
  331. foreach ($langs as $lang)
  332. {
  333. // Remove weight and strip space
  334. list($lang) = explode(';', $lang);
  335. $return_langs[] = trim($lang);
  336. }
  337. return $return_langs;
  338. }
  339. // Nope, just return the string
  340. return $lang;
  341. }
  342. // SECURITY FUNCTIONS ---------------------------------------------------------
  343. protected function _check_login($username = '', $password = null)
  344. {
  345. if (empty($username))
  346. {
  347. return false;
  348. }
  349. $valid_logins = \Config::get('rest.valid_logins');
  350. if (!array_key_exists($username, $valid_logins))
  351. {
  352. return false;
  353. }
  354. // If actually null (not empty string) then do not check it
  355. if ($password !== null and $valid_logins[$username] != $password)
  356. {
  357. return false;
  358. }
  359. return true;
  360. }
  361. protected function _prepare_basic_auth()
  362. {
  363. $username = null;
  364. $password = null;
  365. // mod_php
  366. if (\Input::server('PHP_AUTH_USER'))
  367. {
  368. $username = \Input::server('PHP_AUTH_USER');
  369. $password = \Input::server('PHP_AUTH_PW');
  370. }
  371. // most other servers
  372. elseif (\Input::server('HTTP_AUTHENTICATION'))
  373. {
  374. if (strpos(strtolower(\Input::server('HTTP_AUTHENTICATION')), 'basic') === 0)
  375. {
  376. list($username, $password) = explode(':', base64_decode(substr(\Input::server('HTTP_AUTHORIZATION'), 6)));
  377. }
  378. }
  379. if ( ! static::_check_login($username, $password))
  380. {
  381. static::_force_login();
  382. return false;
  383. }
  384. return true;
  385. }
  386. protected function _prepare_digest_auth()
  387. {
  388. // Empty argument for backward compatibility
  389. $uniqid = uniqid("");
  390. // We need to test which server authentication variable to use
  391. // because the PHP ISAPI module in IIS acts different from CGI
  392. if (\Input::server('PHP_AUTH_DIGEST'))
  393. {
  394. $digest_string = \Input::server('PHP_AUTH_DIGEST');
  395. }
  396. elseif (\Input::server('HTTP_AUTHORIZATION'))
  397. {
  398. $digest_string = \Input::server('HTTP_AUTHORIZATION');
  399. }
  400. else
  401. {
  402. $digest_string = '';
  403. }
  404. // Prompt for authentication if we don't have a digest string
  405. if (empty($digest_string))
  406. {
  407. static::_force_login($uniqid);
  408. return false;
  409. }
  410. // We need to retrieve authentication informations from the $digest_string variable
  411. $digest_params = explode(',', $digest_string);
  412. foreach ($digest_params as $digest_param)
  413. {
  414. $digest_param = explode('=', trim($digest_param), 2);
  415. if (isset($digest_param[1]))
  416. {
  417. $digest[$digest_param[0]] = trim($digest_param[1], '"');
  418. }
  419. }
  420. // if no username, or an invalid username found, re-authenticate
  421. if ( ! array_key_exists('username', $digest) or ! static::_check_login($digest['username']))
  422. {
  423. static::_force_login($uniqid);
  424. return false;
  425. }
  426. // validate the configured login/password
  427. $valid_logins = \Config::get('rest.valid_logins');
  428. $valid_pass = $valid_logins[$digest['username']];
  429. // This is the valid response expected
  430. $A1 = md5($digest['username'] . ':' . \Config::get('rest.realm') . ':' . $valid_pass);
  431. $A2 = md5(strtoupper(\Input::method()) . ':' . $digest['uri']);
  432. $valid_response = md5($A1 . ':' . $digest['nonce'] . ':' . $digest['nc'] . ':' . $digest['cnonce'] . ':' . $digest['qop'] . ':' . $A2);
  433. if ($digest['response'] != $valid_response)
  434. {
  435. return false;
  436. }
  437. return true;
  438. }
  439. protected function _force_login($nonce = '')
  440. {
  441. // Get the configured auth method if none is defined
  442. $this->auth === null and $this->auth = \Config::get('rest.auth');
  443. if ($this->auth == 'basic')
  444. {
  445. $this->response->set_header('WWW-Authenticate', 'Basic realm="'. \Config::get('rest.realm') . '"');
  446. }
  447. elseif ($this->auth == 'digest')
  448. {
  449. $this->response->set_header('WWW-Authenticate', 'Digest realm="' . \Config::get('rest.realm') . '", qop="auth", nonce="' . $nonce . '", opaque="' . md5(\Config::get('rest.realm')) . '"');
  450. }
  451. }
  452. }