PageRenderTime 64ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/application/libraries/REST_Controller.php

http://github.com/philsturgeon/codeigniter-restserver
PHP | 2237 lines | 1174 code | 314 blank | 749 comment | 181 complexity | 971d6cb896a3f0d476b9fe9a914ff7c1 MD5 | raw file
Possible License(s): MIT

Large files files are truncated, but you can click here to view the full file

  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. * Whether or not to perform a CORS check and apply CORS headers to the request
  278. *
  279. * @var bool
  280. */
  281. protected $check_cors = NULL;
  282. /**
  283. * Enable XSS flag
  284. * Determines whether the XSS filter is always active when
  285. * GET, OPTIONS, HEAD, POST, PUT, DELETE and PATCH data is encountered
  286. * Set automatically based on config setting
  287. *
  288. * @var bool
  289. */
  290. protected $_enable_xss = FALSE;
  291. /**
  292. * HTTP status codes and their respective description
  293. * Note: Only the widely used HTTP status codes are used
  294. *
  295. * @var array
  296. * @link http://www.restapitutorial.com/httpstatuscodes.html
  297. */
  298. protected $http_status_codes = [
  299. self::HTTP_OK => 'OK',
  300. self::HTTP_CREATED => 'CREATED',
  301. self::HTTP_NO_CONTENT => 'NO CONTENT',
  302. self::HTTP_NOT_MODIFIED => 'NOT MODIFIED',
  303. self::HTTP_BAD_REQUEST => 'BAD REQUEST',
  304. self::HTTP_UNAUTHORIZED => 'UNAUTHORIZED',
  305. self::HTTP_FORBIDDEN => 'FORBIDDEN',
  306. self::HTTP_NOT_FOUND => 'NOT FOUND',
  307. self::HTTP_METHOD_NOT_ALLOWED => 'METHOD NOT ALLOWED',
  308. self::HTTP_NOT_ACCEPTABLE => 'NOT ACCEPTABLE',
  309. self::HTTP_CONFLICT => 'CONFLICT',
  310. self::HTTP_INTERNAL_SERVER_ERROR => 'INTERNAL SERVER ERROR',
  311. self::HTTP_NOT_IMPLEMENTED => 'NOT IMPLEMENTED'
  312. ];
  313. /**
  314. * Extend this function to apply additional checking early on in the process
  315. *
  316. * @access protected
  317. * @return void
  318. */
  319. protected function early_checks()
  320. {
  321. }
  322. /**
  323. * Constructor for the REST API
  324. *
  325. * @access public
  326. * @param string $config Configuration filename minus the file extension
  327. * e.g: my_rest.php is passed as 'my_rest'
  328. * @return void
  329. */
  330. public function __construct($config = 'rest')
  331. {
  332. parent::__construct();
  333. $this->preflight_checks();
  334. // Set the default value of global xss filtering. Same approach as CodeIgniter 3
  335. $this->_enable_xss = ($this->config->item('global_xss_filtering') === TRUE);
  336. // Don't try to parse template variables like {elapsed_time} and {memory_usage}
  337. // when output is displayed for not damaging data accidentally
  338. $this->output->parse_exec_vars = FALSE;
  339. // Start the timer for how long the request takes
  340. $this->_start_rtime = microtime(TRUE);
  341. // Load the rest.php configuration file
  342. $this->load->config($config);
  343. // At present the library is bundled with REST_Controller 2.5+, but will eventually be part of CodeIgniter (no citation)
  344. $this->load->library('format');
  345. // Determine supported output formats from configuration
  346. $supported_formats = $this->config->item('rest_supported_formats');
  347. // Validate the configuration setting output formats
  348. if (empty($supported_formats))
  349. {
  350. $supported_formats = [];
  351. }
  352. if ( ! is_array($supported_formats))
  353. {
  354. $supported_formats = [$supported_formats];
  355. }
  356. // Add silently the default output format if it is missing
  357. $default_format = $this->_get_default_output_format();
  358. if (!in_array($default_format, $supported_formats))
  359. {
  360. $supported_formats[] = $default_format;
  361. }
  362. // Now update $this->_supported_formats
  363. $this->_supported_formats = array_intersect_key($this->_supported_formats, array_flip($supported_formats));
  364. // Get the language
  365. $language = $this->config->item('rest_language');
  366. if ($language === NULL)
  367. {
  368. $language = 'english';
  369. }
  370. // Load the language file
  371. $this->lang->load('rest_controller', $language);
  372. // Initialise the response, request and rest objects
  373. $this->request = new stdClass();
  374. $this->response = new stdClass();
  375. $this->rest = new stdClass();
  376. // Check to see if the current IP address is blacklisted
  377. if ($this->config->item('rest_ip_blacklist_enabled') === TRUE)
  378. {
  379. $this->_check_blacklist_auth();
  380. }
  381. // Determine whether the connection is HTTPS
  382. $this->request->ssl = is_https();
  383. // How is this request being made? GET, POST, PATCH, DELETE, INSERT, PUT, HEAD or OPTIONS
  384. $this->request->method = $this->_detect_method();
  385. // Check for CORS access request
  386. $check_cors = $this->config->item('check_cors');
  387. if ($check_cors === TRUE)
  388. {
  389. $this->_check_cors();
  390. }
  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. //get header vars
  413. $this->_head_args = $this->input->request_headers();
  414. // Merge both for one mega-args variable
  415. $this->_args = array_merge(
  416. $this->_get_args,
  417. $this->_options_args,
  418. $this->_patch_args,
  419. $this->_head_args,
  420. $this->_put_args,
  421. $this->_post_args,
  422. $this->_delete_args,
  423. $this->{'_'.$this->request->method.'_args'}
  424. );
  425. // Which format should the data be returned in?
  426. $this->response->format = $this->_detect_output_format();
  427. // Which language should the data be returned in?
  428. $this->response->lang = $this->_detect_lang();
  429. // Extend this function to apply additional checking early on in the process
  430. $this->early_checks();
  431. // Load DB if its enabled
  432. if ($this->config->item('rest_database_group') && ($this->config->item('rest_enable_keys') || $this->config->item('rest_enable_logging')))
  433. {
  434. $this->rest->db = $this->load->database($this->config->item('rest_database_group'), TRUE);
  435. }
  436. // Use whatever database is in use (isset returns FALSE)
  437. elseif (property_exists($this, 'db'))
  438. {
  439. $this->rest->db = $this->db;
  440. }
  441. // Check if there is a specific auth type for the current class/method
  442. // _auth_override_check could exit so we need $this->rest->db initialized before
  443. $this->auth_override = $this->_auth_override_check();
  444. // Checking for keys? GET TO WorK!
  445. // Skip keys test for $config['auth_override_class_method']['class'['method'] = 'none'
  446. if ($this->config->item('rest_enable_keys') && $this->auth_override !== TRUE)
  447. {
  448. $this->_allow = $this->_detect_api_key();
  449. }
  450. // Only allow ajax requests
  451. if ($this->input->is_ajax_request() === FALSE && $this->config->item('rest_ajax_only'))
  452. {
  453. // Display an error response
  454. $this->response([
  455. $this->config->item('rest_status_field_name') => FALSE,
  456. $this->config->item('rest_message_field_name') => $this->lang->line('text_rest_ajax_only')
  457. ], self::HTTP_NOT_ACCEPTABLE);
  458. }
  459. // When there is no specific override for the current class/method, use the default auth value set in the config
  460. if ($this->auth_override === FALSE &&
  461. (! ($this->config->item('rest_enable_keys') && $this->_allow === TRUE) ||
  462. ($this->config->item('allow_auth_and_keys') === TRUE && $this->_allow === TRUE)))
  463. {
  464. $rest_auth = strtolower($this->config->item('rest_auth'));
  465. switch ($rest_auth)
  466. {
  467. case 'basic':
  468. $this->_prepare_basic_auth();
  469. break;
  470. case 'digest':
  471. $this->_prepare_digest_auth();
  472. break;
  473. case 'session':
  474. $this->_check_php_session();
  475. break;
  476. }
  477. if ($this->config->item('rest_ip_whitelist_enabled') === TRUE)
  478. {
  479. $this->_check_whitelist_auth();
  480. }
  481. }
  482. }
  483. /**
  484. * Deconstructor
  485. *
  486. * @author Chris Kacerguis
  487. * @access public
  488. * @return void
  489. */
  490. public function __destruct()
  491. {
  492. // Get the current timestamp
  493. $this->_end_rtime = microtime(TRUE);
  494. // Log the loading time to the log table
  495. if ($this->config->item('rest_enable_logging') === TRUE)
  496. {
  497. $this->_log_access_time();
  498. }
  499. }
  500. /**
  501. * Checks to see if we have everything we need to run this library.
  502. *
  503. * @access protected
  504. * @return Exception
  505. */
  506. protected function preflight_checks()
  507. {
  508. // Check to see if PHP is equal to or greater than 5.4.x
  509. if (is_php('5.4') === FALSE)
  510. {
  511. // CodeIgniter 3 is recommended for v5.4 or above
  512. throw new Exception('Using PHP v'.PHP_VERSION.', though PHP v5.4 or greater is required');
  513. }
  514. // Check to see if this is CI 3.x
  515. if (explode('.', CI_VERSION, 2)[0] < 3)
  516. {
  517. throw new Exception('REST Server requires CodeIgniter 3.x');
  518. }
  519. }
  520. /**
  521. * Requests are not made to methods directly, the request will be for
  522. * an "object". This simply maps the object and method to the correct
  523. * Controller method
  524. *
  525. * @access public
  526. * @param string $object_called
  527. * @param array $arguments The arguments passed to the controller method
  528. */
  529. public function _remap($object_called, $arguments = [])
  530. {
  531. // Should we answer if not over SSL?
  532. if ($this->config->item('force_https') && $this->request->ssl === FALSE)
  533. {
  534. $this->response([
  535. $this->config->item('rest_status_field_name') => FALSE,
  536. $this->config->item('rest_message_field_name') => $this->lang->line('text_rest_unsupported')
  537. ], self::HTTP_FORBIDDEN);
  538. }
  539. // Remove the supported format from the function name e.g. index.json => index
  540. $object_called = preg_replace('/^(.*)\.(?:'.implode('|', array_keys($this->_supported_formats)).')$/', '$1', $object_called);
  541. $controller_method = $object_called.'_'.$this->request->method;
  542. // Do we want to log this method (if allowed by config)?
  543. $log_method = ! (isset($this->methods[$controller_method]['log']) && $this->methods[$controller_method]['log'] === FALSE);
  544. // Use keys for this method?
  545. $use_key = ! (isset($this->methods[$controller_method]['key']) && $this->methods[$controller_method]['key'] === FALSE);
  546. // They provided a key, but it wasn't valid, so get them out of here
  547. if ($this->config->item('rest_enable_keys') && $use_key && $this->_allow === FALSE)
  548. {
  549. if ($this->config->item('rest_enable_logging') && $log_method)
  550. {
  551. $this->_log_request();
  552. }
  553. $this->response([
  554. $this->config->item('rest_status_field_name') => FALSE,
  555. $this->config->item('rest_message_field_name') => sprintf($this->lang->line('text_rest_invalid_api_key'), $this->rest->key)
  556. ], self::HTTP_FORBIDDEN);
  557. }
  558. // Check to see if this key has access to the requested controller
  559. if ($this->config->item('rest_enable_keys') && $use_key && empty($this->rest->key) === FALSE && $this->_check_access() === FALSE)
  560. {
  561. if ($this->config->item('rest_enable_logging') && $log_method)
  562. {
  563. $this->_log_request();
  564. }
  565. $this->response([
  566. $this->config->item('rest_status_field_name') => FALSE,
  567. $this->config->item('rest_message_field_name') => $this->lang->line('text_rest_api_key_unauthorized')
  568. ], self::HTTP_UNAUTHORIZED);
  569. }
  570. // Sure it exists, but can they do anything with it?
  571. if (! method_exists($this, $controller_method))
  572. {
  573. $this->response([
  574. $this->config->item('rest_status_field_name') => FALSE,
  575. $this->config->item('rest_message_field_name') => $this->lang->line('text_rest_unknown_method')
  576. ], self::HTTP_METHOD_NOT_ALLOWED);
  577. }
  578. // Doing key related stuff? Can only do it if they have a key right?
  579. if ($this->config->item('rest_enable_keys') && empty($this->rest->key) === FALSE)
  580. {
  581. // Check the limit
  582. if ($this->config->item('rest_enable_limits') && $this->_check_limit($controller_method) === FALSE)
  583. {
  584. $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')];
  585. $this->response($response, self::HTTP_UNAUTHORIZED);
  586. }
  587. // If no level is set use 0, they probably aren't using permissions
  588. $level = isset($this->methods[$controller_method]['level']) ? $this->methods[$controller_method]['level'] : 0;
  589. // If no level is set, or it is lower than/equal to the key's level
  590. $authorized = $level <= $this->rest->level;
  591. // IM TELLIN!
  592. if ($this->config->item('rest_enable_logging') && $log_method)
  593. {
  594. $this->_log_request($authorized);
  595. }
  596. if($authorized === FALSE)
  597. {
  598. // They don't have good enough perms
  599. $response = [$this->config->item('rest_status_field_name') => FALSE, $this->config->item('rest_message_field_name') => $this->lang->line('text_rest_api_key_permissions')];
  600. $this->response($response, self::HTTP_UNAUTHORIZED);
  601. }
  602. }
  603. // No key stuff, but record that stuff is happening
  604. elseif ($this->config->item('rest_enable_logging') && $log_method)
  605. {
  606. $this->_log_request($authorized = TRUE);
  607. }
  608. // Call the controller method and passed arguments
  609. try
  610. {
  611. call_user_func_array([$this, $controller_method], $arguments);
  612. }
  613. catch (Exception $ex)
  614. {
  615. // If the method doesn't exist, then the error will be caught and an error response shown
  616. $this->response([
  617. $this->config->item('rest_status_field_name') => FALSE,
  618. $this->config->item('rest_message_field_name') => [
  619. 'classname' => get_class($ex),
  620. 'message' => $ex->getMessage()
  621. ]
  622. ], self::HTTP_INTERNAL_SERVER_ERROR);
  623. }
  624. }
  625. /**
  626. * Takes mixed data and optionally a status code, then creates the response
  627. *
  628. * @access public
  629. * @param array|NULL $data Data to output to the user
  630. * @param int|NULL $http_code HTTP status code
  631. * @param bool $continue TRUE to flush the response to the client and continue
  632. * running the script; otherwise, exit
  633. */
  634. public function response($data = NULL, $http_code = NULL, $continue = FALSE)
  635. {
  636. // If the HTTP status is not NULL, then cast as an integer
  637. if ($http_code !== NULL)
  638. {
  639. // So as to be safe later on in the process
  640. $http_code = (int) $http_code;
  641. }
  642. // Set the output as NULL by default
  643. $output = NULL;
  644. // If data is NULL and no HTTP status code provided, then display, error and exit
  645. if ($data === NULL && $http_code === NULL)
  646. {
  647. $http_code = self::HTTP_NOT_FOUND;
  648. }
  649. // If data is not NULL and a HTTP status code provided, then continue
  650. elseif ($data !== NULL)
  651. {
  652. // If the format method exists, call and return the output in that format
  653. if (method_exists($this->format, 'to_' . $this->response->format))
  654. {
  655. // Set the format header
  656. $this->output->set_content_type($this->_supported_formats[$this->response->format], strtolower($this->config->item('charset')));
  657. $output = $this->format->factory($data)->{'to_' . $this->response->format}();
  658. // An array must be parsed as a string, so as not to cause an array to string error
  659. // Json is the most appropriate form for such a datatype
  660. if ($this->response->format === 'array')
  661. {
  662. $output = $this->format->factory($output)->{'to_json'}();
  663. }
  664. }
  665. else
  666. {
  667. // If an array or object, then parse as a json, so as to be a 'string'
  668. if (is_array($data) || is_object($data))
  669. {
  670. $data = $this->format->factory($data)->{'to_json'}();
  671. }
  672. // Format is not supported, so output the raw data as a string
  673. $output = $data;
  674. }
  675. }
  676. // If not greater than zero, then set the HTTP status code as 200 by default
  677. // Though perhaps 500 should be set instead, for the developer not passing a
  678. // correct HTTP status code
  679. $http_code > 0 || $http_code = self::HTTP_OK;
  680. $this->output->set_status_header($http_code);
  681. // JC: Log response code only if rest logging enabled
  682. if ($this->config->item('rest_enable_logging') === TRUE)
  683. {
  684. $this->_log_response_code($http_code);
  685. }
  686. // Output the data
  687. $this->output->set_output($output);
  688. if ($continue === FALSE)
  689. {
  690. // Display the data and exit execution
  691. $this->output->_display();
  692. exit;
  693. }
  694. // Otherwise dump the output automatically
  695. }
  696. /**
  697. * Takes mixed data and optionally a status code, then creates the response
  698. * within the buffers of the Output class. The response is sent to the client
  699. * lately by the framework, after the current controller's method termination.
  700. * All the hooks after the controller's method termination are executable
  701. *
  702. * @access public
  703. * @param array|NULL $data Data to output to the user
  704. * @param int|NULL $http_code HTTP status code
  705. */
  706. public function set_response($data = NULL, $http_code = NULL)
  707. {
  708. $this->response($data, $http_code, TRUE);
  709. }
  710. /**
  711. * Get the input format e.g. json or xml
  712. *
  713. * @access protected
  714. * @return string|NULL Supported input format; otherwise, NULL
  715. */
  716. protected function _detect_input_format()
  717. {
  718. // Get the CONTENT-TYPE value from the SERVER variable
  719. $content_type = $this->input->server('CONTENT_TYPE');
  720. if (empty($content_type) === FALSE)
  721. {
  722. // If a semi-colon exists in the string, then explode by ; and get the value of where
  723. // the current array pointer resides. This will generally be the first element of the array
  724. $content_type = (strpos($content_type, ';') !== FALSE ? current(explode(';', $content_type)) : $content_type);
  725. // Check all formats against the CONTENT-TYPE header
  726. foreach ($this->_supported_formats as $type => $mime)
  727. {
  728. // $type = format e.g. csv
  729. // $mime = mime type e.g. application/csv
  730. // If both the mime types match, then return the format
  731. if ($content_type === $mime)
  732. {
  733. return $type;
  734. }
  735. }
  736. }
  737. return NULL;
  738. }
  739. /**
  740. * Gets the default format from the configuration. Fallbacks to 'json'
  741. * if the corresponding configuration option $config['rest_default_format']
  742. * is missing or is empty
  743. *
  744. * @access protected
  745. * @return string The default supported input format
  746. */
  747. protected function _get_default_output_format()
  748. {
  749. $default_format = (string) $this->config->item('rest_default_format');
  750. return $default_format === '' ? 'json' : $default_format;
  751. }
  752. /**
  753. * Detect which format should be used to output the data
  754. *
  755. * @access protected
  756. * @return mixed|NULL|string Output format
  757. */
  758. protected function _detect_output_format()
  759. {
  760. // Concatenate formats to a regex pattern e.g. \.(csv|json|xml)
  761. $pattern = '/\.('.implode('|', array_keys($this->_supported_formats)).')($|\/)/';
  762. $matches = [];
  763. // Check if a file extension is used e.g. http://example.com/api/index.json?param1=param2
  764. if (preg_match($pattern, $this->uri->uri_string(), $matches))
  765. {
  766. return $matches[1];
  767. }
  768. // Get the format parameter named as 'format'
  769. if (isset($this->_get_args['format']))
  770. {
  771. $format = strtolower($this->_get_args['format']);
  772. if (isset($this->_supported_formats[$format]) === TRUE)
  773. {
  774. return $format;
  775. }
  776. }
  777. // Get the HTTP_ACCEPT server variable
  778. $http_accept = $this->input->server('HTTP_ACCEPT');
  779. // Otherwise, check the HTTP_ACCEPT server variable
  780. if ($this->config->item('rest_ignore_http_accept') === FALSE && $http_accept !== NULL)
  781. {
  782. // Check all formats against the HTTP_ACCEPT header
  783. foreach (array_keys($this->_supported_formats) as $format)
  784. {
  785. // Has this format been requested?
  786. if (strpos($http_accept, $format) !== FALSE)
  787. {
  788. if ($format !== 'html' && $format !== 'xml')
  789. {
  790. // If not HTML or XML assume it's correct
  791. return $format;
  792. }
  793. elseif ($format === 'html' && strpos($http_accept, 'xml') === FALSE)
  794. {
  795. // HTML or XML have shown up as a match
  796. // If it is truly HTML, it wont want any XML
  797. return $format;
  798. }
  799. else if ($format === 'xml' && strpos($http_accept, 'html') === FALSE)
  800. {
  801. // If it is truly XML, it wont want any HTML
  802. return $format;
  803. }
  804. }
  805. }
  806. }
  807. // Check if the controller has a default format
  808. if (empty($this->rest_format) === FALSE)
  809. {
  810. return $this->rest_format;
  811. }
  812. // Obtain the default format from the configuration
  813. return $this->_get_default_output_format();
  814. }
  815. /**
  816. * Get the HTTP request string e.g. get or post
  817. *
  818. * @access protected
  819. * @return string|NULL Supported request method as a lowercase string; otherwise, NULL if not supported
  820. */
  821. protected function _detect_method()
  822. {
  823. // Declare a variable to store the method
  824. $method = NULL;
  825. // Determine whether the 'enable_emulate_request' setting is enabled
  826. if ($this->config->item('enable_emulate_request') === TRUE)
  827. {
  828. $method = $this->input->post('_method');
  829. if ($method === NULL)
  830. {
  831. $method = $this->input->server('HTTP_X_HTTP_METHOD_OVERRIDE');
  832. }
  833. $method = strtolower($method);
  834. }
  835. if (empty($method))
  836. {
  837. // Get the request method as a lowercase string
  838. $method = $this->input->method();
  839. }
  840. return in_array($method, $this->allowed_http_methods) && method_exists($this, '_parse_' . $method) ? $method : 'get';
  841. }
  842. /**
  843. * See if the user has provided an API key
  844. *
  845. * @access protected
  846. * @return bool
  847. */
  848. protected function _detect_api_key()
  849. {
  850. // Get the api key name variable set in the rest config file
  851. $api_key_variable = $this->config->item('rest_key_name');
  852. // Work out the name of the SERVER entry based on config
  853. $key_name = 'HTTP_' . strtoupper(str_replace('-', '_', $api_key_variable));
  854. $this->rest->key = NULL;
  855. $this->rest->level = NULL;
  856. $this->rest->user_id = NULL;
  857. $this->rest->ignore_limits = FALSE;
  858. // Find the key from server or arguments
  859. if (($key = isset($this->_args[$api_key_variable]) ? $this->_args[$api_key_variable] : $this->input->server($key_name)))
  860. {
  861. if ( ! ($row = $this->rest->db->where($this->config->item('rest_key_column'), $key)->get($this->config->item('rest_keys_table'))->row()))
  862. {
  863. return FALSE;
  864. }
  865. $this->rest->key = $row->{$this->config->item('rest_key_column')};
  866. isset($row->user_id) && $this->rest->user_id = $row->user_id;
  867. isset($row->level) && $this->rest->level = $row->level;
  868. isset($row->ignore_limits) && $this->rest->ignore_limits = $row->ignore_limits;
  869. $this->_apiuser = $row;
  870. /*
  871. * If "is private key" is enabled, compare the ip address with the list
  872. * of valid ip addresses stored in the database
  873. */
  874. if (empty($row->is_private_key) === FALSE)
  875. {
  876. // Check for a list of valid ip addresses
  877. if (isset($row->ip_addresses))
  878. {
  879. // multiple ip addresses must be separated using a comma, explode and loop
  880. $list_ip_addresses = explode(',', $row->ip_addresses);
  881. $found_address = FALSE;
  882. foreach ($list_ip_addresses as $ip_address)
  883. {
  884. if ($this->input->ip_address() === trim($ip_address))
  885. {
  886. // there is a match, set the the value to TRUE and break out of the loop
  887. $found_address = TRUE;
  888. break;
  889. }
  890. }
  891. return $found_address;
  892. }
  893. else
  894. {
  895. // There should be at least one IP address for this private key
  896. return FALSE;
  897. }
  898. }
  899. return TRUE;
  900. }
  901. // No key has been sent
  902. return FALSE;
  903. }
  904. /**
  905. * Preferred return language
  906. *
  907. * @access protected
  908. * @return string|NULL The language code
  909. */
  910. protected function _detect_lang()
  911. {
  912. $lang = $this->input->server('HTTP_ACCEPT_LANGUAGE');
  913. if ($lang === NULL)
  914. {
  915. return NULL;
  916. }
  917. // It appears more than one language has been sent using a comma delimiter
  918. if (strpos($lang, ',') !== FALSE)
  919. {
  920. $langs = explode(',', $lang);
  921. $return_langs = [];
  922. foreach ($langs as $lang)
  923. {
  924. // Remove weight and trim leading and trailing whitespace
  925. list($lang) = explode(';', $lang);
  926. $return_langs[] = trim($lang);
  927. }
  928. return $return_langs;
  929. }
  930. // Otherwise simply return as a string
  931. return $lang;
  932. }
  933. /**
  934. * Add the request to the log table
  935. *
  936. * @access protected
  937. * @param bool $authorized TRUE the user is authorized; otherwise, FALSE
  938. * @return bool TRUE the data was inserted; otherwise, FALSE
  939. */
  940. protected function _log_request($authorized = FALSE)
  941. {
  942. // Insert the request into the log table
  943. $is_inserted = $this->rest->db
  944. ->insert(
  945. $this->config->item('rest_logs_table'), [
  946. 'uri' => $this->uri->uri_string(),
  947. 'method' => $this->request->method,
  948. 'params' => $this->_args ? ($this->config->item('rest_logs_json_params') === TRUE ? json_encode($this->_args) : serialize($this->_args)) : NULL,
  949. 'api_key' => isset($this->rest->key) ? $this->rest->key : '',
  950. 'ip_address' => $this->input->ip_address(),
  951. 'time' => time(),
  952. 'authorized' => $authorized
  953. ]);
  954. // Get the last insert id to update at a later stage of the request
  955. $this->_insert_id = $this->rest->db->insert_id();
  956. return $is_inserted;
  957. }
  958. /**
  959. * Check if the requests to a controller method exceed a limit
  960. *
  961. * @access protected
  962. * @param string $controller_method The method being called
  963. * @return bool TRUE the call limit is below the threshold; otherwise, FALSE
  964. */
  965. protected function _check_limit($controller_method)
  966. {
  967. // They are special, or it might not even have a limit
  968. if (empty($this->rest->ignore_limits) === FALSE)
  969. {
  970. // Everything is fine
  971. return TRUE;
  972. }
  973. switch ($this->config->item('rest_limits_method'))
  974. {
  975. case 'API_KEY':
  976. $limited_uri = 'api-key:' . (isset($this->rest->key) ? $this->rest->key : '');
  977. $limited_method_name = isset($this->rest->key) ? $this->rest->key : '';
  978. break;
  979. case 'METHOD_NAME':
  980. $limited_uri = 'method-name:' . $controller_method;
  981. $limited_method_name = $controller_method;
  982. break;
  983. case 'ROUTED_URL':
  984. default:
  985. $limited_uri = $this->uri->ruri_string();
  986. if (strpos(strrev($limited_uri), strrev($this->response->format)) === 0)
  987. {
  988. $limited_uri = substr($limited_uri,0, -strlen($this->response->format) - 1);
  989. }
  990. $limited_uri = 'uri:'.$limited_uri.':'.$this->request->method; // It's good to differentiate GET from PUT
  991. $limited_method_name = $controller_method;
  992. break;
  993. }
  994. if (isset($this->methods[$limited_method_name]['limit']) === FALSE )
  995. {
  996. // Everything is fine
  997. return TRUE;
  998. }
  999. // How many times can you get to this method in a defined time_limit (default: 1 hour)?
  1000. $limit = $this->methods[$limited_method_name]['limit'];
  1001. $time_limit = (isset($this->methods[$limited_method_name]['time']) ? $this->methods[$limited_method_name]['time'] : 3600); // 3600 = 60 * 60
  1002. // Get data about a keys' usage and limit to one row
  1003. $result = $this->rest->db
  1004. ->where('uri', $limited_uri)
  1005. ->where('api_key', $this->rest->key)
  1006. ->get($this->config->item('rest_limits_table'))
  1007. ->row();
  1008. // No calls have been made for this key
  1009. if ($result === NULL)
  1010. {
  1011. // Create a new row for the following key
  1012. $this->rest->db->insert($this->config->item('rest_limits_table'), [
  1013. 'uri' => $limited_uri,
  1014. 'api_key' => isset($this->rest->key) ? $this->rest->key : '',
  1015. 'count' => 1,
  1016. 'hour_started' => time()
  1017. ]);
  1018. }
  1019. // Been a time limit (or by default an hour) since they called
  1020. elseif ($result->hour_started < (time() - $time_limit))
  1021. {
  1022. // Reset the started period and count
  1023. $this->rest->db
  1024. ->where('uri', $limited_uri)
  1025. ->where('api_key', isset($this->rest->key) ? $this->rest->key : '')
  1026. ->set('hour_started', time())
  1027. ->set('count', 1)
  1028. ->update($this->config->item('rest_limits_table'));
  1029. }
  1030. // They have called within the hour, so lets update
  1031. else
  1032. {
  1033. // The limit has been exceeded
  1034. if ($result->count >= $limit)
  1035. {
  1036. return FALSE;
  1037. }
  1038. // Increase the count by one
  1039. $this->rest->db
  1040. ->where('uri', $limited_uri)
  1041. ->where('api_key', $this->rest->key)
  1042. ->set('count', 'count + 1', FALSE)
  1043. ->update($this->config->item('rest_limits_table'));
  1044. }
  1045. return TRUE;
  1046. }
  1047. /**
  1048. * Check if there is a specific auth type set for the current class/method/HTTP-method being called
  1049. *
  1050. * @access protected
  1051. * @return bool
  1052. */
  1053. protected function _auth_override_check()
  1054. {
  1055. // Assign the class/method auth type override array from the config
  1056. $auth_override_class_method = $this->config->item('auth_override_class_method');
  1057. // Check to see if the override array is even populated
  1058. if ( ! empty($auth_override_class_method))
  1059. {
  1060. // Check for wildcard flag for rules for classes
  1061. if ( ! empty($auth_override_class_method[$this->router->class]['*'])) // Check for class overrides
  1062. {
  1063. // No auth override found, prepare nothing but send back a TRUE override flag
  1064. if ($auth_override_class_method[$this->router->class]['*'] === 'none')
  1065. {
  1066. return TRUE;
  1067. }
  1068. // Basic auth override found, prepare basic
  1069. if ($auth_override_class_method[$this->router->class]['*'] === 'basic')
  1070. {
  1071. $this->_prepare_basic_auth();
  1072. return TRUE;
  1073. }
  1074. // Digest auth override found, prepare digest
  1075. if ($auth_override_class_method[$this->router->class]['*'] === 'digest')
  1076. {
  1077. $this->_prepare_digest_auth();
  1078. return TRUE;
  1079. }
  1080. // Session auth override found, check session
  1081. if ($auth_override_class_method[$this->router->class]['*'] === 'session')
  1082. {
  1083. $this->_check_php_session();
  1084. return TRUE;
  1085. }
  1086. // Whitelist auth override found, check client's ip against config whitelist
  1087. if ($auth_override_class_method[$this->router->class]['*'] === 'whitelist')
  1088. {
  1089. $this->_check_whitelist_auth();
  1090. return TRUE;
  1091. }
  1092. }
  1093. // Check to see if there's an override value set for the current class/method being called
  1094. if ( ! empty($auth_override_class_method[$this->router->class][$this->router->method]))
  1095. {
  1096. // None auth override found, prepare nothing but send back a TRUE override flag
  1097. if ($auth_override_class_method[$this->router->class][$this->router->method] === 'none')
  1098. {
  1099. return TRUE;
  1100. }
  1101. // Basic auth override found, prepare basic
  1102. if ($auth_override_class_method[$this->router->class][$this->router->method] === 'basic')
  1103. {
  1104. $this->_prepare_basic_auth();
  1105. return TRUE;
  1106. }
  1107. // Digest auth override found, prepare digest
  1108. if ($auth_override_class_method[$this->router->class][$this->router->method] === 'digest')
  1109. {
  1110. $this->_prepare_digest_auth();
  1111. return TRUE;
  1112. }
  1113. // Session auth override found, check session
  1114. if ($auth_override_class_method[$this->router->class][$this->router->method] === 'session')
  1115. {
  1116. $this->_check_php_session();
  1117. return TRUE;
  1118. }
  1119. // Whitelist auth override found, check client's ip against config whitelist
  1120. if ($auth_override_class_method[$this->router->class][$this->router->method] === 'whitelist')
  1121. {
  1122. $this->_check_whitelist_auth();
  1123. return TRUE;
  1124. }
  1125. }
  1126. }
  1127. // Assign the class/method/HTTP-method auth type override array from the config
  1128. $auth_override_class_method_http = $this->config->item('auth_override_class_method_http');
  1129. // Check to see if the override array is even populated
  1130. if ( ! empty($auth_override_class_method_http))
  1131. {
  1132. // check for wildcard flag for rules for classes
  1133. if ( ! empty($auth_override_class_method_http[$this->router->class]['*'][$this->request->method]))
  1134. {
  1135. // None auth override found, prepare nothing but send back a TRUE override flag
  1136. if ($auth_override_class_method_http[$this->router->class]['*'][$this->request->method] === 'none')
  1137. {
  1138. return TRUE;
  1139. }
  1140. // Basic auth override found, prepare basic
  1141. if ($auth_override_class_method_http[$this->router->class]['*'][$this->request->method] === 'basic')
  1142. {
  1143. $this->_prepare_basic_auth();
  1144. return TRUE;
  1145. }
  1146. // Digest auth override found, prepare digest
  1147. if ($auth_override_class_method_http[$this->router->class]['*'][$this->request->method] === 'digest')
  1148. {
  1149. $this->_prepare_digest_auth();
  1150. return TRUE;
  1151. }
  1152. // Session auth override found, check session
  1153. if ($auth_override_class_method_http[$this->router->class]['*'][$this->request->method] === 'session')
  1154. {
  1155. $this->_check_php_session();
  1156. return TRUE;
  1157. }
  1158. // Whitelist auth override found, check client's ip against config whitelist
  1159. if ($auth_override_class_method_http[$this->router->class]['*'][$this->request->method] === 'whitelist')
  1160. {
  1161. $this->_check_whitelist_auth();
  1162. return TRUE;
  1163. }
  1164. }
  1165. // Check to see if there's an override value set for the current class/method/HTTP-method being called
  1166. if ( ! empty($auth_override_class_method_http[$this->router->class][$this->router->method][$this->request->method]))
  1167. {
  1168. // None auth override found, prepare nothing but send back a TRUE override flag
  1169. if ($auth_override_class_method_http[$this->router->class][$this->router->method][$this->request->method] === 'none')
  1170. {
  1171. return TRUE;
  1172. }
  1173. // Basic auth override found, prepare basic
  1174. if ($auth_override_class_method_http[$this->router->class][$this->router->method][$this->request->method] === 'basic')
  1175. {
  1176. $this->_prepare_basic_auth();
  1177. return TRUE;
  1178. }
  1179. // Digest auth override found, prepare digest
  1180. if ($auth_override_class_method_http[$this->router->class][$this->router->method][$this->request->method] === 'digest')
  1181. {
  1182. $this->_prepare_digest_auth();
  1183. return TRUE;
  1184. }
  1185. // Session auth override found, check session
  1186. if ($auth_override_class_method_http[$this->router->class][$this->router->method][$this->request->method] === 'session')
  1187. {
  1188. $this->_check_php_session();
  1189. return TRUE;
  1190. }
  1191. // Whitelist auth override found, check client's ip against config whitelist
  1192. if ($auth_override_class_method_http[$this->router->class][$this->router->method][$this->request->method] === 'whitelist')
  1193. {
  1194. $this->_check_whitelist_auth();
  1195. return TRUE;
  1196. }
  1197. }
  1198. }
  1199. return FALSE;
  1200. }
  1201. /**
  1202. * Parse the GET request arguments
  1203. *
  1204. * @access protected
  1205. * @return void
  1206. */
  1207. protected function _parse_get()
  1208. {
  1209. // Merge both the URI segments and query parameters
  1210. $this->_get_args = array_merge($this->_get_args, $this->_query_args);
  1211. }
  1212. /**
  1213. * Parse the POST request arguments
  1214. *
  1215. * @access protected
  1216. * @return void
  1217. */
  1218. protected function _parse_post()
  1219. {
  1220. $this->_post_args = $_POST;
  1221. if ($this->req

Large files files are truncated, but you can click here to view the full file