PageRenderTime 45ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/fuel/core/classes/controller/rest.php

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