From 1a1776ca2aaed31acab9ca5a4efe2b6b4133619b Mon Sep 17 00:00:00 2001 From: Paminger Date: Fri, 11 Mar 2016 10:09:14 +0100 Subject: [PATCH] Libraries --- application/libraries/Format.php | 531 +++++ application/libraries/REST_Controller.php | 2152 +++++++++++++++++++++ application/libraries/fhcauth.php | 51 + 3 files changed, 2734 insertions(+) create mode 100644 application/libraries/Format.php create mode 100644 application/libraries/REST_Controller.php create mode 100644 application/libraries/fhcauth.php diff --git a/application/libraries/Format.php b/application/libraries/Format.php new file mode 100644 index 000000000..0d3bf7732 --- /dev/null +++ b/application/libraries/Format.php @@ -0,0 +1,531 @@ +_CI = &get_instance(); + + // Load the inflector helper + $this->_CI->load->helper('inflector'); + + // If the provided data is already formatted we should probably convert it to an array + if ($from_type !== NULL) + { + if (method_exists($this, '_from_' . $from_type)) + { + $data = call_user_func([$this, '_from_' . $from_type], $data); + } + else + { + throw new Exception('Format class does not support conversion from "' . $from_type . '".'); + } + } + + // Set the member variable to the data passed + $this->_data = $data; + } + + /** + * Create an instance of the format class + * e.g: echo $this->format->factory(['foo' => 'bar'])->to_csv(); + * + * @param mixed $data Data to convert/parse + * @param string $from_type Type to convert from e.g. json, csv, html + * + * @return object Instance of the format class + */ + public function factory($data, $from_type = NULL) + { + // $class = __CLASS__; + // return new $class(); + + return new static($data, $from_type); + } + + // FORMATTING OUTPUT --------------------------------------------------------- + + /** + * Format data as an array + * + * @param mixed|NULL $data Optional data to pass, so as to override the data passed + * to the constructor + * @return array Data parsed as an array; otherwise, an empty array + */ + public function to_array($data = NULL) + { + // If no data is passed as a parameter, then use the data passed + // via the constructor + if ($data === NULL && func_num_args() === 0) + { + $data = $this->_data; + } + + // Cast as an array if not already + if (is_array($data) === FALSE) + { + $data = (array) $data; + } + + $array = []; + foreach ((array) $data as $key => $value) + { + if (is_object($value) === TRUE || is_array($value) === TRUE) + { + $array[$key] = $this->to_array($value); + } + else + { + $array[$key] = $value; + } + } + + return $array; + } + + /** + * Format data as XML + * + * @param mixed|NULL $data Optional data to pass, so as to override the data passed + * to the constructor + * @param NULL $structure + * @param string $basenode + * @return mixed + */ + public function to_xml($data = NULL, $structure = NULL, $basenode = 'xml') + { + if ($data === NULL && func_num_args() === 0) + { + $data = $this->_data; + } + + // turn off compatibility mode as simple xml throws a wobbly if you don't. + if (ini_get('zend.ze1_compatibility_mode') == 1) + { + ini_set('zend.ze1_compatibility_mode', 0); + } + + if ($structure === NULL) + { + $structure = simplexml_load_string("<$basenode />"); + } + + // Force it to be something useful + if (is_array($data) === FALSE && is_object($data) === FALSE) + { + $data = (array) $data; + } + + foreach ($data as $key => $value) + { + + //change false/true to 0/1 + if (is_bool($value)) + { + $value = (int) $value; + } + + // no numeric keys in our xml please! + if (is_numeric($key)) + { + // make string key... + $key = (singular($basenode) != $basenode) ? singular($basenode) : 'item'; + } + + // replace anything not alpha numeric + $key = preg_replace('/[^a-z_\-0-9]/i', '', $key); + + if ($key === '_attributes' && (is_array($value) || is_object($value))) + { + $attributes = $value; + if (is_object($attributes)) + { + $attributes = get_object_vars($attributes); + } + + foreach ($attributes as $attribute_name => $attribute_value) + { + $structure->addAttribute($attribute_name, $attribute_value); + } + } + // if there is another array found recursively call this function + elseif (is_array($value) || is_object($value)) + { + $node = $structure->addChild($key); + + // recursive call. + $this->to_xml($value, $node, $key); + } + else + { + // add single node. + $value = htmlspecialchars(html_entity_decode($value, ENT_QUOTES, 'UTF-8'), ENT_QUOTES, 'UTF-8'); + + $structure->addChild($key, $value); + } + } + + return $structure->asXML(); + } + + /** + * Format data as HTML + * + * @param mixed|NULL $data Optional data to pass, so as to override the data passed + * to the constructor + * @return mixed + */ + public function to_html($data = NULL) + { + // If no data is passed as a parameter, then use the data passed + // via the constructor + if ($data === NULL && func_num_args() === 0) + { + $data = $this->_data; + } + + // Cast as an array if not already + if (is_array($data) === FALSE) + { + $data = (array) $data; + } + + // Check if it's a multi-dimensional array + if (isset($data[0]) && count($data) !== count($data, COUNT_RECURSIVE)) + { + // Multi-dimensional array + $headings = array_keys($data[0]); + } + else + { + // Single array + $headings = array_keys($data); + $data = [$data]; + } + + // Load the table library + $this->_CI->load->library('table'); + + $this->_CI->table->set_heading($headings); + + foreach ($data as $row) + { + // Suppressing the "array to string conversion" notice + // Keep the "evil" @ here + $row = @array_map('strval', $row); + + $this->_CI->table->add_row($row); + } + + return $this->_CI->table->generate(); + } + + /** + * @link http://www.metashock.de/2014/02/create-csv-file-in-memory-php/ + * @param mixed|NULL $data Optional data to pass, so as to override the data passed + * to the constructor + * @param string $delimiter The optional delimiter parameter sets the field + * delimiter (one character only). NULL will use the default value (,) + * @param string $enclosure The optional enclosure parameter sets the field + * enclosure (one character only). NULL will use the default value (") + * @return string A csv string + */ + public function to_csv($data = NULL, $delimiter = ',', $enclosure = '"') + { + // Use a threshold of 1 MB (1024 * 1024) + $handle = fopen('php://temp/maxmemory:1048576', 'w'); + if ($handle === FALSE) + { + return NULL; + } + + // If no data is passed as a parameter, then use the data passed + // via the constructor + if ($data === NULL && func_num_args() === 0) + { + $data = $this->_data; + } + + // If NULL, then set as the default delimiter + if ($delimiter === NULL) + { + $delimiter = ','; + } + + // If NULL, then set as the default enclosure + if ($enclosure === NULL) + { + $enclosure = '"'; + } + + // Cast as an array if not already + if (is_array($data) === FALSE) + { + $data = (array) $data; + } + + // Check if it's a multi-dimensional array + if (isset($data[0]) && count($data) !== count($data, COUNT_RECURSIVE)) + { + // Multi-dimensional array + $headings = array_keys($data[0]); + } + else + { + // Single array + $headings = array_keys($data); + $data = [$data]; + } + + // Apply the headings + fputcsv($handle, $headings, $delimiter, $enclosure); + + foreach ($data as $record) + { + // If the record is not an array, then break. This is because the 2nd param of + // fputcsv() should be an array + if (is_array($record) === FALSE) + { + break; + } + + // Suppressing the "array to string conversion" notice. + // Keep the "evil" @ here. + $record = @ array_map('strval', $record); + + // Returns the length of the string written or FALSE + fputcsv($handle, $record, $delimiter, $enclosure); + } + + // Reset the file pointer + rewind($handle); + + // Retrieve the csv contents + $csv = stream_get_contents($handle); + + // Close the handle + fclose($handle); + + return $csv; + } + + /** + * Encode data as json + * + * @param mixed|NULL $data Optional data to pass, so as to override the data passed + * to the constructor + * @return string Json representation of a value + */ + public function to_json($data = NULL) + { + // If no data is passed as a parameter, then use the data passed + // via the constructor + if ($data === NULL && func_num_args() === 0) + { + $data = $this->_data; + } + + // Get the callback parameter (if set) + $callback = $this->_CI->input->get('callback'); + + if (empty($callback) === TRUE) + { + return json_encode($data); + } + + // We only honour a jsonp callback which are valid javascript identifiers + elseif (preg_match('/^[a-z_\$][a-z0-9\$_]*(\.[a-z_\$][a-z0-9\$_]*)*$/i', $callback)) + { + // Return the data as encoded json with a callback + return $callback . '(' . json_encode($data) . ');'; + } + + // An invalid jsonp callback function provided. + // Though I don't believe this should be hardcoded here + $data['warning'] = 'INVALID JSONP CALLBACK: ' . $callback; + + return json_encode($data); + } + + /** + * Encode data as a serialized array + * + * @param mixed|NULL $data Optional data to pass, so as to override the data passed + * to the constructor + * @return string Serialized data + */ + public function to_serialized($data = NULL) + { + // If no data is passed as a parameter, then use the data passed + // via the constructor + if ($data === NULL && func_num_args() === 0) + { + $data = $this->_data; + } + + return serialize($data); + } + + /** + * Format data using a PHP structure + * + * @param mixed|NULL $data Optional data to pass, so as to override the data passed + * to the constructor + * @return mixed String representation of a variable + */ + public function to_php($data = NULL) + { + // If no data is passed as a parameter, then use the data passed + // via the constructor + if ($data === NULL && func_num_args() === 0) + { + $data = $this->_data; + } + + return var_export($data, TRUE); + } + + // INTERNAL FUNCTIONS + + /** + * @param $data XML string + * @return SimpleXMLElement XML element object; otherwise, empty array + */ + protected function _from_xml($data) + { + return $data ? (array) simplexml_load_string($data, 'SimpleXMLElement', LIBXML_NOCDATA) : []; + } + + /** + * @param string $data CSV string + * @param string $delimiter The optional delimiter parameter sets the field + * delimiter (one character only). NULL will use the default value (,) + * @param string $enclosure The optional enclosure parameter sets the field + * enclosure (one character only). NULL will use the default value (") + * @return array A multi-dimensional array with the outer array being the number of rows + * and the inner arrays the individual fields + */ + protected function _from_csv($data, $delimiter = ',', $enclosure = '"') + { + // If NULL, then set as the default delimiter + if ($delimiter === NULL) + { + $delimiter = ','; + } + + // If NULL, then set as the default enclosure + if ($enclosure === NULL) + { + $enclosure = '"'; + } + + return str_getcsv($data, $delimiter, $enclosure); + } + + /** + * @param $data Encoded json string + * @return mixed Decoded json string with leading and trailing whitespace removed + */ + protected function _from_json($data) + { + return json_decode(trim($data)); + } + + /** + * @param string Data to unserialized + * @return mixed Unserialized data + */ + protected function _from_serialize($data) + { + return unserialize(trim($data)); + } + + /** + * @param $data Data to trim leading and trailing whitespace + * @return string Data with leading and trailing whitespace removed + */ + protected function _from_php($data) + { + return trim($data); + } + +} diff --git a/application/libraries/REST_Controller.php b/application/libraries/REST_Controller.php new file mode 100644 index 000000000..d13e27ec2 --- /dev/null +++ b/application/libraries/REST_Controller.php @@ -0,0 +1,2152 @@ + 'application/json', + 'array' => 'application/json', + 'csv' => 'application/csv', + 'html' => 'text/html', + 'jsonp' => 'application/javascript', + 'php' => 'text/plain', + 'serialized' => 'application/vnd.php.serialized', + 'xml' => 'application/xml' + ]; + + /** + * Information about the current API user + * + * @var object + */ + protected $_apiuser; + + /** + * Enable XSS flag + * Determines whether the XSS filter is always active when + * GET, OPTIONS, HEAD, POST, PUT, DELETE and PATCH data is encountered. + * Set automatically based on config setting + * + * @var bool + */ + protected $_enable_xss = FALSE; + + /** + * HTTP status codes and their respective description + * Note: Only the widely used HTTP status codes are used + * + * @var array + * @link http://www.restapitutorial.com/httpstatuscodes.html + */ + protected $http_status_codes = [ + self::HTTP_OK => 'OK', + self::HTTP_CREATED => 'CREATED', + self::HTTP_NO_CONTENT => 'NO CONTENT', + self::HTTP_NOT_MODIFIED => 'NOT MODIFIED', + self::HTTP_BAD_REQUEST => 'BAD REQUEST', + self::HTTP_UNAUTHORIZED => 'UNAUTHORIZED', + self::HTTP_FORBIDDEN => 'FORBIDDEN', + self::HTTP_NOT_FOUND => 'NOT FOUND', + self::HTTP_METHOD_NOT_ALLOWED => 'METHOD NOT ALLOWED', + self::HTTP_NOT_ACCEPTABLE => 'NOT ACCEPTABLE', + self::HTTP_CONFLICT => 'CONFLICT', + self::HTTP_INTERNAL_SERVER_ERROR => 'INTERNAL SERVER ERROR', + self::HTTP_NOT_IMPLEMENTED => 'NOT IMPLEMENTED' + ]; + + /** + * Extend this function to apply additional checking early on in the process + * + * @access protected + * @return void + */ + protected function early_checks() + { + } + + /** + * Constructor for the REST API + * + * @access public + * @param string $config Configuration filename minus the file extension + * e.g: my_rest.php is passed as 'my_rest' + * @return void + */ + public function __construct($config = 'rest') + { + parent::__construct(); + + // Disable XML Entity (security vulnerability) + libxml_disable_entity_loader(TRUE); + + // Check to see if PHP is equal to or greater than 5.4.x + if (is_php('5.4') === FALSE) + { + // CodeIgniter 3 is recommended for v5.4 or above + throw new Exception('Using PHP v' . PHP_VERSION . ', though PHP v5.4 or greater is required'); + } + + // Check to see if this is CI 3.x + if (explode('.', CI_VERSION, 2)[0] < 3) + { + throw new Exception('REST Server requires CodeIgniter 3.x'); + } + + // Set the default value of global xss filtering. Same approach as CodeIgniter 3 + $this->_enable_xss = ($this->config->item('global_xss_filtering') === TRUE); + + // Don't try to parse template variables like {elapsed_time} and {memory_usage} + // when output is displayed for not damaging data accidentally + $this->output->parse_exec_vars = FALSE; + + // Start the timer for how long the request takes + $this->_start_rtime = microtime(TRUE); + + // Load the rest.php configuration file + $this->load->config($config); + + // At present the library is bundled with REST_Controller 2.5+, but will eventually be part of CodeIgniter (no citation) + $this->load->library('format'); + + // Determine supported output formats from configiguration. + $supported_formats = $this->config->item('rest_supported_formats'); + + // Validate the configuration setting output formats + if (empty($supported_formats)) + { + $supported_formats = []; + } + + if (!is_array($supported_formats)) + { + $supported_formats = [$supported_formats]; + } + + // Add silently the default output format if it is missing. + $default_format = $this->_get_default_output_format(); + if (!in_array($default_format, $supported_formats)) + { + $supported_formats[] = $default_format; + } + + // Now update $this->_supported_formats + $this->_supported_formats = array_intersect_key($this->_supported_formats, array_flip($supported_formats)); + + // Get the language + $language = $this->config->item('rest_language'); + if ($language === NULL) + { + $language = 'english'; + } + + // Load the language file + $this->lang->load('rest_controller', $language); + + // Initialise the response, request and rest objects + $this->request = new stdClass(); + $this->response = new stdClass(); + $this->rest = new stdClass(); + + // Check to see if the current IP address is blacklisted + if ($this->config->item('rest_ip_blacklist_enabled') === TRUE) + { + $this->_check_blacklist_auth(); + } + + // Determine whether the connection is HTTPS + $this->request->ssl = is_https(); + + // How is this request being made? GET, POST, PATCH, DELETE, INSERT, PUT, HEAD or OPTIONS + $this->request->method = $this->_detect_method(); + + // Create an argument container if it doesn't exist e.g. _get_args + if (isset($this->{'_' . $this->request->method . '_args'}) === FALSE) + { + $this->{'_' . $this->request->method . '_args'} = []; + } + + // Set up the query parameters + $this->_parse_query(); + + // Set up the GET variables + $this->_get_args = array_merge($this->_get_args, $this->uri->ruri_to_assoc()); + + // Try to find a format for the request (means we have a request body) + $this->request->format = $this->_detect_input_format(); + + // Not all methods have a body attached with them + $this->request->body = NULL; + + $this->{'_parse_' . $this->request->method}(); + + // Now we know all about our request, let's try and parse the body if it exists + if ($this->request->format && $this->request->body) + { + $this->request->body = $this->format->factory($this->request->body, $this->request->format)->to_array(); + // Assign payload arguments to proper method container + $this->{'_' . $this->request->method . '_args'} = $this->request->body; + } + + // Merge both for one mega-args variable + $this->_args = array_merge( + $this->_get_args, + $this->_options_args, + $this->_patch_args, + $this->_head_args, + $this->_put_args, + $this->_post_args, + $this->_delete_args, + $this->{'_' . $this->request->method . '_args'} + ); + + // Which format should the data be returned in? + $this->response->format = $this->_detect_output_format(); + + // Which language should the data be returned in? + $this->response->lang = $this->_detect_lang(); + + // Extend this function to apply additional checking early on in the process + $this->early_checks(); + + // Load DB if its enabled + if ($this->config->item('rest_database_group') && ($this->config->item('rest_enable_keys') || $this->config->item('rest_enable_logging'))) + { + $this->rest->db = $this->load->database($this->config->item('rest_database_group'), TRUE); + } + + // Use whatever database is in use (isset returns FALSE) + elseif (property_exists($this, 'db')) + { + $this->rest->db = $this->db; + } + + // Check if there is a specific auth type for the current class/method + // _auth_override_check could exit so we need $this->rest->db initialized before + $this->auth_override = $this->_auth_override_check(); + + // Checking for keys? GET TO WorK! + // Skip keys test for $config['auth_override_class_method']['class'['method'] = 'none' + if ($this->config->item('rest_enable_keys') && $this->auth_override !== TRUE) + { + $this->_allow = $this->_detect_api_key(); + } + + // Only allow ajax requests + if ($this->input->is_ajax_request() === FALSE && $this->config->item('rest_ajax_only')) + { + // Display an error response + $this->response([ + $this->config->item('rest_status_field_name') => FALSE, + $this->config->item('rest_message_field_name') => $this->lang->line('text_rest_ajax_only') + ], self::HTTP_NOT_ACCEPTABLE); + } + + // When there is no specific override for the current class/method, use the default auth value set in the config + if ($this->auth_override === FALSE && !($this->config->item('rest_enable_keys') && $this->_allow === TRUE)) + { + $rest_auth = strtolower($this->config->item('rest_auth')); + switch ($rest_auth) + { + case 'basic': + $this->_prepare_basic_auth(); + break; + case 'digest': + $this->_prepare_digest_auth(); + break; + case 'session': + $this->_check_php_session(); + break; + } + if ($this->config->item('rest_ip_whitelist_enabled') === TRUE) + { + $this->_check_whitelist_auth(); + } + } + } + + /** + * Deconstructor + * + * @author Chris Kacerguis + * @access public + * @return void + */ + public function __destruct() + { + // Get the current timestamp + $this->_end_rtime = microtime(TRUE); + + // Log the loading time to the log table + if ($this->config->item('rest_enable_logging') === TRUE) + { + $this->_log_access_time(); + } + } + + /** + * Requests are not made to methods directly, the request will be for + * an "object". This simply maps the object and method to the correct + * Controller method + * + * @access public + * @param string $object_called + * @param array $arguments The arguments passed to the controller method + */ + public function _remap($object_called, $arguments) + { + // Should we answer if not over SSL? + if ($this->config->item('force_https') && $this->request->ssl === FALSE) + { + $this->response([ + $this->config->item('rest_status_field_name') => FALSE, + $this->config->item('rest_message_field_name') => $this->lang->line('text_rest_unsupported') + ], self::HTTP_FORBIDDEN); + } + + // Remove the supported format from the function name e.g. index.json => index + $object_called = preg_replace('/^(.*)\.(?:' . implode('|', array_keys($this->_supported_formats)) . ')$/', '$1', $object_called); + + $controller_method = $object_called . '_' . $this->request->method; + + // Do we want to log this method (if allowed by config)? + $log_method = !(isset($this->methods[$controller_method]['log']) && $this->methods[$controller_method]['log'] === FALSE); + + // Use keys for this method? + $use_key = !(isset($this->methods[$controller_method]['key']) && $this->methods[$controller_method]['key'] === FALSE); + + // They provided a key, but it wasn't valid, so get them out of here + if ($this->config->item('rest_enable_keys') && $use_key && $this->_allow === FALSE) + { + if ($this->config->item('rest_enable_logging') && $log_method) + { + $this->_log_request(); + } +//echo 'RestKey: '.$this->rest->key; + $this->response([ + $this->config->item('rest_status_field_name') => FALSE, + $this->config->item('rest_message_field_name') => sprintf($this->lang->line('text_rest_invalid_api_key'), $this->rest->key) + ], self::HTTP_FORBIDDEN); + } + + // Check to see if this key has access to the requested controller + if ($this->config->item('rest_enable_keys') && $use_key && empty($this->rest->key) === FALSE && $this->_check_access() === FALSE) + { + if ($this->config->item('rest_enable_logging') && $log_method) + { + $this->_log_request(); + } + + $this->response([ + $this->config->item('rest_status_field_name') => FALSE, + $this->config->item('rest_message_field_name') => $this->lang->line('text_rest_api_key_unauthorized') + ], self::HTTP_UNAUTHORIZED); + } + + // Sure it exists, but can they do anything with it? + if (method_exists($this, $controller_method) === FALSE) + { + $this->response([ + $this->config->item('rest_status_field_name') => FALSE, + $this->config->item('rest_message_field_name') => $this->lang->line('text_rest_unknown_method') + ], self::HTTP_NOT_FOUND); + } + + // Doing key related stuff? Can only do it if they have a key right? + if ($this->config->item('rest_enable_keys') && empty($this->rest->key) === FALSE) + { + // Check the limit + if ($this->config->item('rest_enable_limits') && $this->_check_limit($controller_method) === FALSE) + { + $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')]; + $this->response($response, self::HTTP_UNAUTHORIZED); + } + + // If no level is set use 0, they probably aren't using permissions + $level = isset($this->methods[$controller_method]['level']) ? $this->methods[$controller_method]['level'] : 0; + + // If no level is set, or it is lower than/equal to the key's level + $authorized = $level <= $this->rest->level; + + // IM TELLIN! + if ($this->config->item('rest_enable_logging') && $log_method) + { + $this->_log_request($authorized); + } + + // They don't have good enough perms + $response = [$this->config->item('rest_status_field_name') => FALSE, $this->config->item('rest_message_field_name') => $this->lang->line('text_rest_api_key_permissions')]; + $authorized || $this->response($response, self::HTTP_UNAUTHORIZED); + } + + // No key stuff, but record that stuff is happening + elseif ($this->config->item('rest_enable_logging') && $log_method) + { + $this->_log_request($authorized = TRUE); + } + + // Call the controller method and passed arguments + try + { + call_user_func_array([$this, $controller_method], $arguments); + } + catch (Exception $ex) + { + // If the method doesn't exist, then the error will be caught and an error response shown + $this->response([ + $this->config->item('rest_status_field_name') => FALSE, + $this->config->item('rest_message_field_name') => [ + 'classname' => get_class($ex), + 'message' => $ex->getMessage() + ] + ], self::HTTP_INTERNAL_SERVER_ERROR); + } + } + + /** + * Takes mixed data and optionally a status code, then creates the response + * + * @access public + * @param array|NULL $data Data to output to the user + * @param int|NULL $http_code HTTP status code + * @param bool $continue TRUE to flush the response to the client and continue + * running the script; otherwise, exit + */ + public function response($data = NULL, $http_code = NULL, $continue = FALSE) + { + // If the HTTP status is not NULL, then cast as an integer + if ($http_code !== NULL) + { + // So as to be safe later on in the process + $http_code = (int) $http_code; + } + + // Set the output as NULL by default + $output = NULL; + + // If data is NULL and no HTTP status code provided, then display, error and exit + if ($data === NULL && $http_code === NULL) + { + $http_code = self::HTTP_NOT_FOUND; + } + + // If data is not NULL and a HTTP status code provided, then continue + elseif ($data !== NULL) + { + // If the format method exists, call and return the output in that format + if (method_exists($this->format, 'to_' . $this->response->format)) + { + // Set the format header + $this->output->set_content_type($this->_supported_formats[$this->response->format], strtolower($this->config->item('charset'))); + $output = $this->format->factory($data)->{'to_' . $this->response->format}(); + + // An array must be parsed as a string, so as not to cause an array to string error + // Json is the most appropriate form for such a datatype + if ($this->response->format === 'array') + { + $output = $this->format->factory($output)->{'to_json'}(); + } + } + else + { + // If an array or object, then parse as a json, so as to be a 'string' + if (is_array($data) || is_object($data)) + { + $data = $this->format->factory($data)->{'to_json'}(); + } + + // Format is not supported, so output the raw data as a string + $output = $data; + } + } + + // If not greater than zero, then set the HTTP status code as 200 by default + // Though perhaps 500 should be set instead, for the developer not passing a + // correct HTTP status code + $http_code > 0 || $http_code = self::HTTP_OK; + + $this->output->set_status_header($http_code); + + // JC: Log response code only if rest logging enabled + if ($this->config->item('rest_enable_logging') === TRUE) + { + $this->_log_response_code($http_code); + } + + // Output the data + $this->output->set_output($output); + + if ($continue === FALSE) + { + // Display the data and exit execution + $this->output->_display(); + exit; + } + + // Otherwise dump the output automatically + } + + /** + * Takes mixed data and optionally a status code, then creates the response + * within the buffers of the Output class. The response is sent to the client + * lately by the framework, after the current controller's method termination. + * All the hooks after the controller's method termination are executable + * + * @access public + * @param array|NULL $data Data to output to the user + * @param int|NULL $http_code HTTP status code + */ + public function set_response($data = NULL, $http_code = NULL) + { + $this->response($data, $http_code, TRUE); + } + + /** + * Get the input format e.g. json or xml + * + * @access protected + * @return string|NULL Supported input format; otherwise, NULL + */ + protected function _detect_input_format() + { + // Get the CONTENT-TYPE value from the SERVER variable + $content_type = $this->input->server('CONTENT_TYPE'); + + if (empty($content_type) === FALSE) + { + // Check all formats against the HTTP_ACCEPT header + foreach ($this->_supported_formats as $key => $value) + { + // $key = format e.g. csv + // $value = mime type e.g. application/csv + + // If a semi-colon exists in the string, then explode by ; and get the value of where + // the current array pointer resides. This will generally be the first element of the array + $content_type = (strpos($content_type, ';') !== FALSE ? current(explode(';', $content_type)) : $content_type); + + // If both the mime types match, then return the format + if ($content_type === $value) + { + return $key; + } + } + } + + return NULL; + } + + /** + * Gets the default format from the configuration. Fallbacks to 'json'. + * if the corresponding configuration option $config['rest_default_format'] + * is missing or is empty. + * + * @access protected + * @return string The default supported input format + */ + protected function _get_default_output_format() + { + $default_format = (string) $this->config->item('rest_default_format'); + return $default_format === '' ? 'json' : $default_format; + } + + /** + * Detect which format should be used to output the data + * + * @access protected + * @return mixed|NULL|string Output format + */ + protected function _detect_output_format() + { + // Concatenate formats to a regex pattern e.g. \.(csv|json|xml) + $pattern = '/\.(' . implode('|', array_keys($this->_supported_formats)) . ')($|\/)/'; + $matches = []; + + // Check if a file extension is used e.g. http://example.com/api/index.json?param1=param2 + if (preg_match($pattern, $this->uri->uri_string(), $matches)) + { + return $matches[1]; + } + + // Get the format parameter named as 'format' + if (isset($this->_get_args['format'])) + { + $format = strtolower($this->_get_args['format']); + + if (isset($this->_supported_formats[$format]) === TRUE) + { + return $format; + } + } + + // Get the HTTP_ACCEPT server variable + $http_accept = $this->input->server('HTTP_ACCEPT'); + + // Otherwise, check the HTTP_ACCEPT server variable + if ($this->config->item('rest_ignore_http_accept') === FALSE && $http_accept !== NULL) + { + // Check all formats against the HTTP_ACCEPT header + foreach (array_keys($this->_supported_formats) as $format) + { + // Has this format been requested? + if (strpos($http_accept, $format) !== FALSE) + { + if ($format !== 'html' && $format !== 'xml') + { + // If not HTML or XML assume it's correct + return $format; + } + elseif ($format === 'html' && strpos($http_accept, 'xml') === FALSE) + { + // HTML or XML have shown up as a match + // If it is truly HTML, it wont want any XML + return $format; + } + else if ($format === 'xml' && strpos($http_accept, 'html') === FALSE) + { + // If it is truly XML, it wont want any HTML + return $format; + } + } + } + } + + // Check if the controller has a default format + if (empty($this->rest_format) === FALSE) + { + return $this->rest_format; + } + + // Obtain the default format from the configuration + return $this->_get_default_output_format(); + } + + /** + * Get the HTTP request string e.g. get or post + * + * @access protected + * @return string|NULL Supported request method as a lowercase string; otherwise, NULL if not supported + */ + protected function _detect_method() + { + // Declare a variable to store the method + $method = NULL; + + // Determine whether the 'enable_emulate_request' setting is enabled + if ($this->config->item('enable_emulate_request') === TRUE) + { + $method = $this->input->post('_method'); + if ($method === NULL) + { + $method = $this->input->server('HTTP_X_HTTP_METHOD_OVERRIDE'); + } + + $method = strtolower($method); + } + + if (empty($method)) + { + // Get the request method as a lowercase string + $method = $this->input->method(); + } + + return in_array($method, $this->allowed_http_methods) && method_exists($this, '_parse_' . $method) ? $method : 'get'; + } + + /** + * See if the user has provided an API key + * + * @access protected + * @return bool + */ + protected function _detect_api_key() + { + // Get the api key name variable set in the rest config file + $api_key_variable = $this->config->item('rest_key_name'); + + // Work out the name of the SERVER entry based on config + $key_name = 'HTTP_' . strtoupper(str_replace('-', '_', $api_key_variable)); + + $this->rest->key = NULL; + $this->rest->level = NULL; + $this->rest->user_id = NULL; + $this->rest->ignore_limits = FALSE; +//var_dump($this->_args); +//var_dump($this->input->server($key_name)); + // Find the key from server or arguments + if (($key = isset($this->_args[$api_key_variable]) ? $this->_args[$api_key_variable] : $this->input->server($key_name))) + { + if (!($row = $this->rest->db->where($this->config->item('rest_key_column'), $key)->get($this->config->item('rest_keys_table'))->row())) + { + return FALSE; + } + + $this->rest->key = $row->{$this->config->item('rest_key_column')}; + + isset($row->user_id) && $this->rest->user_id = $row->user_id; + isset($row->level) && $this->rest->level = $row->level; + isset($row->ignore_limits) && $this->rest->ignore_limits = $row->ignore_limits; + + $this->_apiuser = $row; + + /* + * If "is private key" is enabled, compare the ip address with the list + * of valid ip addresses stored in the database + */ + if (empty($row->is_private_key) === FALSE) + { + // Check for a list of valid ip addresses + if (isset($row->ip_addresses)) + { + // multiple ip addresses must be separated using a comma, explode and loop + $list_ip_addresses = explode(',', $row->ip_addresses); + $found_address = FALSE; + + foreach ($list_ip_addresses as $ip_address) + { + if ($this->input->ip_address() === trim($ip_address)) + { + // there is a match, set the the value to TRUE and break out of the loop + $found_address = TRUE; + break; + } + } + + return $found_address; + } + else + { + // There should be at least one IP address for this private key + return FALSE; + } + } + + return TRUE; + } + + // No key has been sent + return FALSE; + } + + /** + * Preferred return language + * + * @access protected + * @return string|NULL The language code + */ + protected function _detect_lang() + { + $lang = $this->input->server('HTTP_ACCEPT_LANGUAGE'); + if ($lang === NULL) + { + return NULL; + } + + // It appears more than one language has been sent using a comma delimiter + if (strpos($lang, ',') !== FALSE) + { + $langs = explode(',', $lang); + + $return_langs = []; + foreach ($langs as $lang) + { + // Remove weight and trim leading and trailing whitespace + list($lang) = explode(';', $lang); + $return_langs[] = trim($lang); + } + + return $return_langs; + } + + // Otherwise simply return as a string + return $lang; + } + + /** + * Add the request to the log table + * + * @access protected + * @param bool $authorized TRUE the user is authorized; otherwise, FALSE + * @return bool TRUE the data was inserted; otherwise, FALSE + */ + protected function _log_request($authorized = FALSE) + { + // Insert the request into the log table + $is_inserted = $this->rest->db + ->insert( + $this->config->item('rest_logs_table'), [ + 'uri' => $this->uri->uri_string(), + 'method' => $this->request->method, + 'params' => $this->_args ? ($this->config->item('rest_logs_json_params') === TRUE ? json_encode($this->_args) : serialize($this->_args)) : NULL, + 'api_key' => isset($this->rest->key) ? $this->rest->key : '', + 'ip_address' => $this->input->ip_address(), + 'time' => time(), + 'authorized' => $authorized + ]); + + // Get the last insert id to update at a later stage of the request + $this->_insert_id = $this->rest->db->insert_id(); + + return $is_inserted; + } + + /** + * Check if the requests to a controller method exceed a limit + * + * @access protected + * @param string $controller_method The method being called + * @return bool TRUE the call limit is below the threshold; otherwise, FALSE + */ + protected function _check_limit($controller_method) + { + // They are special, or it might not even have a limit + if (empty($this->rest->ignore_limits) === FALSE) + { + // Everything is fine + return TRUE; + } + + switch ($this->config->item('rest_limits_method')) + { + case 'API_KEY': + $limited_uri = 'api-key:' . (isset($this->rest->key) ? $this->rest->key : ''); + $limited_method_name = isset($this->rest->key) ? $this->rest->key : ''; + break; + + case 'METHOD_NAME': + $limited_uri = 'method-name:' . $controller_method; + $limited_method_name = $controller_method; + break; + + case 'ROUTED_URL': + default: + $limited_uri = $this->uri->ruri_string(); + if (strpos(strrev($limited_uri), strrev($this->response->format)) === 0) + { + $limited_uri = substr($limited_uri,0, -strlen($this->response->format) - 1); + } + $limited_uri = 'uri:' . $limited_uri . ':' . $this->request->method; // It's good to differentiate GET from PUT + $limited_method_name = $controller_method; + break; + } + + if (isset($this->methods[$limited_method_name]['limit']) === FALSE ) + { + // Everything is fine + return TRUE; + } + + // How many times can you get to this method in a defined time_limit (default: 1 hour)? + $limit = $this->methods[$limited_method_name]['limit']; + + $time_limit = (isset($this->methods[$limited_method_name]['time']) ? $this->methods[$limited_method_name]['time'] : 3600); // 3600 = 60 * 60 + + // Get data about a keys' usage and limit to one row + $result = $this->rest->db + ->where('uri', $limited_uri) + ->where('api_key', $this->rest->key) + ->get($this->config->item('rest_limits_table')) + ->row(); + + // No calls have been made for this key + if ($result === NULL) + { + // Create a new row for the following key + $this->rest->db->insert($this->config->item('rest_limits_table'), [ + 'uri' => $limited_uri, + 'api_key' => isset($this->rest->key) ? $this->rest->key : '', + 'count' => 1, + 'hour_started' => time() + ]); + } + + // Been a time limit (or by default an hour) since they called + elseif ($result->hour_started < (time() - $time_limit)) + { + // Reset the started period and count + $this->rest->db + ->where('uri', $limited_uri) + ->where('api_key', isset($this->rest->key) ? $this->rest->key : '') + ->set('hour_started', time()) + ->set('count', 1) + ->update($this->config->item('rest_limits_table')); + } + + // They have called within the hour, so lets update + else + { + // The limit has been exceeded + if ($result->count >= $limit) + { + return FALSE; + } + + // Increase the count by one + $this->rest->db + ->where('uri', $limited_uri) + ->where('api_key', $this->rest->key) + ->set('count', 'count + 1', FALSE) + ->update($this->config->item('rest_limits_table')); + } + + return TRUE; + } + + /** + * Check if there is a specific auth type set for the current class/method/HTTP-method being called + * + * @access protected + * @return bool + */ + protected function _auth_override_check() + { + // Assign the class/method auth type override array from the config + $auth_override_class_method = $this->config->item('auth_override_class_method'); + + // Check to see if the override array is even populated + if (!empty($auth_override_class_method)) + { + // check for wildcard flag for rules for classes + if (!empty($auth_override_class_method[$this->router->class]['*'])) // Check for class overrides + { + // None auth override found, prepare nothing but send back a TRUE override flag + if ($auth_override_class_method[$this->router->class]['*'] === 'none') + { + return TRUE; + } + + // Basic auth override found, prepare basic + if ($auth_override_class_method[$this->router->class]['*'] === 'basic') + { + $this->_prepare_basic_auth(); + + return TRUE; + } + + // Digest auth override found, prepare digest + if ($auth_override_class_method[$this->router->class]['*'] === 'digest') + { + $this->_prepare_digest_auth(); + + return TRUE; + } + + // Session auth override found, check session + if ($auth_override_class_method[$this->router->class]['*'] === 'session') + { + $this->_check_php_session(); + + return TRUE; + } + + // Whitelist auth override found, check client's ip against config whitelist + if ($auth_override_class_method[$this->router->class]['*'] === 'whitelist') + { + $this->_check_whitelist_auth(); + + return TRUE; + } + } + + // Check to see if there's an override value set for the current class/method being called + if (!empty($auth_override_class_method[$this->router->class][$this->router->method])) + { + // None auth override found, prepare nothing but send back a TRUE override flag + if ($auth_override_class_method[$this->router->class][$this->router->method] === 'none') + { + return TRUE; + } + + // Basic auth override found, prepare basic + if ($auth_override_class_method[$this->router->class][$this->router->method] === 'basic') + { + $this->_prepare_basic_auth(); + + return TRUE; + } + + // Digest auth override found, prepare digest + if ($auth_override_class_method[$this->router->class][$this->router->method] === 'digest') + { + $this->_prepare_digest_auth(); + + return TRUE; + } + + // Session auth override found, check session + if ($auth_override_class_method[$this->router->class][$this->router->method] === 'session') + { + $this->_check_php_session(); + + return TRUE; + } + + // Whitelist auth override found, check client's ip against config whitelist + if ($auth_override_class_method[$this->router->class][$this->router->method] === 'whitelist') + { + $this->_check_whitelist_auth(); + + return TRUE; + } + } + } + + // Assign the class/method/HTTP-method auth type override array from the config + $auth_override_class_method_http = $this->config->item('auth_override_class_method_http'); + + // Check to see if the override array is even populated + if (!empty($auth_override_class_method_http)) + { + // check for wildcard flag for rules for classes + if(!empty($auth_override_class_method_http[$this->router->class]['*'][$this->request->method])) + { + // None auth override found, prepare nothing but send back a TRUE override flag + if ($auth_override_class_method_http[$this->router->class]['*'][$this->request->method] === 'none') + { + return TRUE; + } + + // Basic auth override found, prepare basic + if ($auth_override_class_method_http[$this->router->class]['*'][$this->request->method] === 'basic') + { + $this->_prepare_basic_auth(); + + return TRUE; + } + + // Digest auth override found, prepare digest + if ($auth_override_class_method_http[$this->router->class]['*'][$this->request->method] === 'digest') + { + $this->_prepare_digest_auth(); + + return TRUE; + } + + // Session auth override found, check session + if ($auth_override_class_method_http[$this->router->class]['*'][$this->request->method] === 'session') + { + $this->_check_php_session(); + + return TRUE; + } + + // Whitelist auth override found, check client's ip against config whitelist + if ($auth_override_class_method_http[$this->router->class]['*'][$this->request->method] === 'whitelist') + { + $this->_check_whitelist_auth(); + + return TRUE; + } + } + + // Check to see if there's an override value set for the current class/method/HTTP-method being called + if(!empty($auth_override_class_method_http[$this->router->class][$this->router->method][$this->request->method])) + { + // None auth override found, prepare nothing but send back a TRUE override flag + if ($auth_override_class_method_http[$this->router->class][$this->router->method][$this->request->method] === 'none') + { + return TRUE; + } + + // Basic auth override found, prepare basic + if ($auth_override_class_method_http[$this->router->class][$this->router->method][$this->request->method] === 'basic') + { + $this->_prepare_basic_auth(); + + return TRUE; + } + + // Digest auth override found, prepare digest + if ($auth_override_class_method_http[$this->router->class][$this->router->method][$this->request->method] === 'digest') + { + $this->_prepare_digest_auth(); + + return TRUE; + } + + // Session auth override found, check session + if ($auth_override_class_method_http[$this->router->class][$this->router->method][$this->request->method] === 'session') + { + $this->_check_php_session(); + + return TRUE; + } + + // Whitelist auth override found, check client's ip against config whitelist + if ($auth_override_class_method_http[$this->router->class][$this->router->method][$this->request->method] === 'whitelist') + { + $this->_check_whitelist_auth(); + + return TRUE; + } + } + } + return FALSE; + } + + /** + * Parse the GET request arguments + * + * @access protected + * @return void + */ + protected function _parse_get() + { + // Merge both the URI segments and query parameters + $this->_get_args = array_merge($this->_get_args, $this->_query_args); + } + + /** + * Parse the POST request arguments + * + * @access protected + * @return void + */ + protected function _parse_post() + { + $this->_post_args = $_POST; + + if ($this->request->format) + { + $this->request->body = $this->input->raw_input_stream; + } + } + + /** + * Parse the PUT request arguments + * + * @access protected + * @return void + */ + protected function _parse_put() + { + if ($this->request->format) + { + $this->request->body = $this->input->raw_input_stream; + } + else if ($this->input->method() === 'put') + { + // If no filetype is provided, then there are probably just arguments + $this->_put_args = $this->input->input_stream(); + } + } + + /** + * Parse the HEAD request arguments + * + * @access protected + * @return void + */ + protected function _parse_head() + { + // Parse the HEAD variables + parse_str(parse_url($this->input->server('REQUEST_URI'), PHP_URL_QUERY), $head); + + // Merge both the URI segments and HEAD params + $this->_head_args = array_merge($this->_head_args, $head); + } + + /** + * Parse the OPTIONS request arguments + * + * @access protected + * @return void + */ + protected function _parse_options() + { + // Parse the OPTIONS variables + parse_str(parse_url($this->input->server('REQUEST_URI'), PHP_URL_QUERY), $options); + + // Merge both the URI segments and OPTIONS params + $this->_options_args = array_merge($this->_options_args, $options); + } + + /** + * Parse the PATCH request arguments + * + * @access protected + * @return void + */ + protected function _parse_patch() + { + // It might be a HTTP body + if ($this->request->format) + { + $this->request->body = $this->input->raw_input_stream; + } + else if ($this->input->method() === 'patch') + { + // If no filetype is provided, then there are probably just arguments + $this->_patch_args = $this->input->input_stream(); + } + } + + /** + * Parse the DELETE request arguments + * + * @access protected + * @return void + */ + protected function _parse_delete() + { + // These should exist if a DELETE request + if ($this->input->method() === 'delete') + { + $this->_delete_args = $this->input->input_stream(); + } + } + + /** + * Parse the query parameters + * + * @access protected + * @return void + */ + protected function _parse_query() + { + $this->_query_args = $this->input->get(); + } + + // INPUT FUNCTION -------------------------------------------------------------- + + /** + * Retrieve a value from a GET request + * + * @access public + * @param NULL $key Key to retrieve from the GET request + * If NULL an array of arguments is returned + * @param NULL $xss_clean Whether to apply XSS filtering + * @return array|string|NULL Value from the GET request; otherwise, NULL + */ + public function get($key = NULL, $xss_clean = NULL) + { + if ($key === NULL) + { + return $this->_get_args; + } + + return isset($this->_get_args[$key]) ? $this->_xss_clean($this->_get_args[$key], $xss_clean) : NULL; + } + + /** + * Retrieve a value from a OPTIONS request + * + * @access public + * @param NULL $key Key to retrieve from the OPTIONS request. + * If NULL an array of arguments is returned + * @param NULL $xss_clean Whether to apply XSS filtering + * @return array|string|NULL Value from the OPTIONS request; otherwise, NULL + */ + public function options($key = NULL, $xss_clean = NULL) + { + if ($key === NULL) + { + return $this->_options_args; + } + + return isset($this->_options_args[$key]) ? $this->_xss_clean($this->_options_args[$key], $xss_clean) : NULL; + } + + /** + * Retrieve a value from a HEAD request + * + * @access public + * @param NULL $key Key to retrieve from the HEAD request + * If NULL an array of arguments is returned + * @param NULL $xss_clean Whether to apply XSS filtering + * @return array|string|NULL Value from the HEAD request; otherwise, NULL + */ + public function head($key = NULL, $xss_clean = NULL) + { + if ($key === NULL) + { + return $this->_head_args; + } + + return isset($this->_head_args[$key]) ? $this->_xss_clean($this->_head_args[$key], $xss_clean) : NULL; + } + + /** + * Retrieve a value from a POST request + * + * @access public + * @param NULL $key Key to retrieve from the POST request + * If NULL an array of arguments is returned + * @param NULL $xss_clean Whether to apply XSS filtering + * @return array|string|NULL Value from the POST request; otherwise, NULL + */ + public function post($key = NULL, $xss_clean = NULL) + { + if ($key === NULL) + { + return $this->_post_args; + } + + return isset($this->_post_args[$key]) ? $this->_xss_clean($this->_post_args[$key], $xss_clean) : NULL; + } + + /** + * Retrieve a value from a PUT request + * + * @access public + * @param NULL $key Key to retrieve from the PUT request + * If NULL an array of arguments is returned + * @param NULL $xss_clean Whether to apply XSS filtering + * @return array|string|NULL Value from the PUT request; otherwise, NULL + */ + public function put($key = NULL, $xss_clean = NULL) + { + if ($key === NULL) + { + return $this->_put_args; + } + + return isset($this->_put_args[$key]) ? $this->_xss_clean($this->_put_args[$key], $xss_clean) : NULL; + } + + /** + * Retrieve a value from a DELETE request + * + * @access public + * @param NULL $key Key to retrieve from the DELETE request + * If NULL an array of arguments is returned + * @param NULL $xss_clean Whether to apply XSS filtering + * @return array|string|NULL Value from the DELETE request; otherwise, NULL + */ + public function delete($key = NULL, $xss_clean = NULL) + { + if ($key === NULL) + { + return $this->_delete_args; + } + + return isset($this->_delete_args[$key]) ? $this->_xss_clean($this->_delete_args[$key], $xss_clean) : NULL; + } + + /** + * Retrieve a value from a PATCH request + * + * @access public + * @param NULL $key Key to retrieve from the PATCH request + * If NULL an array of arguments is returned + * @param NULL $xss_clean Whether to apply XSS filtering + * @return array|string|NULL Value from the PATCH request; otherwise, NULL + */ + public function patch($key = NULL, $xss_clean = NULL) + { + if ($key === NULL) + { + return $this->_patch_args; + } + + return isset($this->_patch_args[$key]) ? $this->_xss_clean($this->_patch_args[$key], $xss_clean) : NULL; + } + + /** + * Retrieve a value from the query parameters + * + * @access public + * @param NULL $key Key to retrieve from the query parameters + * If NULL an array of arguments is returned + * @param NULL $xss_clean Whether to apply XSS filtering + * @return array|string|NULL Value from the query parameters; otherwise, NULL + */ + public function query($key = NULL, $xss_clean = NULL) + { + if ($key === NULL) + { + return $this->_query_args; + } + + return isset($this->_query_args[$key]) ? $this->_xss_clean($this->_query_args[$key], $xss_clean) : NULL; + } + + /** + * Sanitizes data so that Cross Site Scripting Hacks can be + * prevented + * + * @access protected + * @param string $value Input data + * @param bool $xss_clean Whether to apply XSS filtering + * @return string + */ + protected function _xss_clean($value, $xss_clean) + { + is_bool($xss_clean) || $xss_clean = $this->_enable_xss; + + return $xss_clean === TRUE ? $this->security->xss_clean($value) : $value; + } + + /** + * Retrieve the validation errors + * + * @access public + * @return array + */ + public function validation_errors() + { + $string = strip_tags($this->form_validation->error_string()); + + return explode(PHP_EOL, trim($string, PHP_EOL)); + } + + // SECURITY FUNCTIONS --------------------------------------------------------- + + /** + * Perform LDAP Authentication + * + * @access protected + * @param string $username The username to validate + * @param string $password The password to validate + * @return bool + */ + protected function _perform_ldap_auth($username = '', $password = NULL) + { + if (empty($username)) + { + log_message('debug', 'LDAP Auth: failure, empty username'); + return FALSE; + } + + log_message('debug', 'LDAP Auth: Loading configuration'); + + $this->config->load('ldap.php', TRUE); + + $ldap = [ + 'timeout' => $this->config->item('timeout', 'ldap'), + 'host' => $this->config->item('server', 'ldap'), + 'port' => $this->config->item('port', 'ldap'), + 'rdn' => $this->config->item('binduser', 'ldap'), + 'pass' => $this->config->item('bindpw', 'ldap'), + 'basedn' => $this->config->item('basedn', 'ldap'), + ]; + + log_message('debug', 'LDAP Auth: Connect to ' . (isset($ldaphost) ? $ldaphost : '[ldap not configured]')); + + // Connect to the ldap server + $ldapconn = ldap_connect($ldap['host'], $ldap['port']); + if ($ldapconn) + { + log_message('debug', 'Setting timeout to ' . $ldap['timeout'] . ' seconds'); + + ldap_set_option($ldapconn, LDAP_OPT_NETWORK_TIMEOUT, $ldap['timeout']); + + log_message('debug', 'LDAP Auth: Binding to ' . $ldap['host'] . ' with dn ' . $ldap['rdn']); + + // Binding to the ldap server + $ldapbind = ldap_bind($ldapconn, $ldap['rdn'], $ldap['pass']); + + // Verify the binding + if ($ldapbind === FALSE) + { + log_message('error', 'LDAP Auth: bind was unsuccessful'); + return FALSE; + } + + log_message('debug', 'LDAP Auth: bind successful'); + } + + // Search for user + if (($res_id = ldap_search($ldapconn, $ldap['basedn'], "uid=$username")) === FALSE) + { + log_message('error', 'LDAP Auth: User ' . $username . ' not found in search'); + return FALSE; + } + + if (ldap_count_entries($ldapconn, $res_id) !== 1) + { + log_message('error', 'LDAP Auth: Failure, username ' . $username . 'found more than once'); + return FALSE; + } + + if (($entry_id = ldap_first_entry($ldapconn, $res_id)) === FALSE) + { + log_message('error', 'LDAP Auth: Failure, entry of search result could not be fetched'); + return FALSE; + } + + if (($user_dn = ldap_get_dn($ldapconn, $entry_id)) === FALSE) + { + log_message('error', 'LDAP Auth: Failure, user-dn could not be fetched'); + return FALSE; + } + + // User found, could not authenticate as user + if (($link_id = ldap_bind($ldapconn, $user_dn, $password)) === FALSE) + { + log_message('error', 'LDAP Auth: Failure, username/password did not match: ' . $user_dn); + return FALSE; + } + + log_message('debug', 'LDAP Auth: Success ' . $user_dn . ' authenticated successfully'); + + $this->_user_ldap_dn = $user_dn; + + ldap_close($ldapconn); + + return TRUE; + } + + /** + * Perform Library Authentication - Override this function to change the way the library is called + * + * @access protected + * @param string $username The username to validate + * @param string $password The password to validate + * @return bool + */ + protected function _perform_library_auth($username = '', $password = NULL) + { + if (empty($username)) + { + log_message('error', 'Library Auth: Failure, empty username'); + return FALSE; + } + + $auth_library_class = strtolower($this->config->item('auth_library_class')); + $auth_library_function = strtolower($this->config->item('auth_library_function')); + + if (empty($auth_library_class)) + { + log_message('debug', 'Library Auth: Failure, empty auth_library_class'); + return FALSE; + } + + if (empty($auth_library_function)) + { + log_message('debug', 'Library Auth: Failure, empty auth_library_function'); + return FALSE; + } + + if (is_callable([$auth_library_class, $auth_library_function]) === FALSE) + { + $this->load->library($auth_library_class); + } + + return $this->{$auth_library_class}->$auth_library_function($username, $password); + } + + /** + * Check if the user is logged in + * + * @access protected + * @param string $username The user's name + * @param bool|string $password The user's password + * @return bool + */ + protected function _check_login($username = NULL, $password = FALSE) + { + if (empty($username)) + { + return FALSE; + } + + $auth_source = strtolower($this->config->item('auth_source')); + $rest_auth = strtolower($this->config->item('rest_auth')); + $valid_logins = $this->config->item('rest_valid_logins'); + + if (!$this->config->item('auth_source') && $rest_auth === 'digest') + { + // For digest we do not have a password passed as argument + return md5($username . ':' . $this->config->item('rest_realm') . ':' . (isset($valid_logins[$username]) ? $valid_logins[$username] : '')); + } + + if ($password === FALSE) + { + return FALSE; + } + + if ($auth_source === 'ldap') + { + log_message('debug', "Performing LDAP authentication for $username"); + + return $this->_perform_ldap_auth($username, $password); + } + + if ($auth_source === 'library') + { + log_message('debug', "Performing Library authentication for $username"); + + return $this->_perform_library_auth($username, $password); + } + + if (array_key_exists($username, $valid_logins) === FALSE) + { + return FALSE; + } + + if ($valid_logins[$username] !== $password) + { + return FALSE; + } + + return TRUE; + } + + /** + * Check to see if the user is logged in with a PHP session key + * + * @access protected + * @return void + */ + protected function _check_php_session() + { + // Get the auth_source config item + $key = $this->config->item('auth_source'); + + // If falsy, then the user isn't logged in + if (!$this->session->userdata($key)) + { + // Display an error response + $this->response([ + $this->config->item('rest_status_field_name') => FALSE, + $this->config->item('rest_message_field_name') => $this->lang->line('text_rest_unauthorized') + ], self::HTTP_UNAUTHORIZED); + } + } + + /** + * Prepares for basic authentication + * + * @access protected + * @return void + */ + protected function _prepare_basic_auth() + { + // If whitelist is enabled it has the first chance to kick them out + if ($this->config->item('rest_ip_whitelist_enabled')) + { + $this->_check_whitelist_auth(); + } + + // Returns NULL if the SERVER variables PHP_AUTH_USER and HTTP_AUTHENTICATION don't exist + $username = $this->input->server('PHP_AUTH_USER'); + $http_auth = $this->input->server('HTTP_AUTHENTICATION'); + + $password = NULL; + if ($username !== NULL) + { + $password = $this->input->server('PHP_AUTH_PW'); + } + elseif ($http_auth !== NULL) + { + // If the authentication header is set as basic, then extract the username and password from + // HTTP_AUTHORIZATION e.g. my_username:my_password. This is passed in the .htaccess file + if (strpos(strtolower($http_auth), 'basic') === 0) + { + // Search online for HTTP_AUTHORIZATION workaround to explain what this is doing + list($username, $password) = explode(':', base64_decode(substr($this->input->server('HTTP_AUTHORIZATION'), 6))); + } + } + + // Check if the user is logged into the system + if ($this->_check_login($username, $password) === FALSE) + { + $this->_force_login(); + } + } + + /** + * Prepares for digest authentication + * + * @access protected + * @return void + */ + protected function _prepare_digest_auth() + { + // If whitelist is enabled it has the first chance to kick them out + if ($this->config->item('rest_ip_whitelist_enabled')) + { + $this->_check_whitelist_auth(); + } + + // We need to test which server authentication variable to use, + // because the PHP ISAPI module in IIS acts different from CGI + $digest_string = $this->input->server('PHP_AUTH_DIGEST'); + if ($digest_string === NULL) + { + $digest_string = $this->input->server('HTTP_AUTHORIZATION'); + } + + $unique_id = uniqid(); + + // The $_SESSION['error_prompted'] variable is used to ask the password + // again if none given or if the user enters wrong auth information + if (empty($digest_string)) + { + $this->_force_login($unique_id); + } + + // We need to retrieve authentication data from the $digest_string variable + $matches = []; + preg_match_all('@(username|nonce|uri|nc|cnonce|qop|response)=[\'"]?([^\'",]+)@', $digest_string, $matches); + $digest = (empty($matches[1]) || empty($matches[2])) ? [] : array_combine($matches[1], $matches[2]); + + // For digest authentication the library function should return already stored md5(username:restrealm:password) for that username @see rest.php::auth_library_function config + $username = $this->_check_login($digest['username'], TRUE); + if (array_key_exists('username', $digest) === FALSE || $username === FALSE) + { + $this->_force_login($unique_id); + } + + $md5 = md5(strtoupper($this->request->method) . ':' . $digest['uri']); + $valid_response = md5($username . ':' . $digest['nonce'] . ':' . $digest['nc'] . ':' . $digest['cnonce'] . ':' . $digest['qop'] . ':' . $md5); + + // Check if the string don't compare (case-insensitive) + if (strcasecmp($digest['response'], $valid_response) !== 0) + { + // Display an error response + $this->response([ + $this->config->item('rest_status_field_name') => FALSE, + $this->config->item('rest_message_field_name') => $this->lang->line('text_rest_invalid_credentials') + ], self::HTTP_UNAUTHORIZED); + } + } + + /** + * Checks if the client's ip is in the 'rest_ip_blacklist' config and generates a 401 response + * + * @access protected + * @return void + */ + protected function _check_blacklist_auth() + { + // Match an ip address in a blacklist e.g. 127.0.0.0, 0.0.0.0 + $pattern = sprintf('/(?:,\s*|^)\Q%s\E(?=,\s*|$)/m', $this->input->ip_address()); + + // Returns 1, 0 or FALSE (on error only). Therefore implicitly convert 1 to TRUE + if (preg_match($pattern, $this->config->item('rest_ip_blacklist'))) + { + // Display an error response + $this->response([ + $this->config->item('rest_status_field_name') => FALSE, + $this->config->item('rest_message_field_name') => $this->lang->line('text_rest_ip_denied') + ], self::HTTP_UNAUTHORIZED); + } + } + + /** + * Check if the client's ip is in the 'rest_ip_whitelist' config and generates a 401 response + * + * @access protected + * @return void + */ + protected function _check_whitelist_auth() + { + $whitelist = explode(',', $this->config->item('rest_ip_whitelist')); + + array_push($whitelist, '127.0.0.1', '0.0.0.0'); + + foreach ($whitelist as &$ip) + { + // As $ip is a reference, trim leading and trailing whitespace, then store the new value + // using the reference + $ip = trim($ip); + } + + if (in_array($this->input->ip_address(), $whitelist) === FALSE) + { + $this->response([ + $this->config->item('rest_status_field_name') => FALSE, + $this->config->item('rest_message_field_name') => $this->lang->line('text_rest_ip_unauthorized') + ], self::HTTP_UNAUTHORIZED); + } + } + + /** + * Force logging in by setting the WWW-Authenticate header + * + * @access protected + * @param string $nonce A server-specified data string which should be uniquely generated + * each time + * @return void + */ + protected function _force_login($nonce = '') + { + $rest_auth = $this->config->item('rest_auth'); + $rest_realm = $this->config->item('rest_realm'); + if (strtolower($rest_auth) === 'basic') + { + // See http://tools.ietf.org/html/rfc2617#page-5 + header('WWW-Authenticate: Basic realm="' . $rest_realm . '"'); + } + elseif (strtolower($rest_auth) === 'digest') + { + // See http://tools.ietf.org/html/rfc2617#page-18 + header( + 'WWW-Authenticate: Digest realm="' . $rest_realm + . '", qop="auth", nonce="' . $nonce + . '", opaque="' . md5($rest_realm) . '"'); + } + + // Display an error response + $this->response([ + $this->config->item('rest_status_field_name') => FALSE, + $this->config->item('rest_message_field_name') => $this->lang->line('text_rest_unauthorized') + ], self::HTTP_UNAUTHORIZED); + } + + /** + * Updates the log table with the total access time + * + * @access protected + * @author Chris Kacerguis + * @return bool TRUE log table updated; otherwise, FALSE + */ + protected function _log_access_time() + { + $payload['rtime'] = $this->_end_rtime - $this->_start_rtime; + + return $this->rest->db->update( + $this->config->item('rest_logs_table'), $payload, [ + 'id' => $this->_insert_id + ]); + } + + /** + * Updates the log table with HTTP response code + * + * @access protected + * @author Justin Chen + * @param $http_code int HTTP status code + * @return bool TRUE log table updated; otherwise, FALSE + */ + protected function _log_response_code($http_code) + { + $payload['response_code'] = $http_code; + + return $this->rest->db->update( + $this->config->item('rest_logs_table'), $payload, [ + 'id' => $this->_insert_id + ]); + } + + /** + * Check to see if the API key has access to the controller and methods + * + * @access protected + * @return bool TRUE the API key has access; otherwise, FALSE + */ + protected function _check_access() + { + // If we don't want to check access, just return TRUE + if ($this->config->item('rest_enable_access') === FALSE) + { + return TRUE; + } + + // Fetch controller based on path and controller name + $controller = implode( + '/', [ + $this->router->directory, + $this->router->class + ]); + + // Remove any double slashes for safety + $controller = str_replace('//', '/', $controller); + + // Query the access table and get the number of results + return $this->rest->db + ->where('key', $this->rest->key) + ->where('controller', $controller) + ->get($this->config->item('rest_access_table')) + ->num_rows() > 0; + } + +} diff --git a/application/libraries/fhcauth.php b/application/libraries/fhcauth.php new file mode 100644 index 000000000..c7b905edc --- /dev/null +++ b/application/libraries/fhcauth.php @@ -0,0 +1,51 @@ +checkpassword($username, $password)) + { + echo 'Auth-Method-False'; + return true; + } + else + { + echo 'Auth-Method-False'; + return false; + } + } +}