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