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

/application/core/CMS_REST_Controller.php

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