PageRenderTime 59ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

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

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