Overview

Packages

  • CodeIgniter
    • Libraries
    • Rest
  • None

Classes

  • Example
  • Format
  • Key
  • REST_Controller
  • Rest_server
  • Welcome
  • Overview
  • Package
  • Class
   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: 
API documentation generated by ApiGen