source: lliurex-jocomunico/trunk/fuentes/lliurex-jocomunico.install/var/lib/application/libraries/REST_Controller.php @ 5728

Last change on this file since 5728 was 5728, checked in by joamuran, 3 years ago

Added zero-installed and updated jocomunico

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