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

/application/libraries/REST_Controller.php

https://github.com/rogers-dwiputra/pa
PHP | 1138 lines | 583 code | 171 blank | 384 comment | 94 complexity | a370c2262bdc88005f4bd45f78872c2a MD5 | raw file
  1. <?php defined('BASEPATH') OR exit('No direct script access allowed');
  2. /**
  3. * CodeIgniter Rest Controller
  4. *
  5. * A fully RESTful server implementation for CodeIgniter using one library, one config file and one controller.
  6. *
  7. * @package CodeIgniter
  8. * @subpackage Libraries
  9. * @category Libraries
  10. * @author Phil Sturgeon
  11. * @license http://philsturgeon.co.uk/code/dbad-license
  12. * @link https://github.com/philsturgeon/codeigniter-restserver
  13. * @version 2.6.1
  14. */
  15. abstract class REST_Controller extends CI_Controller
  16. {
  17. /**
  18. * This defines the rest format.
  19. *
  20. * Must be overridden it in a controller so that it is set.
  21. *
  22. * @var string|null
  23. */
  24. protected $rest_format = NULL;
  25. /**
  26. * Defines the list of method properties such as limit, log and level
  27. *
  28. * @var array
  29. */
  30. protected $methods = array();
  31. /**
  32. * List of allowed HTTP methods
  33. *
  34. * @var array
  35. */
  36. protected $allowed_http_methods = array('get', 'delete', 'post', 'put');
  37. /**
  38. * General request data and information.
  39. * Stores accept, language, body, headers, etc.
  40. *
  41. * @var object
  42. */
  43. protected $request = NULL;
  44. /**
  45. * What is gonna happen in output?
  46. *
  47. * @var object
  48. */
  49. protected $response = NULL;
  50. /**
  51. * Stores DB, keys, key level, etc
  52. *
  53. * @var object
  54. */
  55. protected $rest = NULL;
  56. /**
  57. * Object to store data about the client sending the request
  58. *
  59. * @var object
  60. */
  61. protected $client = NULL;
  62. /**
  63. * The arguments for the GET request method
  64. *
  65. * @var array
  66. */
  67. protected $_get_args = array();
  68. /**
  69. * The arguments for the POST request method
  70. *
  71. * @var array
  72. */
  73. protected $_post_args = array();
  74. /**
  75. * The arguments for the PUT request method
  76. *
  77. * @var array
  78. */
  79. protected $_put_args = array();
  80. /**
  81. * The arguments for the DELETE request method
  82. *
  83. * @var array
  84. */
  85. protected $_delete_args = array();
  86. /**
  87. * The arguments from GET, POST, PUT, DELETE request methods combined.
  88. *
  89. * @var array
  90. */
  91. protected $_args = array();
  92. /**
  93. * If the request is allowed based on the API key provided.
  94. *
  95. * @var boolean
  96. */
  97. protected $_allow = TRUE;
  98. /**
  99. * Determines if output compression is enabled
  100. *
  101. * @var boolean
  102. */
  103. protected $_zlib_oc = FALSE;
  104. /**
  105. * List all supported methods, the first will be the default format
  106. *
  107. * @var array
  108. */
  109. protected $_supported_formats = array(
  110. 'xml' => 'application/xml',
  111. 'json' => 'application/json',
  112. 'jsonp' => 'application/javascript',
  113. 'serialized' => 'application/vnd.php.serialized',
  114. 'php' => 'text/plain',
  115. 'html' => 'text/html',
  116. 'csv' => 'application/csv'
  117. );
  118. /**
  119. * Developers can extend this class and add a check in here.
  120. */
  121. protected function early_checks()
  122. {
  123. }
  124. /**
  125. * Constructor function
  126. * @todo Document more please.
  127. */
  128. public function __construct()
  129. {
  130. parent::__construct();
  131. $this->_zlib_oc = @ini_get('zlib.output_compression');
  132. // Lets grab the config and get ready to party
  133. $this->load->config('rest');
  134. // let's learn about the request
  135. $this->request = new stdClass();
  136. // Is it over SSL?
  137. $this->request->ssl = $this->_detect_ssl();
  138. // How is this request being made? POST, DELETE, GET, PUT?
  139. $this->request->method = $this->_detect_method();
  140. // Create argument container, if nonexistent
  141. if ( ! isset($this->{'_'.$this->request->method.'_args'}))
  142. {
  143. $this->{'_'.$this->request->method.'_args'} = array();
  144. }
  145. // Set up our GET variables
  146. $this->_get_args = array_merge($this->_get_args, $this->uri->ruri_to_assoc());
  147. //$this->load->library('security');
  148. // This library is bundled with REST_Controller 2.5+, but will eventually be part of CodeIgniter itself
  149. $this->load->library('format');
  150. // Try to find a format for the request (means we have a request body)
  151. $this->request->format = $this->_detect_input_format();
  152. // Some Methods cant have a body
  153. $this->request->body = NULL;
  154. $this->{'_parse_' . $this->request->method}();
  155. // Now we know all about our request, let's try and parse the body if it exists
  156. if ($this->request->format and $this->request->body)
  157. {
  158. $this->request->body = $this->format->factory($this->request->body, $this->request->format)->to_array();
  159. // Assign payload arguments to proper method container
  160. $this->{'_'.$this->request->method.'_args'} = $this->request->body;
  161. }
  162. // Merge both for one mega-args variable
  163. $this->_args = array_merge($this->_get_args, $this->_put_args, $this->_post_args, $this->_delete_args, $this->{'_'.$this->request->method.'_args'});
  164. // Which format should the data be returned in?
  165. $this->response = new stdClass();
  166. $this->response->format = $this->_detect_output_format();
  167. // Which format should the data be returned in?
  168. $this->response->lang = $this->_detect_lang();
  169. // Developers can extend this class and add a check in here
  170. $this->early_checks();
  171. // Check if there is a specific auth type for the current class/method
  172. $this->auth_override = $this->_auth_override_check();
  173. // When there is no specific override for the current class/method, use the default auth value set in the config
  174. if ($this->auth_override !== TRUE)
  175. {
  176. if ($this->config->item('rest_auth') == 'basic')
  177. {
  178. $this->_prepare_basic_auth();
  179. }
  180. elseif ($this->config->item('rest_auth') == 'digest')
  181. {
  182. $this->_prepare_digest_auth();
  183. }
  184. elseif ($this->config->item('rest_ip_whitelist_enabled'))
  185. {
  186. $this->_check_whitelist_auth();
  187. }
  188. }
  189. $this->rest = new StdClass();
  190. // Load DB if its enabled
  191. if (config_item('rest_database_group') AND (config_item('rest_enable_keys') OR config_item('rest_enable_logging')))
  192. {
  193. $this->rest->db = $this->load->database(config_item('rest_database_group'), TRUE);
  194. }
  195. // Use whatever database is in use (isset returns false)
  196. elseif (@$this->db)
  197. {
  198. $this->rest->db = $this->db;
  199. }
  200. // Checking for keys? GET TO WORK!
  201. if (config_item('rest_enable_keys'))
  202. {
  203. $this->_allow = $this->_detect_api_key();
  204. }
  205. // only allow ajax requests
  206. if ( ! $this->input->is_ajax_request() AND config_item('rest_ajax_only'))
  207. {
  208. $this->response(array('status' => false, 'error' => 'Only AJAX requests are accepted.'), 505);
  209. }
  210. }
  211. /**
  212. * Remap
  213. *
  214. * Requests are not made to methods directly, the request will be for
  215. * an "object". This simply maps the object and method to the correct
  216. * Controller method.
  217. *
  218. * @param string $object_called
  219. * @param array $arguments The arguments passed to the controller method.
  220. */
  221. public function _remap($object_called, $arguments)
  222. {
  223. // Should we answer if not over SSL?
  224. if (config_item('force_https') AND !$this->_detect_ssl())
  225. {
  226. $this->response(array('status' => false, 'error' => 'Unsupported protocol'), 403);
  227. }
  228. $pattern = '/^(.*)\.('.implode('|', array_keys($this->_supported_formats)).')$/';
  229. if (preg_match($pattern, $object_called, $matches))
  230. {
  231. $object_called = $matches[1];
  232. }
  233. $controller_method = $object_called.'_'.$this->request->method;
  234. // Do we want to log this method (if allowed by config)?
  235. $log_method = !(isset($this->methods[$controller_method]['log']) AND $this->methods[$controller_method]['log'] == FALSE);
  236. // Use keys for this method?
  237. $use_key = !(isset($this->methods[$controller_method]['key']) AND $this->methods[$controller_method]['key'] == FALSE);
  238. // Get that useless shitty key out of here
  239. if (config_item('rest_enable_keys') AND $use_key AND $this->_allow === FALSE)
  240. {
  241. if (config_item('rest_enable_logging') AND $log_method)
  242. {
  243. $this->_log_request();
  244. }
  245. $this->response(array('status' => false, 'error' => 'Invalid API Key.'), 403);
  246. }
  247. // Sure it exists, but can they do anything with it?
  248. if ( ! method_exists($this, $controller_method))
  249. {
  250. $this->response(array('status' => false, 'error' => 'Unknown method.'), 404);
  251. }
  252. // Doing key related stuff? Can only do it if they have a key right?
  253. if (config_item('rest_enable_keys') AND !empty($this->rest->key))
  254. {
  255. // Check the limit
  256. if (config_item('rest_enable_limits') AND !$this->_check_limit($controller_method))
  257. {
  258. $this->response(array('status' => false, 'error' => 'This API key has reached the hourly limit for this method.'), 401);
  259. }
  260. // If no level is set use 0, they probably aren't using permissions
  261. $level = isset($this->methods[$controller_method]['level']) ? $this->methods[$controller_method]['level'] : 0;
  262. // If no level is set, or it is lower than/equal to the key's level
  263. $authorized = $level <= $this->rest->level;
  264. // IM TELLIN!
  265. if (config_item('rest_enable_logging') AND $log_method)
  266. {
  267. $this->_log_request($authorized);
  268. }
  269. // They don't have good enough perms
  270. $authorized OR $this->response(array('status' => false, 'error' => 'This API key does not have enough permissions.'), 401);
  271. }
  272. // No key stuff, but record that stuff is happening
  273. else if (config_item('rest_enable_logging') AND $log_method)
  274. {
  275. $this->_log_request($authorized = TRUE);
  276. }
  277. // And...... GO!
  278. $this->_fire_method(array($this, $controller_method), $arguments);
  279. }
  280. /**
  281. * Fire Method
  282. *
  283. * Fires the designated controller method with the given arguments.
  284. *
  285. * @param array $method The controller method to fire
  286. * @param array $args The arguments to pass to the controller method
  287. */
  288. protected function _fire_method($method, $args)
  289. {
  290. call_user_func_array($method, $args);
  291. }
  292. /**
  293. * Response
  294. *
  295. * Takes pure data and optionally a status code, then creates the response.
  296. *
  297. * @param array $data
  298. * @param null|int $http_code
  299. */
  300. public function response($data = array(), $http_code = null)
  301. {
  302. global $CFG;
  303. // If data is empty and not code provide, error and bail
  304. if (empty($data) && $http_code === null)
  305. {
  306. $http_code = 404;
  307. // create the output variable here in the case of $this->response(array());
  308. $output = NULL;
  309. }
  310. // Otherwise (if no data but 200 provided) or some data, carry on camping!
  311. else
  312. {
  313. // Is compression requested?
  314. if ($CFG->item('compress_output') === TRUE && $this->_zlib_oc == FALSE)
  315. {
  316. if (extension_loaded('zlib'))
  317. {
  318. if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) AND strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE)
  319. {
  320. ob_start('ob_gzhandler');
  321. }
  322. }
  323. }
  324. is_numeric($http_code) OR $http_code = 200;
  325. // If the format method exists, call and return the output in that format
  326. if (method_exists($this, '_format_'.$this->response->format))
  327. {
  328. // Set the correct format header
  329. header('Content-Type: '.$this->_supported_formats[$this->response->format]);
  330. $output = $this->{'_format_'.$this->response->format}($data);
  331. }
  332. // If the format method exists, call and return the output in that format
  333. elseif (method_exists($this->format, 'to_'.$this->response->format))
  334. {
  335. // Set the correct format header
  336. header('Content-Type: '.$this->_supported_formats[$this->response->format]);
  337. $output = $this->format->factory($data)->{'to_'.$this->response->format}();
  338. }
  339. // Format not supported, output directly
  340. else
  341. {
  342. $output = $data;
  343. }
  344. }
  345. header('HTTP/1.1: ' . $http_code);
  346. header('Status: ' . $http_code);
  347. // If zlib.output_compression is enabled it will compress the output,
  348. // but it will not modify the content-length header to compensate for
  349. // the reduction, causing the browser to hang waiting for more data.
  350. // We'll just skip content-length in those cases.
  351. if ( ! $this->_zlib_oc && ! $CFG->item('compress_output'))
  352. {
  353. header('Content-Length: ' . strlen($output));
  354. }
  355. exit($output);
  356. }
  357. /*
  358. * Detect SSL use
  359. *
  360. * Detect whether SSL is being used or not
  361. */
  362. protected function _detect_ssl()
  363. {
  364. return (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == "on");
  365. }
  366. /*
  367. * Detect input format
  368. *
  369. * Detect which format the HTTP Body is provided in
  370. */
  371. protected function _detect_input_format()
  372. {
  373. if ($this->input->server('CONTENT_TYPE'))
  374. {
  375. // Check all formats against the HTTP_ACCEPT header
  376. foreach ($this->_supported_formats as $format => $mime)
  377. {
  378. if (strpos($match = $this->input->server('CONTENT_TYPE'), ';'))
  379. {
  380. $match = current(explode(';', $match));
  381. }
  382. if ($match == $mime)
  383. {
  384. return $format;
  385. }
  386. }
  387. }
  388. return NULL;
  389. }
  390. /**
  391. * Detect format
  392. *
  393. * Detect which format should be used to output the data.
  394. *
  395. * @return string The output format.
  396. */
  397. protected function _detect_output_format()
  398. {
  399. $pattern = '/\.('.implode('|', array_keys($this->_supported_formats)).')$/';
  400. // Check if a file extension is used
  401. if (preg_match($pattern, $this->uri->uri_string(), $matches))
  402. {
  403. return $matches[1];
  404. }
  405. // Check if a file extension is used
  406. elseif ($this->_get_args AND !is_array(end($this->_get_args)) AND preg_match($pattern, end($this->_get_args), $matches))
  407. {
  408. // The key of the last argument
  409. $last_key = end(array_keys($this->_get_args));
  410. // Remove the extension from arguments too
  411. $this->_get_args[$last_key] = preg_replace($pattern, '', $this->_get_args[$last_key]);
  412. $this->_args[$last_key] = preg_replace($pattern, '', $this->_args[$last_key]);
  413. return $matches[1];
  414. }
  415. // A format has been passed as an argument in the URL and it is supported
  416. if (isset($this->_get_args['format']) AND array_key_exists($this->_get_args['format'], $this->_supported_formats))
  417. {
  418. return $this->_get_args['format'];
  419. }
  420. // Otherwise, check the HTTP_ACCEPT (if it exists and we are allowed)
  421. if ($this->config->item('rest_ignore_http_accept') === FALSE AND $this->input->server('HTTP_ACCEPT'))
  422. {
  423. // Check all formats against the HTTP_ACCEPT header
  424. foreach (array_keys($this->_supported_formats) as $format)
  425. {
  426. // Has this format been requested?
  427. if (strpos($this->input->server('HTTP_ACCEPT'), $format) !== FALSE)
  428. {
  429. // If not HTML or XML assume its right and send it on its way
  430. if ($format != 'html' AND $format != 'xml')
  431. {
  432. return $format;
  433. }
  434. // HTML or XML have shown up as a match
  435. else
  436. {
  437. // If it is truly HTML, it wont want any XML
  438. if ($format == 'html' AND strpos($this->input->server('HTTP_ACCEPT'), 'xml') === FALSE)
  439. {
  440. return $format;
  441. }
  442. // If it is truly XML, it wont want any HTML
  443. elseif ($format == 'xml' AND strpos($this->input->server('HTTP_ACCEPT'), 'html') === FALSE)
  444. {
  445. return $format;
  446. }
  447. }
  448. }
  449. }
  450. } // End HTTP_ACCEPT checking
  451. // Well, none of that has worked! Let's see if the controller has a default
  452. if ( ! empty($this->rest_format))
  453. {
  454. return $this->rest_format;
  455. }
  456. // Just use the default format
  457. return config_item('rest_default_format');
  458. }
  459. /**
  460. * Detect method
  461. *
  462. * Detect which HTTP method is being used
  463. *
  464. * @return string
  465. */
  466. protected function _detect_method()
  467. {
  468. $method = strtolower($this->input->server('REQUEST_METHOD'));
  469. if ($this->config->item('enable_emulate_request'))
  470. {
  471. if ($this->input->post('_method'))
  472. {
  473. $method = strtolower($this->input->post('_method'));
  474. }
  475. elseif ($this->input->server('HTTP_X_HTTP_METHOD_OVERRIDE'))
  476. {
  477. $method = strtolower($this->input->server('HTTP_X_HTTP_METHOD_OVERRIDE'));
  478. }
  479. }
  480. if (in_array($method, $this->allowed_http_methods) && method_exists($this, '_parse_' . $method))
  481. {
  482. return $method;
  483. }
  484. return 'get';
  485. }
  486. /**
  487. * Detect API Key
  488. *
  489. * See if the user has provided an API key
  490. *
  491. * @return boolean
  492. */
  493. protected function _detect_api_key()
  494. {
  495. // Get the api key name variable set in the rest config file
  496. $api_key_variable = config_item('rest_key_name');
  497. // Work out the name of the SERVER entry based on config
  498. $key_name = 'HTTP_'.strtoupper(str_replace('-', '_', $api_key_variable));
  499. $this->rest->key = NULL;
  500. $this->rest->level = NULL;
  501. $this->rest->user_id = NULL;
  502. $this->rest->ignore_limits = FALSE;
  503. // Find the key from server or arguments
  504. if (($key = isset($this->_args[$api_key_variable]) ? $this->_args[$api_key_variable] : $this->input->server($key_name)))
  505. {
  506. if ( ! ($this->client = $this->rest->db->where(config_item('rest_key_column'), $key)->get(config_item('rest_keys_table'))->row()))
  507. {
  508. return FALSE;
  509. }
  510. $this->rest->key = $this->client->{config_item('rest_key_column')};
  511. /*
  512. isset($row->user_id) AND $this->rest->user_id = $row->user_id;
  513. isset($row->level) AND $this->rest->level = $row->level;
  514. isset($row->ignore_limits) AND $this->rest->ignore_limits = $row->ignore_limits;
  515. */
  516. return $this->client;
  517. }
  518. // No key has been sent
  519. return FALSE;
  520. }
  521. /**
  522. * Detect language(s)
  523. *
  524. * What language do they want it in?
  525. *
  526. * @return null|string The language code.
  527. */
  528. protected function _detect_lang()
  529. {
  530. if ( ! $lang = $this->input->server('HTTP_ACCEPT_LANGUAGE'))
  531. {
  532. return NULL;
  533. }
  534. // They might have sent a few, make it an array
  535. if (strpos($lang, ',') !== FALSE)
  536. {
  537. $langs = explode(',', $lang);
  538. $return_langs = array();
  539. $i = 1;
  540. foreach ($langs as $lang)
  541. {
  542. // Remove weight and strip space
  543. list($lang) = explode(';', $lang);
  544. $return_langs[] = trim($lang);
  545. }
  546. return $return_langs;
  547. }
  548. // Nope, just return the string
  549. return $lang;
  550. }
  551. /**
  552. * Log request
  553. *
  554. * Record the entry for awesomeness purposes
  555. *
  556. * @param boolean $authorized
  557. * @return object
  558. */
  559. protected function _log_request($authorized = FALSE)
  560. {
  561. return $this->rest->db->insert(config_item('rest_logs_table'), array(
  562. 'uri' => $this->uri->uri_string(),
  563. 'method' => $this->request->method,
  564. 'params' => $this->_args ? serialize($this->_args) : null,
  565. 'api_key' => isset($this->rest->key) ? $this->rest->key : '',
  566. 'ip_address' => $this->input->ip_address(),
  567. 'time' => function_exists('now') ? now() : time(),
  568. 'authorized' => $authorized
  569. ));
  570. }
  571. /**
  572. * Limiting requests
  573. *
  574. * Check if the requests are coming in a tad too fast.
  575. *
  576. * @param string $controller_method The method being called.
  577. * @return boolean
  578. */
  579. protected function _check_limit($controller_method)
  580. {
  581. // They are special, or it might not even have a limit
  582. if ( ! empty($this->rest->ignore_limits) OR !isset($this->methods[$controller_method]['limit']))
  583. {
  584. // On your way sonny-jim.
  585. return TRUE;
  586. }
  587. // How many times can you get to this method an hour?
  588. $limit = $this->methods[$controller_method]['limit'];
  589. // Get data on a keys usage
  590. $result = $this->rest->db
  591. ->where('uri', $this->uri->uri_string())
  592. ->where('api_key', $this->rest->key)
  593. ->get(config_item('rest_limits_table'))
  594. ->row();
  595. // No calls yet, or been an hour since they called
  596. if ( ! $result OR $result->hour_started < time() - (60 * 60))
  597. {
  598. // Right, set one up from scratch
  599. $this->rest->db->insert(config_item('rest_limits_table'), array(
  600. 'uri' => $this->uri->uri_string(),
  601. 'api_key' => isset($this->rest->key) ? $this->rest->key : '',
  602. 'count' => 1,
  603. 'hour_started' => time()
  604. ));
  605. }
  606. // They have called within the hour, so lets update
  607. else
  608. {
  609. // Your luck is out, you've called too many times!
  610. if ($result->count >= $limit)
  611. {
  612. return FALSE;
  613. }
  614. $this->rest->db
  615. ->where('uri', $this->uri->uri_string())
  616. ->where('api_key', $this->rest->key)
  617. ->set('count', 'count + 1', FALSE)
  618. ->update(config_item('rest_limits_table'));
  619. }
  620. return TRUE;
  621. }
  622. /**
  623. * Auth override check
  624. *
  625. * Check if there is a specific auth type set for the current class/method
  626. * being called.
  627. *
  628. * @return boolean
  629. */
  630. protected function _auth_override_check()
  631. {
  632. // Assign the class/method auth type override array from the config
  633. $this->overrides_array = $this->config->item('auth_override_class_method');
  634. // Check to see if the override array is even populated, otherwise return false
  635. if (empty($this->overrides_array))
  636. {
  637. return false;
  638. }
  639. // Check to see if there's an override value set for the current class/method being called
  640. if (empty($this->overrides_array[$this->router->class][$this->router->method]))
  641. {
  642. return false;
  643. }
  644. // None auth override found, prepare nothing but send back a true override flag
  645. if ($this->overrides_array[$this->router->class][$this->router->method] == 'none')
  646. {
  647. return true;
  648. }
  649. // Basic auth override found, prepare basic
  650. if ($this->overrides_array[$this->router->class][$this->router->method] == 'basic')
  651. {
  652. $this->_prepare_basic_auth();
  653. return true;
  654. }
  655. // Digest auth override found, prepare digest
  656. if ($this->overrides_array[$this->router->class][$this->router->method] == 'digest')
  657. {
  658. $this->_prepare_digest_auth();
  659. return true;
  660. }
  661. // Whitelist auth override found, check client's ip against config whitelist
  662. if ($this->overrides_array[$this->router->class][$this->router->method] == 'whitelist')
  663. {
  664. $this->_check_whitelist_auth();
  665. return true;
  666. }
  667. // Return false when there is an override value set but it does not match
  668. // 'basic', 'digest', or 'none'. (the value was misspelled)
  669. return false;
  670. }
  671. /**
  672. * Parse GET
  673. */
  674. protected function _parse_get()
  675. {
  676. // Grab proper GET variables
  677. parse_str(parse_url($_SERVER['REQUEST_URI'], PHP_URL_QUERY), $get);
  678. // Merge both the URI segments and GET params
  679. $this->_get_args = array_merge($this->_get_args, $get);
  680. }
  681. /**
  682. * Parse POST
  683. */
  684. protected function _parse_post()
  685. {
  686. $this->_post_args = $_POST;
  687. $this->request->format and $this->request->body = file_get_contents('php://input');
  688. }
  689. /**
  690. * Parse PUT
  691. */
  692. protected function _parse_put()
  693. {
  694. // It might be a HTTP body
  695. if ($this->request->format)
  696. {
  697. $this->request->body = file_get_contents('php://input');
  698. }
  699. // If no file type is provided, this is probably just arguments
  700. else
  701. {
  702. parse_str(file_get_contents('php://input'), $this->_put_args);
  703. }
  704. }
  705. /**
  706. * Parse DELETE
  707. */
  708. protected function _parse_delete()
  709. {
  710. // Set up out DELETE variables (which shouldn't really exist, but sssh!)
  711. parse_str(file_get_contents('php://input'), $this->_delete_args);
  712. }
  713. // INPUT FUNCTION --------------------------------------------------------------
  714. /**
  715. * Retrieve a value from the GET request arguments.
  716. *
  717. * @param string $key The key for the GET request argument to retrieve
  718. * @param boolean $xss_clean Whether the value should be XSS cleaned or not.
  719. * @return string The GET argument value.
  720. */
  721. public function get($key = NULL, $xss_clean = TRUE)
  722. {
  723. if ($key === NULL)
  724. {
  725. return $this->_get_args;
  726. }
  727. return array_key_exists($key, $this->_get_args) ? $this->_xss_clean($this->_get_args[$key], $xss_clean) : FALSE;
  728. }
  729. /**
  730. * Retrieve a value from the POST request arguments.
  731. *
  732. * @param string $key The key for the POST request argument to retrieve
  733. * @param boolean $xss_clean Whether the value should be XSS cleaned or not.
  734. * @return string The POST argument value.
  735. */
  736. public function post($key = NULL, $xss_clean = TRUE)
  737. {
  738. if ($key === NULL)
  739. {
  740. return $this->_post_args;
  741. }
  742. return array_key_exists($key, $this->_post_args) ? $this->_xss_clean($this->_post_args[$key], $xss_clean) : FALSE;
  743. }
  744. /**
  745. * Retrieve a value from the PUT request arguments.
  746. *
  747. * @param string $key The key for the PUT request argument to retrieve
  748. * @param boolean $xss_clean Whether the value should be XSS cleaned or not.
  749. * @return string The PUT argument value.
  750. */
  751. public function put($key = NULL, $xss_clean = TRUE)
  752. {
  753. if ($key === NULL)
  754. {
  755. return $this->_put_args;
  756. }
  757. return array_key_exists($key, $this->_put_args) ? $this->_xss_clean($this->_put_args[$key], $xss_clean) : FALSE;
  758. }
  759. /**
  760. * Retrieve a value from the DELETE request arguments.
  761. *
  762. * @param string $key The key for the DELETE request argument to retrieve
  763. * @param boolean $xss_clean Whether the value should be XSS cleaned or not.
  764. * @return string The DELETE argument value.
  765. */
  766. public function delete($key = NULL, $xss_clean = TRUE)
  767. {
  768. if ($key === NULL)
  769. {
  770. return $this->_delete_args;
  771. }
  772. return array_key_exists($key, $this->_delete_args) ? $this->_xss_clean($this->_delete_args[$key], $xss_clean) : FALSE;
  773. }
  774. /**
  775. * Process to protect from XSS attacks.
  776. *
  777. * @param string $val The input.
  778. * @param boolean $process Do clean or note the input.
  779. * @return string
  780. */
  781. protected function _xss_clean($val, $process)
  782. {
  783. if (CI_VERSION < 2)
  784. {
  785. return $process ? $this->input->xss_clean($val) : $val;
  786. }
  787. return $process ? $this->security->xss_clean($val) : $val;
  788. }
  789. /**
  790. * Retrieve the validation errors.
  791. *
  792. * @return array
  793. */
  794. public function validation_errors()
  795. {
  796. $string = strip_tags($this->form_validation->error_string());
  797. return explode("\n", trim($string, "\n"));
  798. }
  799. // SECURITY FUNCTIONS ---------------------------------------------------------
  800. /**
  801. * Check if the user is logged in.
  802. *
  803. * @param string $username The user's name
  804. * @param string $password The user's password
  805. * @return boolean
  806. */
  807. protected function _check_login($username = '', $password = NULL)
  808. {
  809. if (empty($username))
  810. {
  811. return FALSE;
  812. }
  813. $valid_logins = & $this->config->item('rest_valid_logins');
  814. if ( ! array_key_exists($username, $valid_logins))
  815. {
  816. return FALSE;
  817. }
  818. // If actually NULL (not empty string) then do not check it
  819. if ($password !== NULL AND $valid_logins[$username] != $password)
  820. {
  821. return FALSE;
  822. }
  823. return TRUE;
  824. }
  825. /**
  826. * @todo document this.
  827. */
  828. protected function _prepare_basic_auth()
  829. {
  830. // If whitelist is enabled it has the first chance to kick them out
  831. if (config_item('rest_ip_whitelist_enabled'))
  832. {
  833. $this->_check_whitelist_auth();
  834. }
  835. $username = NULL;
  836. $password = NULL;
  837. // mod_php
  838. if ($this->input->server('PHP_AUTH_USER'))
  839. {
  840. $username = $this->input->server('PHP_AUTH_USER');
  841. $password = $this->input->server('PHP_AUTH_PW');
  842. }
  843. // most other servers
  844. elseif ($this->input->server('HTTP_AUTHENTICATION'))
  845. {
  846. if (strpos(strtolower($this->input->server('HTTP_AUTHENTICATION')), 'basic') === 0)
  847. {
  848. list($username, $password) = explode(':', base64_decode(substr($this->input->server('HTTP_AUTHORIZATION'), 6)));
  849. }
  850. }
  851. if ( ! $this->_check_login($username, $password))
  852. {
  853. $this->_force_login();
  854. }
  855. }
  856. /**
  857. * @todo Document this.
  858. */
  859. protected function _prepare_digest_auth()
  860. {
  861. // If whitelist is enabled it has the first chance to kick them out
  862. if (config_item('rest_ip_whitelist_enabled'))
  863. {
  864. $this->_check_whitelist_auth();
  865. }
  866. $uniqid = uniqid(""); // Empty argument for backward compatibility
  867. // We need to test which server authentication variable to use
  868. // because the PHP ISAPI module in IIS acts different from CGI
  869. if ($this->input->server('PHP_AUTH_DIGEST'))
  870. {
  871. $digest_string = $this->input->server('PHP_AUTH_DIGEST');
  872. }
  873. elseif ($this->input->server('HTTP_AUTHORIZATION'))
  874. {
  875. $digest_string = $this->input->server('HTTP_AUTHORIZATION');
  876. }
  877. else
  878. {
  879. $digest_string = "";
  880. }
  881. // The $_SESSION['error_prompted'] variable is used to ask the password
  882. // again if none given or if the user enters wrong auth information.
  883. if (empty($digest_string))
  884. {
  885. $this->_force_login($uniqid);
  886. }
  887. // We need to retrieve authentication informations from the $auth_data variable
  888. preg_match_all('@(username|nonce|uri|nc|cnonce|qop|response)=[\'"]?([^\'",]+)@', $digest_string, $matches);
  889. $digest = array_combine($matches[1], $matches[2]);
  890. if ( ! array_key_exists('username', $digest) OR !$this->_check_login($digest['username']))
  891. {
  892. $this->_force_login($uniqid);
  893. }
  894. $valid_logins = & $this->config->item('rest_valid_logins');
  895. $valid_pass = $valid_logins[$digest['username']];
  896. // This is the valid response expected
  897. $A1 = md5($digest['username'].':'.$this->config->item('rest_realm').':'.$valid_pass);
  898. $A2 = md5(strtoupper($this->request->method).':'.$digest['uri']);
  899. $valid_response = md5($A1.':'.$digest['nonce'].':'.$digest['nc'].':'.$digest['cnonce'].':'.$digest['qop'].':'.$A2);
  900. if ($digest['response'] != $valid_response)
  901. {
  902. header('HTTP/1.0 401 Unauthorized');
  903. header('HTTP/1.1 401 Unauthorized');
  904. exit;
  905. }
  906. }
  907. /**
  908. * Check if the client's ip is in the 'rest_ip_whitelist' config
  909. */
  910. protected function _check_whitelist_auth()
  911. {
  912. $whitelist = explode(',', config_item('rest_ip_whitelist'));
  913. array_push($whitelist, '127.0.0.1', '0.0.0.0');
  914. foreach ($whitelist AS &$ip)
  915. {
  916. $ip = trim($ip);
  917. }
  918. if ( ! in_array($this->input->ip_address(), $whitelist))
  919. {
  920. $this->response(array('status' => false, 'error' => 'Not authorized'), 401);
  921. }
  922. }
  923. /**
  924. * @todo Document this.
  925. *
  926. * @param string $nonce
  927. */
  928. protected function _force_login($nonce = '')
  929. {
  930. if ($this->config->item('rest_auth') == 'basic')
  931. {
  932. header('WWW-Authenticate: Basic realm="'.$this->config->item('rest_realm').'"');
  933. }
  934. elseif ($this->config->item('rest_auth') == 'digest')
  935. {
  936. header('WWW-Authenticate: Digest realm="'.$this->config->item('rest_realm').'", qop="auth", nonce="'.$nonce.'", opaque="'.md5($this->config->item('rest_realm')).'"');
  937. }
  938. $this->response(array('status' => false, 'error' => 'Not authorized'), 401);
  939. }
  940. /**
  941. * Force it into an array
  942. *
  943. * @param object|array $data
  944. * @return array
  945. */
  946. protected function _force_loopable($data)
  947. {
  948. // Force it to be something useful
  949. if ( ! is_array($data) AND !is_object($data))
  950. {
  951. $data = (array) $data;
  952. }
  953. return $data;
  954. }
  955. // FORMATING FUNCTIONS ---------------------------------------------------------
  956. // Many of these have been moved to the Format class for better separation, but these methods will be checked too
  957. /**
  958. * Encode as JSONP
  959. *
  960. * @param array $data The input data.
  961. * @return string The JSONP data string (loadable from Javascript).
  962. */
  963. protected function _format_jsonp($data = array())
  964. {
  965. return $this->get('callback').'('.json_encode($data).')';
  966. }
  967. }