PageRenderTime 51ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 1ms

/application/libraries/REST_Controller.php

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