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