You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2165 lines
72 KiB

7 years ago
  1. <?php
  2. defined('BASEPATH') OR exit('No direct script access allowed');
  3. /**
  4. * CodeIgniter Rest Controller
  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, Chris Kacerguis
  11. * @license MIT
  12. * @link https://github.com/chriskacerguis/codeigniter-restserver
  13. * @version 3.0.0
  14. */
  15. abstract class REST_Controller extends CI_Controller {
  16. // Note: Only the widely used HTTP status codes are documented
  17. // Informational
  18. const HTTP_CONTINUE = 100;
  19. const HTTP_SWITCHING_PROTOCOLS = 101;
  20. const HTTP_PROCESSING = 102; // RFC2518
  21. // Success
  22. /**
  23. * The request has succeeded
  24. */
  25. const HTTP_OK = 200;
  26. /**
  27. * The server successfully created a new resource
  28. */
  29. const HTTP_CREATED = 201;
  30. const HTTP_ACCEPTED = 202;
  31. const HTTP_NON_AUTHORITATIVE_INFORMATION = 203;
  32. /**
  33. * The server successfully processed the request, though no content is returned
  34. */
  35. const HTTP_NO_CONTENT = 204;
  36. const HTTP_RESET_CONTENT = 205;
  37. const HTTP_PARTIAL_CONTENT = 206;
  38. const HTTP_MULTI_STATUS = 207; // RFC4918
  39. const HTTP_ALREADY_REPORTED = 208; // RFC5842
  40. const HTTP_IM_USED = 226; // RFC3229
  41. // Redirection
  42. const HTTP_MULTIPLE_CHOICES = 300;
  43. const HTTP_MOVED_PERMANENTLY = 301;
  44. const HTTP_FOUND = 302;
  45. const HTTP_SEE_OTHER = 303;
  46. /**
  47. * The resource has not been modified since the last request
  48. */
  49. const HTTP_NOT_MODIFIED = 304;
  50. const HTTP_USE_PROXY = 305;
  51. const HTTP_RESERVED = 306;
  52. const HTTP_TEMPORARY_REDIRECT = 307;
  53. const HTTP_PERMANENTLY_REDIRECT = 308; // RFC7238
  54. // Client Error
  55. /**
  56. * The request cannot be fulfilled due to multiple errors
  57. */
  58. const HTTP_BAD_REQUEST = 400;
  59. /**
  60. * The user is unauthorized to access the requested resource
  61. */
  62. const HTTP_UNAUTHORIZED = 401;
  63. const HTTP_PAYMENT_REQUIRED = 402;
  64. /**
  65. * The requested resource is unavailable at this present time
  66. */
  67. const HTTP_FORBIDDEN = 403;
  68. /**
  69. * The requested resource could not be found
  70. *
  71. * Note: This is sometimes used to mask if there was an UNAUTHORIZED (401) or
  72. * FORBIDDEN (403) error, for security reasons
  73. */
  74. const HTTP_NOT_FOUND = 404;
  75. /**
  76. * The request method is not supported by the following resource
  77. */
  78. const HTTP_METHOD_NOT_ALLOWED = 405;
  79. /**
  80. * The request was not acceptable
  81. */
  82. const HTTP_NOT_ACCEPTABLE = 406;
  83. const HTTP_PROXY_AUTHENTICATION_REQUIRED = 407;
  84. const HTTP_REQUEST_TIMEOUT = 408;
  85. /**
  86. * The request could not be completed due to a conflict with the current state
  87. * of the resource
  88. */
  89. const HTTP_CONFLICT = 409;
  90. const HTTP_GONE = 410;
  91. const HTTP_LENGTH_REQUIRED = 411;
  92. const HTTP_PRECONDITION_FAILED = 412;
  93. const HTTP_REQUEST_ENTITY_TOO_LARGE = 413;
  94. const HTTP_REQUEST_URI_TOO_LONG = 414;
  95. const HTTP_UNSUPPORTED_MEDIA_TYPE = 415;
  96. const HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
  97. const HTTP_EXPECTATION_FAILED = 417;
  98. const HTTP_I_AM_A_TEAPOT = 418; // RFC2324
  99. const HTTP_UNPROCESSABLE_ENTITY = 422; // RFC4918
  100. const HTTP_LOCKED = 423; // RFC4918
  101. const HTTP_FAILED_DEPENDENCY = 424; // RFC4918
  102. const HTTP_RESERVED_FOR_WEBDAV_ADVANCED_COLLECTIONS_EXPIRED_PROPOSAL = 425; // RFC2817
  103. const HTTP_UPGRADE_REQUIRED = 426; // RFC2817
  104. const HTTP_PRECONDITION_REQUIRED = 428; // RFC6585
  105. const HTTP_TOO_MANY_REQUESTS = 429; // RFC6585
  106. const HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE = 431; // RFC6585
  107. // Server Error
  108. /**
  109. * The server encountered an unexpected error
  110. *
  111. * Note: This is a generic error message when no specific message
  112. * is suitable
  113. */
  114. const HTTP_INTERNAL_SERVER_ERROR = 500;
  115. /**
  116. * The server does not recognise the request method
  117. */
  118. const HTTP_NOT_IMPLEMENTED = 501;
  119. const HTTP_BAD_GATEWAY = 502;
  120. const HTTP_SERVICE_UNAVAILABLE = 503;
  121. const HTTP_GATEWAY_TIMEOUT = 504;
  122. const HTTP_VERSION_NOT_SUPPORTED = 505;
  123. const HTTP_VARIANT_ALSO_NEGOTIATES_EXPERIMENTAL = 506; // RFC2295
  124. const HTTP_INSUFFICIENT_STORAGE = 507; // RFC4918
  125. const HTTP_LOOP_DETECTED = 508; // RFC5842
  126. const HTTP_NOT_EXTENDED = 510; // RFC2774
  127. const HTTP_NETWORK_AUTHENTICATION_REQUIRED = 511;
  128. /**
  129. * This defines the rest format
  130. * Must be overridden it in a controller so that it is set
  131. *
  132. * @var string|NULL
  133. */
  134. protected $rest_format = NULL;
  135. /**
  136. * Defines the list of method properties such as limit, log and level
  137. *
  138. * @var array
  139. */
  140. protected $methods = [];
  141. /**
  142. * List of allowed HTTP methods
  143. *
  144. * @var array
  145. */
  146. protected $allowed_http_methods = ['get', 'delete', 'post', 'put', 'options', 'patch', 'head'];
  147. /**
  148. * Contains details about the request
  149. * Fields: body, format, method, ssl
  150. * Note: This is a dynamic object (stdClass)
  151. *
  152. * @var object
  153. */
  154. protected $request = NULL;
  155. /**
  156. * Contains details about the response
  157. * Fields: format, lang
  158. * Note: This is a dynamic object (stdClass)
  159. *
  160. * @var object
  161. */
  162. protected $response = NULL;
  163. /**
  164. * Contains details about the REST API
  165. * Fields: db, ignore_limits, key, level, user_id
  166. * Note: This is a dynamic object (stdClass)
  167. *
  168. * @var object
  169. */
  170. protected $rest = NULL;
  171. /**
  172. * The arguments for the GET request method
  173. *
  174. * @var array
  175. */
  176. protected $_get_args = [];
  177. /**
  178. * The arguments for the POST request method
  179. *
  180. * @var array
  181. */
  182. protected $_post_args = [];
  183. /**
  184. * The arguments for the PUT request method
  185. *
  186. * @var array
  187. */
  188. protected $_put_args = [];
  189. /**
  190. * The arguments for the DELETE request method
  191. *
  192. * @var array
  193. */
  194. protected $_delete_args = [];
  195. /**
  196. * The arguments for the PATCH request method
  197. *
  198. * @var array
  199. */
  200. protected $_patch_args = [];
  201. /**
  202. * The arguments for the HEAD request method
  203. *
  204. * @var array
  205. */
  206. protected $_head_args = [];
  207. /**
  208. * The arguments for the OPTIONS request method
  209. *
  210. * @var array
  211. */
  212. protected $_options_args = [];
  213. /**
  214. * The arguments for the query parameters
  215. *
  216. * @var array
  217. */
  218. protected $_query_args = [];
  219. /**
  220. * The arguments from GET, POST, PUT, DELETE, PATCH, HEAD and OPTIONS request methods combined
  221. *
  222. * @var array
  223. */
  224. protected $_args = [];
  225. /**
  226. * The insert_id of the log entry (if we have one)
  227. *
  228. * @var string
  229. */
  230. protected $_insert_id = '';
  231. /**
  232. * If the request is allowed based on the API key provided
  233. *
  234. * @var bool
  235. */
  236. protected $_allow = TRUE;
  237. /**
  238. * The LDAP Distinguished Name of the User post authentication
  239. *
  240. * @var string
  241. */
  242. protected $_user_ldap_dn = '';
  243. /**
  244. * The start of the response time from the server
  245. *
  246. * @var string
  247. */
  248. protected $_start_rtime = '';
  249. /**
  250. * The end of the response time from the server
  251. *
  252. * @var string
  253. */
  254. protected $_end_rtime = '';
  255. /**
  256. * List all supported methods, the first will be the default format
  257. *
  258. * @var array
  259. */
  260. protected $_supported_formats = [
  261. 'json' => 'application/json',
  262. 'array' => 'application/json',
  263. 'csv' => 'application/csv',
  264. 'html' => 'text/html',
  265. 'jsonp' => 'application/javascript',
  266. 'php' => 'text/plain',
  267. 'serialized' => 'application/vnd.php.serialized',
  268. 'xml' => 'application/xml'
  269. ];
  270. /**
  271. * Information about the current API user
  272. *
  273. * @var object
  274. */
  275. protected $_apiuser;
  276. /**
  277. * Enable XSS flag
  278. * Determines whether the XSS filter is always active when
  279. * GET, OPTIONS, HEAD, POST, PUT, DELETE and PATCH data is encountered
  280. * Set automatically based on config setting
  281. *
  282. * @var bool
  283. */
  284. protected $_enable_xss = FALSE;
  285. /**
  286. * HTTP status codes and their respective description
  287. * Note: Only the widely used HTTP status codes are used
  288. *
  289. * @var array
  290. * @link http://www.restapitutorial.com/httpstatuscodes.html
  291. */
  292. protected $http_status_codes = [
  293. self::HTTP_OK => 'OK',
  294. self::HTTP_CREATED => 'CREATED',
  295. self::HTTP_NO_CONTENT => 'NO CONTENT',
  296. self::HTTP_NOT_MODIFIED => 'NOT MODIFIED',
  297. self::HTTP_BAD_REQUEST => 'BAD REQUEST',
  298. self::HTTP_UNAUTHORIZED => 'UNAUTHORIZED',
  299. self::HTTP_FORBIDDEN => 'FORBIDDEN',
  300. self::HTTP_NOT_FOUND => 'NOT FOUND',
  301. self::HTTP_METHOD_NOT_ALLOWED => 'METHOD NOT ALLOWED',
  302. self::HTTP_NOT_ACCEPTABLE => 'NOT ACCEPTABLE',
  303. self::HTTP_CONFLICT => 'CONFLICT',
  304. self::HTTP_INTERNAL_SERVER_ERROR => 'INTERNAL SERVER ERROR',
  305. self::HTTP_NOT_IMPLEMENTED => 'NOT IMPLEMENTED'
  306. ];
  307. /**
  308. * Extend this function to apply additional checking early on in the process
  309. *
  310. * @access protected
  311. * @return void
  312. */
  313. protected function early_checks()
  314. {
  315. }
  316. /**
  317. * Constructor for the REST API
  318. *
  319. * @access public
  320. * @param string $config Configuration filename minus the file extension
  321. * e.g: my_rest.php is passed as 'my_rest'
  322. * @return void
  323. */
  324. public function __construct($config = 'rest')
  325. {
  326. parent::__construct();
  327. // Disable XML Entity (security vulnerability)
  328. libxml_disable_entity_loader(TRUE);
  329. // Check to see if PHP is equal to or greater than 5.4.x
  330. if (is_php('5.4') === FALSE)
  331. {
  332. // CodeIgniter 3 is recommended for v5.4 or above
  333. throw new Exception('Using PHP v' . PHP_VERSION . ', though PHP v5.4 or greater is required');
  334. }
  335. // Check to see if this is CI 3.x
  336. if (explode('.', CI_VERSION, 2)[0] < 3)
  337. {
  338. throw new Exception('REST Server requires CodeIgniter 3.x');
  339. }
  340. // Set the default value of global xss filtering. Same approach as CodeIgniter 3
  341. $this->_enable_xss = ($this->config->item('global_xss_filtering') === TRUE);
  342. // Don't try to parse template variables like {elapsed_time} and {memory_usage}
  343. // when output is displayed for not damaging data accidentally
  344. $this->output->parse_exec_vars = FALSE;
  345. // Start the timer for how long the request takes
  346. $this->_start_rtime = microtime(TRUE);
  347. // Load the rest.php configuration file
  348. $this->load->config($config);
  349. // At present the library is bundled with REST_Controller 2.5+, but will eventually be part of CodeIgniter (no citation)
  350. $this->load->library('format');
  351. // Determine supported output formats from configuration
  352. $supported_formats = $this->config->item('rest_supported_formats');
  353. // Validate the configuration setting output formats
  354. if (empty($supported_formats))
  355. {
  356. $supported_formats = [];
  357. }
  358. if (!is_array($supported_formats))
  359. {
  360. $supported_formats = [$supported_formats];
  361. }
  362. // Add silently the default output format if it is missing
  363. $default_format = $this->_get_default_output_format();
  364. if (!in_array($default_format, $supported_formats))
  365. {
  366. $supported_formats[] = $default_format;
  367. }
  368. // Now update $this->_supported_formats
  369. $this->_supported_formats = array_intersect_key($this->_supported_formats, array_flip($supported_formats));
  370. // Get the language
  371. $language = $this->config->item('rest_language');
  372. if ($language === NULL)
  373. {
  374. $language = 'english';
  375. }
  376. // Load the language file
  377. $this->lang->load('rest_controller', $language);
  378. // Initialise the response, request and rest objects
  379. $this->request = new stdClass();
  380. $this->response = new stdClass();
  381. $this->rest = new stdClass();
  382. // Check to see if the current IP address is blacklisted
  383. if ($this->config->item('rest_ip_blacklist_enabled') === TRUE)
  384. {
  385. $this->_check_blacklist_auth();
  386. }
  387. // Determine whether the connection is HTTPS
  388. $this->request->ssl = is_https();
  389. // How is this request being made? GET, POST, PATCH, DELETE, INSERT, PUT, HEAD or OPTIONS
  390. $this->request->method = $this->_detect_method();
  391. // Create an argument container if it doesn't exist e.g. _get_args
  392. if (isset($this->{'_' . $this->request->method . '_args'}) === FALSE)
  393. {
  394. $this->{'_' . $this->request->method . '_args'} = [];
  395. }
  396. // Set up the query parameters
  397. $this->_parse_query();
  398. // Set up the GET variables
  399. $this->_get_args = array_merge($this->_get_args, $this->uri->ruri_to_assoc());
  400. // Try to find a format for the request (means we have a request body)
  401. $this->request->format = $this->_detect_input_format();
  402. // Not all methods have a body attached with them
  403. $this->request->body = NULL;
  404. $this->{'_parse_' . $this->request->method}();
  405. // Now we know all about our request, let's try and parse the body if it exists
  406. if ($this->request->format && $this->request->body)
  407. {
  408. $this->request->body = $this->format->factory($this->request->body, $this->request->format)->to_array();
  409. // Assign payload arguments to proper method container
  410. $this->{'_' . $this->request->method . '_args'} = $this->request->body;
  411. }
  412. // Merge both for one mega-args variable
  413. $this->_args = array_merge(
  414. $this->_get_args,
  415. $this->_options_args,
  416. $this->_patch_args,
  417. $this->_head_args,
  418. $this->_put_args,
  419. $this->_post_args,
  420. $this->_delete_args,
  421. $this->{'_' . $this->request->method . '_args'}
  422. );
  423. // Which format should the data be returned in?
  424. $this->response->format = $this->_detect_output_format();
  425. // Which language should the data be returned in?
  426. $this->response->lang = $this->_detect_lang();
  427. // Extend this function to apply additional checking early on in the process
  428. $this->early_checks();
  429. // Load DB if its enabled
  430. if ($this->config->item('rest_database_group') && ($this->config->item('rest_enable_keys') || $this->config->item('rest_enable_logging')))
  431. {
  432. $this->rest->db = $this->load->database($this->config->item('rest_database_group'), TRUE);
  433. }
  434. // Use whatever database is in use (isset returns FALSE)
  435. elseif (property_exists($this, 'db'))
  436. {
  437. $this->rest->db = $this->db;
  438. }
  439. // Check if there is a specific auth type for the current class/method
  440. // _auth_override_check could exit so we need $this->rest->db initialized before
  441. $this->auth_override = $this->_auth_override_check();
  442. // Checking for keys? GET TO WorK!
  443. // Skip keys test for $config['auth_override_class_method']['class'['method'] = 'none'
  444. if ($this->config->item('rest_enable_keys') && $this->auth_override !== TRUE)
  445. {
  446. $this->_allow = $this->_detect_api_key();
  447. }
  448. // Only allow ajax requests
  449. if ($this->input->is_ajax_request() === FALSE && $this->config->item('rest_ajax_only'))
  450. {
  451. // Display an error response
  452. $this->response([
  453. $this->config->item('rest_status_field_name') => FALSE,
  454. $this->config->item('rest_message_field_name') => $this->lang->line('text_rest_ajax_only')
  455. ], self::HTTP_NOT_ACCEPTABLE);
  456. }
  457. // When there is no specific override for the current class/method, use the default auth value set in the config
  458. if ($this->auth_override === FALSE && !($this->config->item('rest_enable_keys') && $this->_allow === TRUE) || ($this->config->item('allow_auth_and_keys') === TRUE && $this->_allow === TRUE))
  459. {
  460. $rest_auth = strtolower($this->config->item('rest_auth'));
  461. switch ($rest_auth)
  462. {
  463. case 'basic':
  464. $this->_prepare_basic_auth();
  465. break;
  466. case 'digest':
  467. $this->_prepare_digest_auth();
  468. break;
  469. case 'session':
  470. $this->_check_php_session();
  471. break;
  472. }
  473. if ($this->config->item('rest_ip_whitelist_enabled') === TRUE)
  474. {
  475. $this->_check_whitelist_auth();
  476. }
  477. }
  478. }
  479. /**
  480. * Deconstructor
  481. *
  482. * @author Chris Kacerguis
  483. * @access public
  484. * @return void
  485. */
  486. public function __destruct()
  487. {
  488. // Get the current timestamp
  489. $this->_end_rtime = microtime(TRUE);
  490. // Log the loading time to the log table
  491. if ($this->config->item('rest_enable_logging') === TRUE)
  492. {
  493. $this->_log_access_time();
  494. }
  495. }
  496. /**
  497. * Requests are not made to methods directly, the request will be for
  498. * an "object". This simply maps the object and method to the correct
  499. * Controller method
  500. *
  501. * @access public
  502. * @param string $object_called
  503. * @param array $arguments The arguments passed to the controller method
  504. */
  505. public function _remap($object_called, $arguments = [])
  506. {
  507. // Should we answer if not over SSL?
  508. if ($this->config->item('force_https') && $this->request->ssl === FALSE)
  509. {
  510. $this->response([
  511. $this->config->item('rest_status_field_name') => FALSE,
  512. $this->config->item('rest_message_field_name') => $this->lang->line('text_rest_unsupported')
  513. ], self::HTTP_FORBIDDEN);
  514. }
  515. // Remove the supported format from the function name e.g. index.json => index
  516. $object_called = preg_replace('/^(.*)\.(?:' . implode('|', array_keys($this->_supported_formats)) . ')$/', '$1', $object_called);
  517. $controller_method = $object_called . '_' . $this->request->method;
  518. // Do we want to log this method (if allowed by config)?
  519. $log_method = !(isset($this->methods[$controller_method]['log']) && $this->methods[$controller_method]['log'] === FALSE);
  520. // Use keys for this method?
  521. $use_key = !(isset($this->methods[$controller_method]['key']) && $this->methods[$controller_method]['key'] === FALSE);
  522. // They provided a key, but it wasn't valid, so get them out of here
  523. if ($this->config->item('rest_enable_keys') && $use_key && $this->_allow === FALSE)
  524. {
  525. if ($this->config->item('rest_enable_logging') && $log_method)
  526. {
  527. $this->_log_request();
  528. }
  529. $this->response([
  530. $this->config->item('rest_status_field_name') => FALSE,
  531. $this->config->item('rest_message_field_name') => sprintf($this->lang->line('text_rest_invalid_api_key'), $this->rest->key)
  532. ], self::HTTP_FORBIDDEN);
  533. }
  534. // Check to see if this key has access to the requested controller
  535. if ($this->config->item('rest_enable_keys') && $use_key && empty($this->rest->key) === FALSE && $this->_check_access() === FALSE)
  536. {
  537. if ($this->config->item('rest_enable_logging') && $log_method)
  538. {
  539. $this->_log_request();
  540. }
  541. $this->response([
  542. $this->config->item('rest_status_field_name') => FALSE,
  543. $this->config->item('rest_message_field_name') => $this->lang->line('text_rest_api_key_unauthorized')
  544. ], self::HTTP_UNAUTHORIZED);
  545. }
  546. // Sure it exists, but can they do anything with it?
  547. if (method_exists($this, $controller_method) === FALSE)
  548. {
  549. $this->response([
  550. $this->config->item('rest_status_field_name') => FALSE,
  551. $this->config->item('rest_message_field_name') => $this->lang->line('text_rest_unknown_method')
  552. ], self::HTTP_NOT_FOUND);
  553. }
  554. // Doing key related stuff? Can only do it if they have a key right?
  555. if ($this->config->item('rest_enable_keys') && empty($this->rest->key) === FALSE)
  556. {
  557. // Check the limit
  558. if ($this->config->item('rest_enable_limits') && $this->_check_limit($controller_method) === FALSE)
  559. {
  560. $response = [$this->config->item('rest_status_field_name') => FALSE, $this->config->item('rest_message_field_name') => $this->lang->line('text_rest_api_key_time_limit')];
  561. $this->response($response, self::HTTP_UNAUTHORIZED);
  562. }
  563. // If no level is set use 0, they probably aren't using permissions
  564. $level = isset($this->methods[$controller_method]['level']) ? $this->methods[$controller_method]['level'] : 0;
  565. // If no level is set, or it is lower than/equal to the key's level
  566. $authorized = $level <= $this->rest->level;
  567. // IM TELLIN!
  568. if ($this->config->item('rest_enable_logging') && $log_method)
  569. {
  570. $this->_log_request($authorized);
  571. }
  572. // They don't have good enough perms
  573. $response = [$this->config->item('rest_status_field_name') => FALSE, $this->config->item('rest_message_field_name') => $this->lang->line('text_rest_api_key_permissions')];
  574. $authorized || $this->response($response, self::HTTP_UNAUTHORIZED);
  575. }
  576. // No key stuff, but record that stuff is happening
  577. elseif ($this->config->item('rest_enable_logging') && $log_method)
  578. {
  579. $this->_log_request($authorized = TRUE);
  580. }
  581. // Call the controller method and passed arguments
  582. try
  583. {
  584. call_user_func_array([$this, $controller_method], $arguments);
  585. }
  586. catch (Exception $ex)
  587. {
  588. // If the method doesn't exist, then the error will be caught and an error response shown
  589. $this->response([
  590. $this->config->item('rest_status_field_name') => FALSE,
  591. $this->config->item('rest_message_field_name') => [
  592. 'classname' => get_class($ex),
  593. 'message' => $ex->getMessage()
  594. ]
  595. ], self::HTTP_INTERNAL_SERVER_ERROR);
  596. }
  597. }
  598. /**
  599. * Takes mixed data and optionally a status code, then creates the response
  600. *
  601. * @access public
  602. * @param array|NULL $data Data to output to the user
  603. * @param int|NULL $http_code HTTP status code
  604. * @param bool $continue TRUE to flush the response to the client and continue
  605. * running the script; otherwise, exit
  606. */
  607. public function response($data = NULL, $http_code = NULL, $continue = FALSE)
  608. {
  609. // If the HTTP status is not NULL, then cast as an integer
  610. if ($http_code !== NULL)
  611. {
  612. // So as to be safe later on in the process
  613. $http_code = (int) $http_code;
  614. }
  615. // Set the output as NULL by default
  616. $output = NULL;
  617. // If data is NULL and no HTTP status code provided, then display, error and exit
  618. if ($data === NULL && $http_code === NULL)
  619. {
  620. $http_code = self::HTTP_NOT_FOUND;
  621. }
  622. // If data is not NULL and a HTTP status code provided, then continue
  623. elseif ($data !== NULL)
  624. {
  625. // If the format method exists, call and return the output in that format
  626. if (method_exists($this->format, 'to_' . $this->response->format))
  627. {
  628. // Set the format header
  629. $this->output->set_content_type($this->_supported_formats[$this->response->format], strtolower($this->config->item('charset')));
  630. $output = $this->format->factory($data)->{'to_' . $this->response->format}();
  631. // An array must be parsed as a string, so as not to cause an array to string error
  632. // Json is the most appropriate form for such a datatype
  633. if ($this->response->format === 'array')
  634. {
  635. $output = $this->format->factory($output)->{'to_json'}();
  636. }
  637. }
  638. else
  639. {
  640. // If an array or object, then parse as a json, so as to be a 'string'
  641. if (is_array($data) || is_object($data))
  642. {
  643. $data = $this->format->factory($data)->{'to_json'}();
  644. }
  645. // Format is not supported, so output the raw data as a string
  646. $output = $data;
  647. }
  648. }
  649. // If not greater than zero, then set the HTTP status code as 200 by default
  650. // Though perhaps 500 should be set instead, for the developer not passing a
  651. // correct HTTP status code
  652. $http_code > 0 || $http_code = self::HTTP_OK;
  653. $this->output->set_status_header($http_code);
  654. // JC: Log response code only if rest logging enabled
  655. if ($this->config->item('rest_enable_logging') === TRUE)
  656. {
  657. $this->_log_response_code($http_code);
  658. }
  659. // Output the data
  660. $this->output->set_output($output);
  661. if ($continue === FALSE)
  662. {
  663. // Display the data and exit execution
  664. $this->output->_display();
  665. exit;
  666. }
  667. // Otherwise dump the output automatically
  668. }
  669. /**
  670. * Takes mixed data and optionally a status code, then creates the response
  671. * within the buffers of the Output class. The response is sent to the client
  672. * lately by the framework, after the current controller's method termination.
  673. * All the hooks after the controller's method termination are executable
  674. *
  675. * @access public
  676. * @param array|NULL $data Data to output to the user
  677. * @param int|NULL $http_code HTTP status code
  678. */
  679. public function set_response($data = NULL, $http_code = NULL)
  680. {
  681. $this->response($data, $http_code, TRUE);
  682. }
  683. /**
  684. * Get the input format e.g. json or xml
  685. *
  686. * @access protected
  687. * @return string|NULL Supported input format; otherwise, NULL
  688. */
  689. protected function _detect_input_format()
  690. {
  691. // Get the CONTENT-TYPE value from the SERVER variable
  692. $content_type = $this->input->server('CONTENT_TYPE');
  693. if (empty($content_type) === FALSE)
  694. {
  695. // Check all formats against the HTTP_ACCEPT header
  696. foreach ($this->_supported_formats as $key => $value)
  697. {
  698. // $key = format e.g. csv
  699. // $value = mime type e.g. application/csv
  700. // If a semi-colon exists in the string, then explode by ; and get the value of where
  701. // the current array pointer resides. This will generally be the first element of the array
  702. $content_type = (strpos($content_type, ';') !== FALSE ? current(explode(';', $content_type)) : $content_type);
  703. // If both the mime types match, then return the format
  704. if ($content_type === $value)
  705. {
  706. return $key;
  707. }
  708. }
  709. }
  710. return NULL;
  711. }
  712. /**
  713. * Gets the default format from the configuration. Fallbacks to 'json'
  714. * if the corresponding configuration option $config['rest_default_format']
  715. * is missing or is empty
  716. *
  717. * @access protected
  718. * @return string The default supported input format
  719. */
  720. protected function _get_default_output_format()
  721. {
  722. $default_format = (string) $this->config->item('rest_default_format');
  723. return $default_format === '' ? 'json' : $default_format;
  724. }
  725. /**
  726. * Detect which format should be used to output the data
  727. *
  728. * @access protected
  729. * @return mixed|NULL|string Output format
  730. */
  731. protected function _detect_output_format()
  732. {
  733. // Concatenate formats to a regex pattern e.g. \.(csv|json|xml)
  734. $pattern = '/\.(' . implode('|', array_keys($this->_supported_formats)) . ')($|\/)/';
  735. $matches = [];
  736. // Check if a file extension is used e.g. http://example.com/api/index.json?param1=param2
  737. if (preg_match($pattern, $this->uri->uri_string(), $matches))
  738. {
  739. return $matches[1];
  740. }
  741. // Get the format parameter named as 'format'
  742. if (isset($this->_get_args['format']))
  743. {
  744. $format = strtolower($this->_get_args['format']);
  745. if (isset($this->_supported_formats[$format]) === TRUE)
  746. {
  747. return $format;
  748. }
  749. }
  750. // Get the HTTP_ACCEPT server variable
  751. $http_accept = $this->input->server('HTTP_ACCEPT');
  752. // Otherwise, check the HTTP_ACCEPT server variable
  753. if ($this->config->item('rest_ignore_http_accept') === FALSE && $http_accept !== NULL)
  754. {
  755. // Check all formats against the HTTP_ACCEPT header
  756. foreach (array_keys($this->_supported_formats) as $format)
  757. {
  758. // Has this format been requested?
  759. if (strpos($http_accept, $format) !== FALSE)
  760. {
  761. if ($format !== 'html' && $format !== 'xml')
  762. {
  763. // If not HTML or XML assume it's correct
  764. return $format;
  765. }
  766. elseif ($format === 'html' && strpos($http_accept, 'xml') === FALSE)
  767. {
  768. // HTML or XML have shown up as a match
  769. // If it is truly HTML, it wont want any XML
  770. return $format;
  771. }
  772. else if ($format === 'xml' && strpos($http_accept, 'html') === FALSE)
  773. {
  774. // If it is truly XML, it wont want any HTML
  775. return $format;
  776. }
  777. }
  778. }
  779. }
  780. // Check if the controller has a default format
  781. if (empty($this->rest_format) === FALSE)
  782. {
  783. return $this->rest_format;
  784. }
  785. // Obtain the default format from the configuration
  786. return $this->_get_default_output_format();
  787. }
  788. /**
  789. * Get the HTTP request string e.g. get or post
  790. *
  791. * @access protected
  792. * @return string|NULL Supported request method as a lowercase string; otherwise, NULL if not supported
  793. */
  794. protected function _detect_method()
  795. {
  796. // Declare a variable to store the method
  797. $method = NULL;
  798. // Determine whether the 'enable_emulate_request' setting is enabled
  799. if ($this->config->item('enable_emulate_request') === TRUE)
  800. {
  801. $method = $this->input->post('_method');
  802. if ($method === NULL)
  803. {
  804. $method = $this->input->server('HTTP_X_HTTP_METHOD_OVERRIDE');
  805. }
  806. $method = strtolower($method);
  807. }
  808. if (empty($method))
  809. {
  810. // Get the request method as a lowercase string
  811. $method = $this->input->method();
  812. }
  813. return in_array($method, $this->allowed_http_methods) && method_exists($this, '_parse_' . $method) ? $method : 'get';
  814. }
  815. /**
  816. * See if the user has provided an API key
  817. *
  818. * @access protected
  819. * @return bool
  820. */
  821. protected function _detect_api_key()
  822. {
  823. // Get the api key name variable set in the rest config file
  824. $api_key_variable = $this->config->item('rest_key_name');
  825. // Work out the name of the SERVER entry based on config
  826. $key_name = 'HTTP_' . strtoupper(str_replace('-', '_', $api_key_variable));
  827. $this->rest->key = NULL;
  828. $this->rest->level = NULL;
  829. $this->rest->user_id = NULL;
  830. $this->rest->ignore_limits = FALSE;
  831. // Find the key from server or arguments
  832. if (($key = isset($this->_args[$api_key_variable]) ? $this->_args[$api_key_variable] : $this->input->server($key_name)))
  833. {
  834. if (!($row = $this->rest->db->where($this->config->item('rest_key_column'), $key)->get($this->config->item('rest_keys_table'))->row()))
  835. {
  836. return FALSE;
  837. }
  838. $this->rest->key = $row->{$this->config->item('rest_key_column')};
  839. isset($row->user_id) && $this->rest->user_id = $row->user_id;
  840. isset($row->level) && $this->rest->level = $row->level;
  841. isset($row->ignore_limits) && $this->rest->ignore_limits = $row->ignore_limits;
  842. $this->_apiuser = $row;
  843. /*
  844. * If "is private key" is enabled, compare the ip address with the list
  845. * of valid ip addresses stored in the database
  846. */
  847. if (empty($row->is_private_key) === FALSE)
  848. {
  849. // Check for a list of valid ip addresses
  850. if (isset($row->ip_addresses))
  851. {
  852. // multiple ip addresses must be separated using a comma, explode and loop
  853. $list_ip_addresses = explode(',', $row->ip_addresses);
  854. $found_address = FALSE;
  855. foreach ($list_ip_addresses as $ip_address)
  856. {
  857. if ($this->input->ip_address() === trim($ip_address))
  858. {
  859. // there is a match, set the the value to TRUE and break out of the loop
  860. $found_address = TRUE;
  861. break;
  862. }
  863. }
  864. return $found_address;
  865. }
  866. else
  867. {
  868. // There should be at least one IP address for this private key
  869. return FALSE;
  870. }
  871. }
  872. return TRUE;
  873. }
  874. // No key has been sent
  875. return FALSE;
  876. }
  877. /**
  878. * Preferred return language
  879. *
  880. * @access protected
  881. * @return string|NULL The language code
  882. */
  883. protected function _detect_lang()
  884. {
  885. $lang = $this->input->server('HTTP_ACCEPT_LANGUAGE');
  886. if ($lang === NULL)
  887. {
  888. return NULL;
  889. }
  890. // It appears more than one language has been sent using a comma delimiter
  891. if (strpos($lang, ',') !== FALSE)
  892. {
  893. $langs = explode(',', $lang);
  894. $return_langs = [];
  895. foreach ($langs as $lang)
  896. {
  897. // Remove weight and trim leading and trailing whitespace
  898. list($lang) = explode(';', $lang);
  899. $return_langs[] = trim($lang);
  900. }
  901. return $return_langs;
  902. }
  903. // Otherwise simply return as a string
  904. return $lang;
  905. }
  906. /**
  907. * Add the request to the log table
  908. *
  909. * @access protected
  910. * @param bool $authorized TRUE the user is authorized; otherwise, FALSE
  911. * @return bool TRUE the data was inserted; otherwise, FALSE
  912. */
  913. protected function _log_request($authorized = FALSE)
  914. {
  915. // Insert the request into the log table
  916. $is_inserted = $this->rest->db
  917. ->insert(
  918. $this->config->item('rest_logs_table'), [
  919. 'uri' => $this->uri->uri_string(),
  920. 'method' => $this->request->method,
  921. 'params' => $this->_args ? ($this->config->item('rest_logs_json_params') === TRUE ? json_encode($this->_args) : serialize($this->_args)) : NULL,
  922. 'api_key' => isset($this->rest->key) ? $this->rest->key : '',
  923. 'ip_address' => $this->input->ip_address(),
  924. 'time' => time(),
  925. 'authorized' => $authorized
  926. ]);
  927. // Get the last insert id to update at a later stage of the request
  928. $this->_insert_id = $this->rest->db->insert_id();
  929. return $is_inserted;
  930. }
  931. /**
  932. * Check if the requests to a controller method exceed a limit
  933. *
  934. * @access protected
  935. * @param string $controller_method The method being called
  936. * @return bool TRUE the call limit is below the threshold; otherwise, FALSE
  937. */
  938. protected function _check_limit($controller_method)
  939. {
  940. // They are special, or it might not even have a limit
  941. if (empty($this->rest->ignore_limits) === FALSE)
  942. {
  943. // Everything is fine
  944. return TRUE;
  945. }
  946. switch ($this->config->item('rest_limits_method'))
  947. {
  948. case 'API_KEY':
  949. $limited_uri = 'api-key:' . (isset($this->rest->key) ? $this->rest->key : '');
  950. $limited_method_name = isset($this->rest->key) ? $this->rest->key : '';
  951. break;
  952. case 'METHOD_NAME':
  953. $limited_uri = 'method-name:' . $controller_method;
  954. $limited_method_name = $controller_method;
  955. break;
  956. case 'ROUTED_URL':
  957. default:
  958. $limited_uri = $this->uri->ruri_string();
  959. if (strpos(strrev($limited_uri), strrev($this->response->format)) === 0)
  960. {
  961. $limited_uri = substr($limited_uri,0, -strlen($this->response->format) - 1);
  962. }
  963. $limited_uri = 'uri:' . $limited_uri . ':' . $this->request->method; // It's good to differentiate GET from PUT
  964. $limited_method_name = $controller_method;
  965. break;
  966. }
  967. if (isset($this->methods[$limited_method_name]['limit']) === FALSE )
  968. {
  969. // Everything is fine
  970. return TRUE;
  971. }
  972. // How many times can you get to this method in a defined time_limit (default: 1 hour)?
  973. $limit = $this->methods[$limited_method_name]['limit'];
  974. $time_limit = (isset($this->methods[$limited_method_name]['time']) ? $this->methods[$limited_method_name]['time'] : 3600); // 3600 = 60 * 60
  975. // Get data about a keys' usage and limit to one row
  976. $result = $this->rest->db
  977. ->where('uri', $limited_uri)
  978. ->where('api_key', $this->rest->key)
  979. ->get($this->config->item('rest_limits_table'))
  980. ->row();
  981. // No calls have been made for this key
  982. if ($result === NULL)
  983. {
  984. // Create a new row for the following key
  985. $this->rest->db->insert($this->config->item('rest_limits_table'), [
  986. 'uri' => $limited_uri,
  987. 'api_key' => isset($this->rest->key) ? $this->rest->key : '',
  988. 'count' => 1,
  989. 'hour_started' => time()
  990. ]);
  991. }
  992. // Been a time limit (or by default an hour) since they called
  993. elseif ($result->hour_started < (time() - $time_limit))
  994. {
  995. // Reset the started period and count
  996. $this->rest->db
  997. ->where('uri', $limited_uri)
  998. ->where('api_key', isset($this->rest->key) ? $this->rest->key : '')
  999. ->set('hour_started', time())
  1000. ->set('count', 1)
  1001. ->update($this->config->item('rest_limits_table'));
  1002. }
  1003. // They have called within the hour, so lets update
  1004. else
  1005. {
  1006. // The limit has been exceeded
  1007. if ($result->count >= $limit)
  1008. {
  1009. return FALSE;
  1010. }
  1011. // Increase the count by one
  1012. $this->rest->db
  1013. ->where('uri', $limited_uri)
  1014. ->where('api_key', $this->rest->key)
  1015. ->set('count', 'count + 1', FALSE)
  1016. ->update($this->config->item('rest_limits_table'));
  1017. }
  1018. return TRUE;
  1019. }
  1020. /**
  1021. * Check if there is a specific auth type set for the current class/method/HTTP-method being called
  1022. *
  1023. * @access protected
  1024. * @return bool
  1025. */
  1026. protected function _auth_override_check()
  1027. {
  1028. // Assign the class/method auth type override array from the config
  1029. $auth_override_class_method = $this->config->item('auth_override_class_method');
  1030. // Check to see if the override array is even populated
  1031. if (!empty($auth_override_class_method))
  1032. {
  1033. // check for wildcard flag for rules for classes
  1034. if (!empty($auth_override_class_method[$this->router->class]['*'])) // Check for class overrides
  1035. {
  1036. // None auth override found, prepare nothing but send back a TRUE override flag
  1037. if ($auth_override_class_method[$this->router->class]['*'] === 'none')
  1038. {
  1039. return TRUE;
  1040. }
  1041. // Basic auth override found, prepare basic
  1042. if ($auth_override_class_method[$this->router->class]['*'] === 'basic')
  1043. {
  1044. $this->_prepare_basic_auth();
  1045. return TRUE;
  1046. }
  1047. // Digest auth override found, prepare digest
  1048. if ($auth_override_class_method[$this->router->class]['*'] === 'digest')
  1049. {
  1050. $this->_prepare_digest_auth();
  1051. return TRUE;
  1052. }
  1053. // Session auth override found, check session
  1054. if ($auth_override_class_method[$this->router->class]['*'] === 'session')
  1055. {
  1056. $this->_check_php_session();
  1057. return TRUE;
  1058. }
  1059. // Whitelist auth override found, check client's ip against config whitelist
  1060. if ($auth_override_class_method[$this->router->class]['*'] === 'whitelist')
  1061. {
  1062. $this->_check_whitelist_auth();
  1063. return TRUE;
  1064. }
  1065. }
  1066. // Check to see if there's an override value set for the current class/method being called
  1067. if (!empty($auth_override_class_method[$this->router->class][$this->router->method]))
  1068. {
  1069. // None auth override found, prepare nothing but send back a TRUE override flag
  1070. if ($auth_override_class_method[$this->router->class][$this->router->method] === 'none')
  1071. {
  1072. return TRUE;
  1073. }
  1074. // Basic auth override found, prepare basic
  1075. if ($auth_override_class_method[$this->router->class][$this->router->method] === 'basic')
  1076. {
  1077. $this->_prepare_basic_auth();
  1078. return TRUE;
  1079. }
  1080. // Digest auth override found, prepare digest
  1081. if ($auth_override_class_method[$this->router->class][$this->router->method] === 'digest')
  1082. {
  1083. $this->_prepare_digest_auth();
  1084. return TRUE;
  1085. }
  1086. // Session auth override found, check session
  1087. if ($auth_override_class_method[$this->router->class][$this->router->method] === 'session')
  1088. {
  1089. $this->_check_php_session();
  1090. return TRUE;
  1091. }
  1092. // Whitelist auth override found, check client's ip against config whitelist
  1093. if ($auth_override_class_method[$this->router->class][$this->router->method] === 'whitelist')
  1094. {
  1095. $this->_check_whitelist_auth();
  1096. return TRUE;
  1097. }
  1098. }
  1099. }
  1100. // Assign the class/method/HTTP-method auth type override array from the config
  1101. $auth_override_class_method_http = $this->config->item('auth_override_class_method_http');
  1102. // Check to see if the override array is even populated
  1103. if (!empty($auth_override_class_method_http))
  1104. {
  1105. // check for wildcard flag for rules for classes
  1106. if(!empty($auth_override_class_method_http[$this->router->class]['*'][$this->request->method]))
  1107. {
  1108. // None auth override found, prepare nothing but send back a TRUE override flag
  1109. if ($auth_override_class_method_http[$this->router->class]['*'][$this->request->method] === 'none')
  1110. {
  1111. return TRUE;
  1112. }
  1113. // Basic auth override found, prepare basic
  1114. if ($auth_override_class_method_http[$this->router->class]['*'][$this->request->method] === 'basic')
  1115. {
  1116. $this->_prepare_basic_auth();
  1117. return TRUE;
  1118. }
  1119. // Digest auth override found, prepare digest
  1120. if ($auth_override_class_method_http[$this->router->class]['*'][$this->request->method] === 'digest')
  1121. {
  1122. $this->_prepare_digest_auth();
  1123. return TRUE;
  1124. }
  1125. // Session auth override found, check session
  1126. if ($auth_override_class_method_http[$this->router->class]['*'][$this->request->method] === 'session')
  1127. {
  1128. $this->_check_php_session();
  1129. return TRUE;
  1130. }
  1131. // Whitelist auth override found, check client's ip against config whitelist
  1132. if ($auth_override_class_method_http[$this->router->class]['*'][$this->request->method] === 'whitelist')
  1133. {
  1134. $this->_check_whitelist_auth();
  1135. return TRUE;
  1136. }
  1137. }
  1138. // Check to see if there's an override value set for the current class/method/HTTP-method being called
  1139. if(!empty($auth_override_class_method_http[$this->router->class][$this->router->method][$this->request->method]))
  1140. {
  1141. // None auth override found, prepare nothing but send back a TRUE override flag
  1142. if ($auth_override_class_method_http[$this->router->class][$this->router->method][$this->request->method] === 'none')
  1143. {
  1144. return TRUE;
  1145. }
  1146. // Basic auth override found, prepare basic
  1147. if ($auth_override_class_method_http[$this->router->class][$this->router->method][$this->request->method] === 'basic')
  1148. {
  1149. $this->_prepare_basic_auth();
  1150. return TRUE;
  1151. }
  1152. // Digest auth override found, prepare digest
  1153. if ($auth_override_class_method_http[$this->router->class][$this->router->method][$this->request->method] === 'digest')
  1154. {
  1155. $this->_prepare_digest_auth();
  1156. return TRUE;
  1157. }
  1158. // Session auth override found, check session
  1159. if ($auth_override_class_method_http[$this->router->class][$this->router->method][$this->request->method] === 'session')
  1160. {
  1161. $this->_check_php_session();
  1162. return TRUE;
  1163. }
  1164. // Whitelist auth override found, check client's ip against config whitelist
  1165. if ($auth_override_class_method_http[$this->router->class][$this->router->method][$this->request->method] === 'whitelist')
  1166. {
  1167. $this->_check_whitelist_auth();
  1168. return TRUE;
  1169. }
  1170. }
  1171. }
  1172. return FALSE;
  1173. }
  1174. /**
  1175. * Parse the GET request arguments
  1176. *
  1177. * @access protected
  1178. * @return void
  1179. */
  1180. protected function _parse_get()
  1181. {
  1182. // Merge both the URI segments and query parameters
  1183. $this->_get_args = array_merge($this->_get_args, $this->_query_args);
  1184. }
  1185. /**
  1186. * Parse the POST request arguments
  1187. *
  1188. * @access protected
  1189. * @return void
  1190. */
  1191. protected function _parse_post()
  1192. {
  1193. $this->_post_args = $_POST;
  1194. if ($this->request->format)
  1195. {
  1196. $this->request->body = $this->input->raw_input_stream;
  1197. }
  1198. }
  1199. /**
  1200. * Parse the PUT request arguments
  1201. *
  1202. * @access protected
  1203. * @return void
  1204. */
  1205. protected function _parse_put()
  1206. {
  1207. if ($this->request->format)
  1208. {
  1209. $this->request->body = $this->input->raw_input_stream;
  1210. }
  1211. else if ($this->input->method() === 'put')
  1212. {
  1213. // If no filetype is provided, then there are probably just arguments
  1214. $this->_put_args = $this->input->input_stream();
  1215. }
  1216. }
  1217. /**
  1218. * Parse the HEAD request arguments
  1219. *
  1220. * @access protected
  1221. * @return void
  1222. */
  1223. protected function _parse_head()
  1224. {
  1225. // Parse the HEAD variables
  1226. parse_str(parse_url($this->input->server('REQUEST_URI'), PHP_URL_QUERY), $head);
  1227. // Merge both the URI segments and HEAD params
  1228. $this->_head_args = array_merge($this->_head_args, $head);
  1229. }
  1230. /**
  1231. * Parse the OPTIONS request arguments
  1232. *
  1233. * @access protected
  1234. * @return void
  1235. */
  1236. protected function _parse_options()
  1237. {
  1238. // Parse the OPTIONS variables
  1239. parse_str(parse_url($this->input->server('REQUEST_URI'), PHP_URL_QUERY), $options);
  1240. // Merge both the URI segments and OPTIONS params
  1241. $this->_options_args = array_merge($this->_options_args, $options);
  1242. }
  1243. /**
  1244. * Parse the PATCH request arguments
  1245. *
  1246. * @access protected
  1247. * @return void
  1248. */
  1249. protected function _parse_patch()
  1250. {
  1251. // It might be a HTTP body
  1252. if ($this->request->format)
  1253. {
  1254. $this->request->body = $this->input->raw_input_stream;
  1255. }
  1256. else if ($this->input->method() === 'patch')
  1257. {
  1258. // If no filetype is provided, then there are probably just arguments
  1259. $this->_patch_args = $this->input->input_stream();
  1260. }
  1261. }
  1262. /**
  1263. * Parse the DELETE request arguments
  1264. *
  1265. * @access protected
  1266. * @return void
  1267. */
  1268. protected function _parse_delete()
  1269. {
  1270. // These should exist if a DELETE request
  1271. if ($this->input->method() === 'delete')
  1272. {
  1273. $this->_delete_args = $this->input->input_stream();
  1274. }
  1275. }
  1276. /**
  1277. * Parse the query parameters
  1278. *
  1279. * @access protected
  1280. * @return void
  1281. */
  1282. protected function _parse_query()
  1283. {
  1284. $this->_query_args = $this->input->get();
  1285. }
  1286. // INPUT FUNCTION --------------------------------------------------------------
  1287. /**
  1288. * Retrieve a value from a GET request
  1289. *
  1290. * @access public
  1291. * @param NULL $key Key to retrieve from the GET request
  1292. * If NULL an array of arguments is returned
  1293. * @param NULL $xss_clean Whether to apply XSS filtering
  1294. * @return array|string|NULL Value from the GET request; otherwise, NULL
  1295. */
  1296. public function get($key = NULL, $xss_clean = NULL)
  1297. {
  1298. if ($key === NULL)
  1299. {
  1300. return $this->_get_args;
  1301. }
  1302. return isset($this->_get_args[$key]) ? $this->_xss_clean($this->_get_args[$key], $xss_clean) : NULL;
  1303. }
  1304. /**
  1305. * Retrieve a value from a OPTIONS request
  1306. *
  1307. * @access public
  1308. * @param NULL $key Key to retrieve from the OPTIONS request.
  1309. * If NULL an array of arguments is returned
  1310. * @param NULL $xss_clean Whether to apply XSS filtering
  1311. * @return array|string|NULL Value from the OPTIONS request; otherwise, NULL
  1312. */
  1313. public function options($key = NULL, $xss_clean = NULL)
  1314. {
  1315. if ($key === NULL)
  1316. {
  1317. return $this->_options_args;
  1318. }
  1319. return isset($this->_options_args[$key]) ? $this->_xss_clean($this->_options_args[$key], $xss_clean) : NULL;
  1320. }
  1321. /**
  1322. * Retrieve a value from a HEAD request
  1323. *
  1324. * @access public
  1325. * @param NULL $key Key to retrieve from the HEAD request
  1326. * If NULL an array of arguments is returned
  1327. * @param NULL $xss_clean Whether to apply XSS filtering
  1328. * @return array|string|NULL Value from the HEAD request; otherwise, NULL
  1329. */
  1330. public function head($key = NULL, $xss_clean = NULL)
  1331. {
  1332. if ($key === NULL)
  1333. {
  1334. return $this->_head_args;
  1335. }
  1336. return isset($this->_head_args[$key]) ? $this->_xss_clean($this->_head_args[$key], $xss_clean) : NULL;
  1337. }
  1338. /**
  1339. * Retrieve a value from a POST request
  1340. *
  1341. * @access public
  1342. * @param NULL $key Key to retrieve from the POST request
  1343. * If NULL an array of arguments is returned
  1344. * @param NULL $xss_clean Whether to apply XSS filtering
  1345. * @return array|string|NULL Value from the POST request; otherwise, NULL
  1346. */
  1347. public function post($key = NULL, $xss_clean = NULL)
  1348. {
  1349. if ($key === NULL)
  1350. {
  1351. return $this->_post_args;
  1352. }
  1353. return isset($this->_post_args[$key]) ? $this->_xss_clean($this->_post_args[$key], $xss_clean) : NULL;
  1354. }
  1355. /**
  1356. * Retrieve a value from a PUT request
  1357. *
  1358. * @access public
  1359. * @param NULL $key Key to retrieve from the PUT request
  1360. * If NULL an array of arguments is returned
  1361. * @param NULL $xss_clean Whether to apply XSS filtering
  1362. * @return array|string|NULL Value from the PUT request; otherwise, NULL
  1363. */
  1364. public function put($key = NULL, $xss_clean = NULL)
  1365. {
  1366. if ($key === NULL)
  1367. {
  1368. return $this->_put_args;
  1369. }
  1370. return isset($this->_put_args[$key]) ? $this->_xss_clean($this->_put_args[$key], $xss_clean) : NULL;
  1371. }
  1372. /**
  1373. * Retrieve a value from a DELETE request
  1374. *
  1375. * @access public
  1376. * @param NULL $key Key to retrieve from the DELETE request
  1377. * If NULL an array of arguments is returned
  1378. * @param NULL $xss_clean Whether to apply XSS filtering
  1379. * @return array|string|NULL Value from the DELETE request; otherwise, NULL
  1380. */
  1381. public function delete($key = NULL, $xss_clean = NULL)
  1382. {
  1383. if ($key === NULL)
  1384. {
  1385. return $this->_delete_args;
  1386. }
  1387. return isset($this->_delete_args[$key]) ? $this->_xss_clean($this->_delete_args[$key], $xss_clean) : NULL;
  1388. }
  1389. /**
  1390. * Retrieve a value from a PATCH request
  1391. *
  1392. * @access public
  1393. * @param NULL $key Key to retrieve from the PATCH request
  1394. * If NULL an array of arguments is returned
  1395. * @param NULL $xss_clean Whether to apply XSS filtering
  1396. * @return array|string|NULL Value from the PATCH request; otherwise, NULL
  1397. */
  1398. public function patch($key = NULL, $xss_clean = NULL)
  1399. {
  1400. if ($key === NULL)
  1401. {
  1402. return $this->_patch_args;
  1403. }
  1404. return isset($this->_patch_args[$key]) ? $this->_xss_clean($this->_patch_args[$key], $xss_clean) : NULL;
  1405. }
  1406. /**
  1407. * Retrieve a value from the query parameters
  1408. *
  1409. * @access public
  1410. * @param NULL $key Key to retrieve from the query parameters
  1411. * If NULL an array of arguments is returned
  1412. * @param NULL $xss_clean Whether to apply XSS filtering
  1413. * @return array|string|NULL Value from the query parameters; otherwise, NULL
  1414. */
  1415. public function query($key = NULL, $xss_clean = NULL)
  1416. {
  1417. if ($key === NULL)
  1418. {
  1419. return $this->_query_args;
  1420. }
  1421. return isset($this->_query_args[$key]) ? $this->_xss_clean($this->_query_args[$key], $xss_clean) : NULL;
  1422. }
  1423. /**
  1424. * Sanitizes data so that Cross Site Scripting Hacks can be
  1425. * prevented
  1426. *
  1427. * @access protected
  1428. * @param string $value Input data
  1429. * @param bool $xss_clean Whether to apply XSS filtering
  1430. * @return string
  1431. */
  1432. protected function _xss_clean($value, $xss_clean)
  1433. {
  1434. is_bool($xss_clean) || $xss_clean = $this->_enable_xss;
  1435. return $xss_clean === TRUE ? $this->security->xss_clean($value) : $value;
  1436. }
  1437. /**
  1438. * Retrieve the validation errors
  1439. *
  1440. * @access public
  1441. * @return array
  1442. */
  1443. public function validation_errors()
  1444. {
  1445. $string = strip_tags($this->form_validation->error_string());
  1446. return explode(PHP_EOL, trim($string, PHP_EOL));
  1447. }
  1448. // SECURITY FUNCTIONS ---------------------------------------------------------
  1449. /**
  1450. * Perform LDAP Authentication
  1451. *
  1452. * @access protected
  1453. * @param string $username The username to validate
  1454. * @param string $password The password to validate
  1455. * @return bool
  1456. */
  1457. protected function _perform_ldap_auth($username = '', $password = NULL)
  1458. {
  1459. if (empty($username))
  1460. {
  1461. log_message('debug', 'LDAP Auth: failure, empty username');
  1462. return FALSE;
  1463. }
  1464. log_message('debug', 'LDAP Auth: Loading configuration');
  1465. $this->config->load('ldap.php', TRUE);
  1466. $ldap = [
  1467. 'timeout' => $this->config->item('timeout', 'ldap'),
  1468. 'host' => $this->config->item('server', 'ldap'),
  1469. 'port' => $this->config->item('port', 'ldap'),
  1470. 'rdn' => $this->config->item('binduser', 'ldap'),
  1471. 'pass' => $this->config->item('bindpw', 'ldap'),
  1472. 'basedn' => $this->config->item('basedn', 'ldap'),
  1473. ];
  1474. log_message('debug', 'LDAP Auth: Connect to ' . (isset($ldaphost) ? $ldaphost : '[ldap not configured]'));
  1475. // Connect to the ldap server
  1476. $ldapconn = ldap_connect($ldap['host'], $ldap['port']);
  1477. if ($ldapconn)
  1478. {
  1479. log_message('debug', 'Setting timeout to ' . $ldap['timeout'] . ' seconds');
  1480. ldap_set_option($ldapconn, LDAP_OPT_NETWORK_TIMEOUT, $ldap['timeout']);
  1481. log_message('debug', 'LDAP Auth: Binding to ' . $ldap['host'] . ' with dn ' . $ldap['rdn']);
  1482. // Binding to the ldap server
  1483. $ldapbind = ldap_bind($ldapconn, $ldap['rdn'], $ldap['pass']);
  1484. // Verify the binding
  1485. if ($ldapbind === FALSE)
  1486. {
  1487. log_message('error', 'LDAP Auth: bind was unsuccessful');
  1488. return FALSE;
  1489. }
  1490. log_message('debug', 'LDAP Auth: bind successful');
  1491. }
  1492. // Search for user
  1493. if (($res_id = ldap_search($ldapconn, $ldap['basedn'], "uid=$username")) === FALSE)
  1494. {
  1495. log_message('error', 'LDAP Auth: User ' . $username . ' not found in search');
  1496. return FALSE;
  1497. }
  1498. if (ldap_count_entries($ldapconn, $res_id) !== 1)
  1499. {
  1500. log_message('error', 'LDAP Auth: Failure, username ' . $username . 'found more than once');
  1501. return FALSE;
  1502. }
  1503. if (($entry_id = ldap_first_entry($ldapconn, $res_id)) === FALSE)
  1504. {
  1505. log_message('error', 'LDAP Auth: Failure, entry of search result could not be fetched');
  1506. return FALSE;
  1507. }
  1508. if (($user_dn = ldap_get_dn($ldapconn, $entry_id)) === FALSE)
  1509. {
  1510. log_message('error', 'LDAP Auth: Failure, user-dn could not be fetched');
  1511. return FALSE;
  1512. }
  1513. // User found, could not authenticate as user
  1514. if (($link_id = ldap_bind($ldapconn, $user_dn, $password)) === FALSE)
  1515. {
  1516. log_message('error', 'LDAP Auth: Failure, username/password did not match: ' . $user_dn);
  1517. return FALSE;
  1518. }
  1519. log_message('debug', 'LDAP Auth: Success ' . $user_dn . ' authenticated successfully');
  1520. $this->_user_ldap_dn = $user_dn;
  1521. ldap_close($ldapconn);
  1522. return TRUE;
  1523. }
  1524. /**
  1525. * Perform Library Authentication - Override this function to change the way the library is called
  1526. *
  1527. * @access protected
  1528. * @param string $username The username to validate
  1529. * @param string $password The password to validate
  1530. * @return bool
  1531. */
  1532. protected function _perform_library_auth($username = '', $password = NULL)
  1533. {
  1534. if (empty($username))
  1535. {
  1536. log_message('error', 'Library Auth: Failure, empty username');
  1537. return FALSE;
  1538. }
  1539. $auth_library_class = strtolower($this->config->item('auth_library_class'));
  1540. $auth_library_function = strtolower($this->config->item('auth_library_function'));
  1541. if (empty($auth_library_class))
  1542. {
  1543. log_message('debug', 'Library Auth: Failure, empty auth_library_class');
  1544. return FALSE;
  1545. }
  1546. if (empty($auth_library_function))
  1547. {
  1548. log_message('debug', 'Library Auth: Failure, empty auth_library_function');
  1549. return FALSE;
  1550. }
  1551. if (is_callable([$auth_library_class, $auth_library_function]) === FALSE)
  1552. {
  1553. $this->load->library($auth_library_class);
  1554. }
  1555. return $this->{$auth_library_class}->$auth_library_function($username, $password);
  1556. }
  1557. /**
  1558. * Check if the user is logged in
  1559. *
  1560. * @access protected
  1561. * @param string $username The user's name
  1562. * @param bool|string $password The user's password
  1563. * @return bool
  1564. */
  1565. protected function _check_login($username = NULL, $password = FALSE)
  1566. {
  1567. if (empty($username))
  1568. {
  1569. return FALSE;
  1570. }
  1571. $auth_source = strtolower($this->config->item('auth_source'));
  1572. $rest_auth = strtolower($this->config->item('rest_auth'));
  1573. $valid_logins = $this->config->item('rest_valid_logins');
  1574. if (!$this->config->item('auth_source') && $rest_auth === 'digest')
  1575. {
  1576. // For digest we do not have a password passed as argument
  1577. return md5($username . ':' . $this->config->item('rest_realm') . ':' . (isset($valid_logins[$username]) ? $valid_logins[$username] : ''));
  1578. }
  1579. if ($password === FALSE)
  1580. {
  1581. return FALSE;
  1582. }
  1583. if ($auth_source === 'ldap')
  1584. {
  1585. log_message('debug', "Performing LDAP authentication for $username");
  1586. return $this->_perform_ldap_auth($username, $password);
  1587. }
  1588. if ($auth_source === 'library')
  1589. {
  1590. log_message('debug', "Performing Library authentication for $username");
  1591. return $this->_perform_library_auth($username, $password);
  1592. }
  1593. if (array_key_exists($username, $valid_logins) === FALSE)
  1594. {
  1595. return FALSE;
  1596. }
  1597. if ($valid_logins[$username] !== $password)
  1598. {
  1599. return FALSE;
  1600. }
  1601. return TRUE;
  1602. }
  1603. /**
  1604. * Check to see if the user is logged in with a PHP session key
  1605. *
  1606. * @access protected
  1607. * @return void
  1608. */
  1609. protected function _check_php_session()
  1610. {
  1611. // Get the auth_source config item
  1612. $key = $this->config->item('auth_source');
  1613. // If falsy, then the user isn't logged in
  1614. if (!$this->session->userdata($key))
  1615. {
  1616. // Display an error response
  1617. $this->response([
  1618. $this->config->item('rest_status_field_name') => FALSE,
  1619. $this->config->item('rest_message_field_name') => $this->lang->line('text_rest_unauthorized')
  1620. ], self::HTTP_UNAUTHORIZED);
  1621. }
  1622. }
  1623. /**
  1624. * Prepares for basic authentication
  1625. *
  1626. * @access protected
  1627. * @return void
  1628. */
  1629. protected function _prepare_basic_auth()
  1630. {
  1631. // If whitelist is enabled it has the first chance to kick them out
  1632. if ($this->config->item('rest_ip_whitelist_enabled'))
  1633. {
  1634. $this->_check_whitelist_auth();
  1635. }
  1636. // Returns NULL if the SERVER variables PHP_AUTH_USER and HTTP_AUTHENTICATION don't exist
  1637. $username = $this->input->server('PHP_AUTH_USER');
  1638. $http_auth = $this->input->server('HTTP_AUTHENTICATION');
  1639. $password = NULL;
  1640. if ($username !== NULL)
  1641. {
  1642. $password = $this->input->server('PHP_AUTH_PW');
  1643. }
  1644. elseif ($http_auth !== NULL)
  1645. {
  1646. // If the authentication header is set as basic, then extract the username and password from
  1647. // HTTP_AUTHORIZATION e.g. my_username:my_password. This is passed in the .htaccess file
  1648. if (strpos(strtolower($http_auth), 'basic') === 0)
  1649. {
  1650. // Search online for HTTP_AUTHORIZATION workaround to explain what this is doing
  1651. list($username, $password) = explode(':', base64_decode(substr($this->input->server('HTTP_AUTHORIZATION'), 6)));
  1652. }
  1653. }
  1654. // Check if the user is logged into the system
  1655. if ($this->_check_login($username, $password) === FALSE)
  1656. {
  1657. $this->_force_login();
  1658. }
  1659. }
  1660. /**
  1661. * Prepares for digest authentication
  1662. *
  1663. * @access protected
  1664. * @return void
  1665. */
  1666. protected function _prepare_digest_auth()
  1667. {
  1668. // If whitelist is enabled it has the first chance to kick them out
  1669. if ($this->config->item('rest_ip_whitelist_enabled'))
  1670. {
  1671. $this->_check_whitelist_auth();
  1672. }
  1673. // We need to test which server authentication variable to use,
  1674. // because the PHP ISAPI module in IIS acts different from CGI
  1675. $digest_string = $this->input->server('PHP_AUTH_DIGEST');
  1676. if ($digest_string === NULL)
  1677. {
  1678. $digest_string = $this->input->server('HTTP_AUTHORIZATION');
  1679. }
  1680. $unique_id = uniqid();
  1681. // The $_SESSION['error_prompted'] variable is used to ask the password
  1682. // again if none given or if the user enters wrong auth information
  1683. if (empty($digest_string))
  1684. {
  1685. $this->_force_login($unique_id);
  1686. }
  1687. // We need to retrieve authentication data from the $digest_string variable
  1688. $matches = [];
  1689. preg_match_all('@(username|nonce|uri|nc|cnonce|qop|response)=[\'"]?([^\'",]+)@', $digest_string, $matches);
  1690. $digest = (empty($matches[1]) || empty($matches[2])) ? [] : array_combine($matches[1], $matches[2]);
  1691. // For digest authentication the library function should return already stored md5(username:restrealm:password) for that username @see rest.php::auth_library_function config
  1692. $username = $this->_check_login($digest['username'], TRUE);
  1693. if (array_key_exists('username', $digest) === FALSE || $username === FALSE)
  1694. {
  1695. $this->_force_login($unique_id);
  1696. }
  1697. $md5 = md5(strtoupper($this->request->method) . ':' . $digest['uri']);
  1698. $valid_response = md5($username . ':' . $digest['nonce'] . ':' . $digest['nc'] . ':' . $digest['cnonce'] . ':' . $digest['qop'] . ':' . $md5);
  1699. // Check if the string don't compare (case-insensitive)
  1700. if (strcasecmp($digest['response'], $valid_response) !== 0)
  1701. {
  1702. // Display an error response
  1703. $this->response([
  1704. $this->config->item('rest_status_field_name') => FALSE,
  1705. $this->config->item('rest_message_field_name') => $this->lang->line('text_rest_invalid_credentials')
  1706. ], self::HTTP_UNAUTHORIZED);
  1707. }
  1708. }
  1709. /**
  1710. * Checks if the client's ip is in the 'rest_ip_blacklist' config and generates a 401 response
  1711. *
  1712. * @access protected
  1713. * @return void
  1714. */
  1715. protected function _check_blacklist_auth()
  1716. {
  1717. // Match an ip address in a blacklist e.g. 127.0.0.0, 0.0.0.0
  1718. $pattern = sprintf('/(?:,\s*|^)\Q%s\E(?=,\s*|$)/m', $this->input->ip_address());
  1719. // Returns 1, 0 or FALSE (on error only). Therefore implicitly convert 1 to TRUE
  1720. if (preg_match($pattern, $this->config->item('rest_ip_blacklist')))
  1721. {
  1722. // Display an error response
  1723. $this->response([
  1724. $this->config->item('rest_status_field_name') => FALSE,
  1725. $this->config->item('rest_message_field_name') => $this->lang->line('text_rest_ip_denied')
  1726. ], self::HTTP_UNAUTHORIZED);
  1727. }
  1728. }
  1729. /**
  1730. * Check if the client's ip is in the 'rest_ip_whitelist' config and generates a 401 response
  1731. *
  1732. * @access protected
  1733. * @return void
  1734. */
  1735. protected function _check_whitelist_auth()
  1736. {
  1737. $whitelist = explode(',', $this->config->item('rest_ip_whitelist'));
  1738. array_push($whitelist, '127.0.0.1', '0.0.0.0');
  1739. foreach ($whitelist as &$ip)
  1740. {
  1741. // As $ip is a reference, trim leading and trailing whitespace, then store the new value
  1742. // using the reference
  1743. $ip = trim($ip);
  1744. }
  1745. if (in_array($this->input->ip_address(), $whitelist) === FALSE)
  1746. {
  1747. $this->response([
  1748. $this->config->item('rest_status_field_name') => FALSE,
  1749. $this->config->item('rest_message_field_name') => $this->lang->line('text_rest_ip_unauthorized')
  1750. ], self::HTTP_UNAUTHORIZED);
  1751. }
  1752. }
  1753. /**
  1754. * Force logging in by setting the WWW-Authenticate header
  1755. *
  1756. * @access protected
  1757. * @param string $nonce A server-specified data string which should be uniquely generated
  1758. * each time
  1759. * @return void
  1760. */
  1761. protected function _force_login($nonce = '')
  1762. {
  1763. $rest_auth = $this->config->item('rest_auth');
  1764. $rest_realm = $this->config->item('rest_realm');
  1765. if (strtolower($rest_auth) === 'basic')
  1766. {
  1767. // See http://tools.ietf.org/html/rfc2617#page-5
  1768. header('WWW-Authenticate: Basic realm="' . $rest_realm . '"');
  1769. }
  1770. elseif (strtolower($rest_auth) === 'digest')
  1771. {
  1772. // See http://tools.ietf.org/html/rfc2617#page-18
  1773. header(
  1774. 'WWW-Authenticate: Digest realm="' . $rest_realm
  1775. . '", qop="auth", nonce="' . $nonce
  1776. . '", opaque="' . md5($rest_realm) . '"');
  1777. }
  1778. // Display an error response
  1779. $this->response([
  1780. $this->config->item('rest_status_field_name') => FALSE,
  1781. $this->config->item('rest_message_field_name') => $this->lang->line('text_rest_unauthorized')
  1782. ], self::HTTP_UNAUTHORIZED);
  1783. }
  1784. /**
  1785. * Updates the log table with the total access time
  1786. *
  1787. * @access protected
  1788. * @author Chris Kacerguis
  1789. * @return bool TRUE log table updated; otherwise, FALSE
  1790. */
  1791. protected function _log_access_time()
  1792. {
  1793. $payload['rtime'] = $this->_end_rtime - $this->_start_rtime;
  1794. return $this->rest->db->update(
  1795. $this->config->item('rest_logs_table'), $payload, [
  1796. 'id' => $this->_insert_id
  1797. ]);
  1798. }
  1799. /**
  1800. * Updates the log table with HTTP response code
  1801. *
  1802. * @access protected
  1803. * @author Justin Chen
  1804. * @param $http_code int HTTP status code
  1805. * @return bool TRUE log table updated; otherwise, FALSE
  1806. */
  1807. protected function _log_response_code($http_code)
  1808. {
  1809. $payload['response_code'] = $http_code;
  1810. return $this->rest->db->update(
  1811. $this->config->item('rest_logs_table'), $payload, [
  1812. 'id' => $this->_insert_id
  1813. ]);
  1814. }
  1815. /**
  1816. * Check to see if the API key has access to the controller and methods
  1817. *
  1818. * @access protected
  1819. * @return bool TRUE the API key has access; otherwise, FALSE
  1820. */
  1821. protected function _check_access()
  1822. {
  1823. // If we don't want to check access, just return TRUE
  1824. if ($this->config->item('rest_enable_access') === FALSE)
  1825. {
  1826. return TRUE;
  1827. }
  1828. // Fetch controller based on path and controller name
  1829. $controller = implode(
  1830. '/', [
  1831. $this->router->directory,
  1832. $this->router->class
  1833. ]);
  1834. // Remove any double slashes for safety
  1835. $controller = str_replace('//', '/', $controller);
  1836. // Query the access table and get the number of results
  1837. return $this->rest->db
  1838. ->where('key', $this->rest->key)
  1839. ->where('controller', $controller)
  1840. ->get($this->config->item('rest_access_table'))
  1841. ->num_rows() > 0;
  1842. }
  1843. /**
  1844. * 에러 리턴
  1845. * @param $message
  1846. * @param int $error_code
  1847. */
  1848. public function error_return( $message, $error_code = 400 )
  1849. {
  1850. $result = array(
  1851. "result" => FALSE,
  1852. "message" => $message
  1853. );
  1854. $this->response($result, $error_code);
  1855. }
  1856. }