From 877447c1efefc3a2717f5417a941ea1bb55adb7a Mon Sep 17 00:00:00 2001 From: softwarespot Date: Tue, 21 Jul 2015 08:05:05 +0300 Subject: [PATCH 01/15] Fixed missing semi-colon Fixes #512 --- application/libraries/REST_Controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/libraries/REST_Controller.php b/application/libraries/REST_Controller.php index 1cb9524a..1e04a1c6 100644 --- a/application/libraries/REST_Controller.php +++ b/application/libraries/REST_Controller.php @@ -969,7 +969,7 @@ protected function _detect_api_key() */ protected function _detect_lang() { - $lang = $this->input->server('HTTP_ACCEPT_LANGUAGE') + $lang = $this->input->server('HTTP_ACCEPT_LANGUAGE'); if ($lang === NULL) { return NULL; From 391737275947239c52d95caa51fa5a5b34cc73e8 Mon Sep 17 00:00:00 2001 From: softwarespot Date: Tue, 21 Jul 2015 08:42:10 +0300 Subject: [PATCH 02/15] Rebased code created by @lagaisse Closes #510 --- application/config/rest.php | 17 +++++++- application/libraries/REST_Controller.php | 48 +++++++++++++++++------ 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/application/config/rest.php b/application/config/rest.php index 72eddd52..d75c8696 100644 --- a/application/config/rest.php +++ b/application/config/rest.php @@ -270,7 +270,7 @@ | `id` INT(11) NOT NULL AUTO_INCREMENT, | `key` VARCHAR(40) NOT NULL, | `level` INT(2) NOT NULL, -| `ignore_limits` TINY(1) NOT NULL DEFAULT '0', +| `ignore_limits` TINYINT(1) NOT NULL DEFAULT '0', | `is_private_key` TINYINT(1) NOT NULL DEFAULT '0', | `ip_addresses` TEXT NULL DEFAULT NULL, | `date_created` INT(11) NOT NULL, @@ -291,6 +291,21 @@ */ $config['rest_key_column'] = 'key'; +/* +|-------------------------------------------------------------------------- +| REST API Limits method +|-------------------------------------------------------------------------- +| +| Specify the method used to limit the API calls +| +| Available methods are : +| $config['rest_limits_method'] = 'API_KEY'; // Put a limit per api key +| $config['rest_limits_method'] = 'METHOD_NAME'; // Put a limit on method calls +| $config['rest_limits_method'] = 'ROUTED_URL'; // Put a limit on the routed URL +| +*/ +$config['rest_limits_method'] = 'ROUTED_URL'; + /* |-------------------------------------------------------------------------- | REST Key Length diff --git a/application/libraries/REST_Controller.php b/application/libraries/REST_Controller.php index 1e04a1c6..0f9e672c 100644 --- a/application/libraries/REST_Controller.php +++ b/application/libraries/REST_Controller.php @@ -1033,36 +1033,60 @@ protected function _log_request($authorized = 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 || isset($this->methods[$controller_method]['limit']) === FALSE) + if (empty($this->rest->ignore_limits) === 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[$controller_method]['limit']; + 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; + } - $uri_noext = $this->uri->uri_string(); - if (strpos(strrev($this->uri->uri_string()), strrev($this->response->format)) === 0) + if (isset($this->methods[$limited_method_name]['limit']) === FALSE ) { - $uri_noext = substr($this->uri->uri_string(),0, -strlen($this->response->format) - 1); + // 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']; + + $timelimit = (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', $uri_noext) + ->where('uri', $limited_uri) ->where('api_key', $this->rest->key) ->get($this->config->item('rest_limits_table')) ->row(); - $time_limit = (isset($this->methods[$controller_method]['time']) ? $this->methods[$controller_method]['time'] : 3600); - // 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' => $this->uri->uri_string(), + 'uri' => $limited_uri, 'api_key' => isset($this->rest->key) ? $this->rest->key : '', 'count' => 1, 'hour_started' => time() @@ -1074,7 +1098,7 @@ protected function _check_limit($controller_method) { // Reset the started period and count $this->rest->db - ->where('uri', $uri_noext) + ->where('uri', $limited_uri) ->where('api_key', isset($this->rest->key) ? $this->rest->key : '') ->set('hour_started', time()) ->set('count', 1) @@ -1092,7 +1116,7 @@ protected function _check_limit($controller_method) // Increase the count by one $this->rest->db - ->where('uri', $uri_noext) + ->where('uri', $limited_uri) ->where('api_key', $this->rest->key) ->set('count', 'count + 1', FALSE) ->update($this->config->item('rest_limits_table')); From 8dfdf59514409d11976d5667ff2bcbd0c544bb68 Mon Sep 17 00:00:00 2001 From: softwarespot Date: Tue, 21 Jul 2015 20:06:34 +0300 Subject: [PATCH 03/15] Fixed undefined constants Mentioned by @lagaisse #515 --- application/controllers/api/Example.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/controllers/api/Example.php b/application/controllers/api/Example.php index 0d837d0d..8e6b994a 100644 --- a/application/controllers/api/Example.php +++ b/application/controllers/api/Example.php @@ -57,7 +57,7 @@ public function user_get() $this->set_response([ 'status' => FALSE, 'error' => 'User could not be found' - ], REST_Controller::NOT_FOUND); // NOT_FOUND (404) being the HTTP response code + ], REST_Controller::HTTP_NOT_FOUND); // NOT_FOUND (404) being the HTTP response code } } @@ -103,7 +103,7 @@ public function users_get() $this->set_response([ 'status' => FALSE, 'error' => 'No users were found' - ], REST_Controller::NOT_FOUND); // NOT_FOUND (404) being the HTTP response code + ], REST_Controller::HTTP_NOT_FOUND); // NOT_FOUND (404) being the HTTP response code } } } From c24b3caf543c40ea44d58ed3ae1ab03936c630da Mon Sep 17 00:00:00 2001 From: softwarespot Date: Tue, 21 Jul 2015 20:44:37 +0300 Subject: [PATCH 04/15] Fixed issue if a developer doesn't pass an array --- application/libraries/Format.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/application/libraries/Format.php b/application/libraries/Format.php index 8d073067..1c34ffeb 100644 --- a/application/libraries/Format.php +++ b/application/libraries/Format.php @@ -357,6 +357,13 @@ public function to_csv($data = NULL, $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; + } + // Returns the length of the string written or FALSE fputcsv($handle, $record, $delimiter, $enclosure); } From 5085669c5397ffda7df2b97aa7907efb247ad6d0 Mon Sep 17 00:00:00 2001 From: softwarespot Date: Tue, 21 Jul 2015 23:25:08 +0300 Subject: [PATCH 05/15] Changed || to OR as part of the CI style guide #516 --- application/libraries/Format.php | 6 +++--- application/libraries/REST_Controller.php | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/application/libraries/Format.php b/application/libraries/Format.php index 1c34ffeb..db1f6691 100644 --- a/application/libraries/Format.php +++ b/application/libraries/Format.php @@ -149,7 +149,7 @@ public function to_array($data = NULL) $array = []; foreach ((array) $data as $key => $value) { - if (is_object($value) === TRUE || is_array($value) === TRUE) + if (is_object($value) === TRUE OR is_array($value) === TRUE) { $array[$key] = $this->to_array($value); } @@ -214,7 +214,7 @@ public function to_xml($data = NULL, $structure = NULL, $basenode = 'xml') // replace anything not alpha numeric $key = preg_replace('/[^a-z_\-0-9]/i', '', $key); - if ($key === '_attributes' && (is_array($value) || is_object($value))) + if ($key === '_attributes' && (is_array($value) OR is_object($value))) { $attributes = $value; if (is_object($attributes)) @@ -228,7 +228,7 @@ public function to_xml($data = NULL, $structure = NULL, $basenode = 'xml') } } // if there is another array found recursively call this function - elseif (is_array($value) || is_object($value)) + elseif (is_array($value) OR is_object($value)) { $node = $structure->addChild($key); diff --git a/application/libraries/REST_Controller.php b/application/libraries/REST_Controller.php index 0f9e672c..c055614c 100644 --- a/application/libraries/REST_Controller.php +++ b/application/libraries/REST_Controller.php @@ -473,7 +473,7 @@ public function __construct($config = 'rest') $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'))) + if ($this->config->item('rest_database_group') && ($this->config->item('rest_enable_keys') OR $this->config->item('rest_enable_logging'))) { $this->rest->db = $this->load->database($this->config->item('rest_database_group'), TRUE); } @@ -627,7 +627,7 @@ public function _remap($object_called, $arguments) // They don't have good enough perms $response = [$this->config->item('rest_status_field_name') => FALSE, $this->config->item('rest_message_field_name') => 'This API key does not have enough permissions.']; - $authorized || $this->response($response, self::HTTP_UNAUTHORIZED); + $authorized OR $this->response($response, self::HTTP_UNAUTHORIZED); } // No key stuff, but record that stuff is happening @@ -701,7 +701,7 @@ public function response($data = NULL, $http_code = NULL, $continue = FALSE) else { // If an array or object, then parse as a json, so as to be a 'string' - if (is_array($data) || is_object($data)) + if (is_array($data) OR is_object($data)) { $data = $this->format->factory($data)->{'to_json'}(); } @@ -714,7 +714,7 @@ public function response($data = NULL, $http_code = NULL, $continue = FALSE) // 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; + $http_code > 0 OR $http_code = self::HTTP_OK; $this->output->set_status_header($http_code); @@ -1914,11 +1914,11 @@ protected function _prepare_digest_auth() // 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]); + $digest = (empty($matches[1]) OR 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 $A1 = $this->_check_login($digest['username'], TRUE); - if (array_key_exists('username', $digest) === FALSE || $A1 === FALSE) + if (array_key_exists('username', $digest) === FALSE OR $A1 === FALSE) { $this->_force_login($uniqueId); } From 90262afe1017287138b25cdb32fc041afd1b739e Mon Sep 17 00:00:00 2001 From: softwarespot Date: Tue, 21 Jul 2015 23:37:11 +0300 Subject: [PATCH 06/15] Renamed variables to adhere to the CI style guide --- application/libraries/Format.php | 4 +-- application/libraries/REST_Controller.php | 42 +++++++++++------------ 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/application/libraries/Format.php b/application/libraries/Format.php index db1f6691..eac62e45 100644 --- a/application/libraries/Format.php +++ b/application/libraries/Format.php @@ -222,9 +222,9 @@ public function to_xml($data = NULL, $structure = NULL, $basenode = 'xml') $attributes = get_object_vars($attributes); } - foreach ($attributes as $attributeName => $attributeValue) + foreach ($attributes as $attribute_name => $attribute_value) { - $structure->addAttribute($attributeName, $attributeValue); + $structure->addAttribute($attribute_name, $attribute_value); } } // if there is another array found recursively call this function diff --git a/application/libraries/REST_Controller.php b/application/libraries/REST_Controller.php index c055614c..7e92c633 100644 --- a/application/libraries/REST_Controller.php +++ b/application/libraries/REST_Controller.php @@ -761,9 +761,9 @@ public function set_response($data = NULL, $http_code = NULL) protected function _detect_input_format() { // Get the CONTENT-TYPE value from the SERVER variable - $contentType = $this->input->server('CONTENT_TYPE'); + $content_type = $this->input->server('CONTENT_TYPE'); - if (empty($contentType) === FALSE) + if (empty($content_type) === FALSE) { // Check all formats against the HTTP_ACCEPT header foreach ($this->_supported_formats as $key => $value) @@ -773,10 +773,10 @@ protected function _detect_input_format() // 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 - $contentType = (strpos($contentType, ';') !== FALSE ? current(explode(';', $contentType)) : $contentType); + $content_type = (strpos($content_type, ';') !== FALSE ? current(explode(';', $content_type)) : $content_type); // If both the mime types match, then return the format - if ($contentType === $value) + if ($content_type === $value) { return $key; } @@ -1005,7 +1005,7 @@ protected function _detect_lang() protected function _log_request($authorized = FALSE) { // Insert the request into the log table - $isInserted = $this->rest->db + $is_inserted = $this->rest->db ->insert( $this->config->item('rest_logs_table'), [ 'uri' => $this->uri->uri_string(), @@ -1020,7 +1020,7 @@ protected function _log_request($authorized = FALSE) // Get the last insert id to update at a later stage of the request $this->_insert_id = $this->rest->db->insert_id(); - return $isInserted; + return $is_inserted; } /** @@ -1902,13 +1902,13 @@ protected function _prepare_digest_auth() $digest_string = $this->input->server('HTTP_AUTHORIZATION'); } - $uniqueId = uniqid(); + $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($uniqueId); + $this->_force_login($unique_id); } // We need to retrieve authentication data from the $digest_string variable @@ -1917,14 +1917,14 @@ protected function _prepare_digest_auth() $digest = (empty($matches[1]) OR 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 - $A1 = $this->_check_login($digest['username'], TRUE); - if (array_key_exists('username', $digest) === FALSE OR $A1 === FALSE) + $username = $this->_check_login($digest['username'], TRUE); + if (array_key_exists('username', $digest) === FALSE OR $username === FALSE) { - $this->_force_login($uniqueId); + $this->_force_login($unique_id); } - $A2 = md5(strtoupper($this->request->method) . ':' . $digest['uri']); - $valid_response = md5($A1 . ':' . $digest['nonce'] . ':' . $digest['nc'] . ':' . $digest['cnonce'] . ':' . $digest['qop'] . ':' . $A2); + $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) @@ -1972,7 +1972,7 @@ protected function _check_whitelist_auth() array_push($whitelist, '127.0.0.1', '0.0.0.0'); - foreach ($whitelist AS &$ip) + foreach ($whitelist as &$ip) { $ip = trim($ip); } @@ -1993,20 +1993,20 @@ protected function _check_whitelist_auth() */ protected function _force_login($nonce = '') { - $restAuth = $this->config->item('rest_auth'); - $restRealm = $this->config->item('rest_realm'); - if (strtolower($restAuth) === 'basic') + $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="' . $restRealm . '"'); + header('WWW-Authenticate: Basic realm="' . $rest_realm . '"'); } - elseif (strtolower($restAuth) === 'digest') + elseif (strtolower($rest_auth) === 'digest') { // See http://tools.ietf.org/html/rfc2617#page-18 header( - 'WWW-Authenticate: Digest realm="' . $restRealm + 'WWW-Authenticate: Digest realm="' . $rest_realm . '", qop="auth", nonce="' . $nonce - . '", opaque="' . md5($restRealm) . '"'); + . '", opaque="' . md5($rest_realm) . '"'); } // Display an error response From bba6dffe29ed5f1443769e6534d23e6a738eb7ec Mon Sep 17 00:00:00 2001 From: softwarespot Date: Tue, 21 Jul 2015 23:40:10 +0300 Subject: [PATCH 07/15] Reverted OR => || --- application/libraries/Format.php | 6 +++--- application/libraries/REST_Controller.php | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/application/libraries/Format.php b/application/libraries/Format.php index eac62e45..e008c170 100644 --- a/application/libraries/Format.php +++ b/application/libraries/Format.php @@ -149,7 +149,7 @@ public function to_array($data = NULL) $array = []; foreach ((array) $data as $key => $value) { - if (is_object($value) === TRUE OR is_array($value) === TRUE) + if (is_object($value) === TRUE || is_array($value) === TRUE) { $array[$key] = $this->to_array($value); } @@ -214,7 +214,7 @@ public function to_xml($data = NULL, $structure = NULL, $basenode = 'xml') // replace anything not alpha numeric $key = preg_replace('/[^a-z_\-0-9]/i', '', $key); - if ($key === '_attributes' && (is_array($value) OR is_object($value))) + if ($key === '_attributes' && (is_array($value) || is_object($value))) { $attributes = $value; if (is_object($attributes)) @@ -228,7 +228,7 @@ public function to_xml($data = NULL, $structure = NULL, $basenode = 'xml') } } // if there is another array found recursively call this function - elseif (is_array($value) OR is_object($value)) + elseif (is_array($value) || is_object($value)) { $node = $structure->addChild($key); diff --git a/application/libraries/REST_Controller.php b/application/libraries/REST_Controller.php index 7e92c633..e6a0cced 100644 --- a/application/libraries/REST_Controller.php +++ b/application/libraries/REST_Controller.php @@ -473,7 +473,7 @@ public function __construct($config = 'rest') $this->early_checks(); // Load DB if its enabled - if ($this->config->item('rest_database_group') && ($this->config->item('rest_enable_keys') OR $this->config->item('rest_enable_logging'))) + 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); } @@ -627,7 +627,7 @@ public function _remap($object_called, $arguments) // They don't have good enough perms $response = [$this->config->item('rest_status_field_name') => FALSE, $this->config->item('rest_message_field_name') => 'This API key does not have enough permissions.']; - $authorized OR $this->response($response, self::HTTP_UNAUTHORIZED); + $authorized || $this->response($response, self::HTTP_UNAUTHORIZED); } // No key stuff, but record that stuff is happening @@ -701,7 +701,7 @@ public function response($data = NULL, $http_code = NULL, $continue = FALSE) else { // If an array or object, then parse as a json, so as to be a 'string' - if (is_array($data) OR is_object($data)) + if (is_array($data) || is_object($data)) { $data = $this->format->factory($data)->{'to_json'}(); } @@ -714,7 +714,7 @@ public function response($data = NULL, $http_code = NULL, $continue = FALSE) // 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 OR $http_code = self::HTTP_OK; + $http_code > 0 || $http_code = self::HTTP_OK; $this->output->set_status_header($http_code); @@ -1602,7 +1602,7 @@ public function query($key = NULL, $xss_clean = NULL) */ protected function _xss_clean($value, $xss_clean) { - is_bool($xss_clean) OR $xss_clean = $this->_enable_xss; + is_bool($xss_clean) || $xss_clean = $this->_enable_xss; return $xss_clean === TRUE ? $this->security->xss_clean($value) : $value; } @@ -1914,11 +1914,11 @@ protected function _prepare_digest_auth() // 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]) OR empty($matches[2])) ? [] : array_combine($matches[1], $matches[2]); + $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 OR $username === FALSE) + if (array_key_exists('username', $digest) === FALSE || $username === FALSE) { $this->_force_login($unique_id); } From 170c41bb53332b542080e9b5c073ea8800688969 Mon Sep 17 00:00:00 2001 From: softwarespot Date: Tue, 21 Jul 2015 23:50:05 +0300 Subject: [PATCH 08/15] Unified response lines --- application/controllers/api/Example.php | 4 +-- application/libraries/REST_Controller.php | 36 ++++++++++++++++------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/application/controllers/api/Example.php b/application/controllers/api/Example.php index 8e6b994a..2fafbc6c 100644 --- a/application/controllers/api/Example.php +++ b/application/controllers/api/Example.php @@ -101,8 +101,8 @@ public function users_get() else { $this->set_response([ - 'status' => FALSE, - 'error' => 'No users were found' + 'status' => FALSE, + 'error' => 'No users were found' ], REST_Controller::HTTP_NOT_FOUND); // NOT_FOUND (404) being the HTTP response code } } diff --git a/application/libraries/REST_Controller.php b/application/libraries/REST_Controller.php index e6a0cced..885c4837 100644 --- a/application/libraries/REST_Controller.php +++ b/application/libraries/REST_Controller.php @@ -561,7 +561,10 @@ 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') => 'Unsupported protocol'], self::HTTP_FORBIDDEN); + $this->response([ + $this->config->item('rest_status_field_name') => FALSE, + $this->config->item('rest_message_field_name') => 'Unsupported protocol' + ], self::HTTP_FORBIDDEN); } // Remove the supported format from the function name e.g. index.json => index @@ -583,7 +586,10 @@ public function _remap($object_called, $arguments) $this->_log_request(); } - $this->response([$this->config->item('rest_status_field_name') => FALSE, $this->config->item('rest_message_field_name') => 'Invalid API Key ' . $this->rest->key], self::HTTP_FORBIDDEN); + $this->response([ + $this->config->item('rest_status_field_name') => FALSE, + $this->config->item('rest_message_field_name') => 'Invalid API Key ' . $this->rest->key + ], self::HTTP_FORBIDDEN); } // Check to see if this key has access to the requested controller. @@ -594,13 +600,19 @@ public function _remap($object_called, $arguments) $this->_log_request(); } - $this->response([$this->config->item('rest_status_field_name') => FALSE, $this->config->item('rest_message_field_name') => 'This API key does not have access to the requested controller.'], self::HTTP_UNAUTHORIZED); + $this->response([ + $this->config->item('rest_status_field_name') => FALSE, + $this->config->item('rest_message_field_name') => 'This API key does not have access to the requested controller.' + ], 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') => 'Unknown method.'], self::HTTP_NOT_FOUND); + $this->response([ + $this->config->item('rest_status_field_name') => FALSE, + $this->config->item('rest_message_field_name') => 'Unknown method.' + ], self::HTTP_NOT_FOUND); } // Doing key related stuff? Can only do it if they have a key right? @@ -645,10 +657,10 @@ public function _remap($object_called, $arguments) { // 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() + $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); } @@ -1955,8 +1967,7 @@ protected function _check_blacklist_auth() $this->response([ 'status' => FALSE, 'error' => 'IP Denied' - ], - self::HTTP_UNAUTHORIZED); + ], self::HTTP_UNAUTHORIZED); } } @@ -1979,7 +1990,10 @@ protected function _check_whitelist_auth() 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') => 'IP not authorized'], self::HTTP_UNAUTHORIZED); + $this->response([ + $this->config->item('rest_status_field_name') => FALSE, + $this->config->item('rest_message_field_name') => 'IP not authorized' + ], self::HTTP_UNAUTHORIZED); } } From 8122c829f3371a9d62a9314c7b3837b6eea77a60 Mon Sep 17 00:00:00 2001 From: softwarespot Date: Tue, 21 Jul 2015 23:58:14 +0300 Subject: [PATCH 09/15] Added comment and fixed indentation --- application/libraries/REST_Controller.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/application/libraries/REST_Controller.php b/application/libraries/REST_Controller.php index 885c4837..c2b794e1 100644 --- a/application/libraries/REST_Controller.php +++ b/application/libraries/REST_Controller.php @@ -1985,6 +1985,8 @@ protected function _check_whitelist_auth() 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); } @@ -2041,8 +2043,7 @@ protected function _log_access_time() { $payload['rtime'] = $this->_end_rtime - $this->_start_rtime; - return $this->rest->db - ->update( + return $this->rest->db->update( $this->config->item('rest_logs_table'), $payload, [ 'id' => $this->_insert_id ]); From fd8bf1cbc4fb23a8a6fa5ddd74f6fb991eadbcd1 Mon Sep 17 00:00:00 2001 From: softwarespot Date: Wed, 22 Jul 2015 00:01:07 +0300 Subject: [PATCH 10/15] Changed calling input->server() function twice --- application/libraries/REST_Controller.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/application/libraries/REST_Controller.php b/application/libraries/REST_Controller.php index c2b794e1..51e27390 100644 --- a/application/libraries/REST_Controller.php +++ b/application/libraries/REST_Controller.php @@ -1904,12 +1904,8 @@ protected function _prepare_digest_auth() // We need to test which server authentication variable to use, // because the PHP ISAPI module in IIS acts different from CGI - $digest_string = ''; - if ($this->input->server('PHP_AUTH_DIGEST')) - { - $digest_string = $this->input->server('PHP_AUTH_DIGEST'); - } - elseif ($this->input->server('HTTP_AUTHORIZATION')) + $digest_string = $this->input->server('PHP_AUTH_DIGEST'); + if ($digest_string === NULL) { $digest_string = $this->input->server('HTTP_AUTHORIZATION'); } From 0f3502563bc1b3f177c5c7b1aced3ab680a8337d Mon Sep 17 00:00:00 2001 From: softwarespot Date: Wed, 22 Jul 2015 01:02:59 +0300 Subject: [PATCH 11/15] Fixed incorrect comment --- application/libraries/REST_Controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/libraries/REST_Controller.php b/application/libraries/REST_Controller.php index 51e27390..98b3c9ce 100644 --- a/application/libraries/REST_Controller.php +++ b/application/libraries/REST_Controller.php @@ -466,7 +466,7 @@ public function __construct($config = 'rest') // Which format should the data be returned in? $this->response->format = $this->_detect_output_format(); - // Which format should the data be returned in? + // 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 From 5b415bcaf98cbb6912ed044391cc2403c7b68c87 Mon Sep 17 00:00:00 2001 From: softwarespot Date: Wed, 22 Jul 2015 01:42:33 +0300 Subject: [PATCH 12/15] Code by @lagaisse to fix #515 --- application/libraries/REST_Controller.php | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/application/libraries/REST_Controller.php b/application/libraries/REST_Controller.php index 98b3c9ce..f4efc6a6 100644 --- a/application/libraries/REST_Controller.php +++ b/application/libraries/REST_Controller.php @@ -807,13 +807,27 @@ protected function _detect_input_format() protected function _detect_output_format() { // Concatenate formats to a regex pattern e.g. \.(csv|json|xml) - $pattern = '/\.(' . implode('|', array_keys($this->_supported_formats)) . ')$/'; + $pattern = '/([^\/]+)\.(' . implode('|', array_keys($this->_supported_formats)) . ')$/'; - // Check if a file extension is used e.g. http://example.com/api/index.json?param1=param2 + // Check if a file extension is used e.g. http://example.com/api/resource/1.json?param1=param2 => http://example.com/api/resource/id/1.json?param1=param2 + // Match[0] = Full match i.e. value.format + // match[1] = value + // Match[2] = Format (with no dot) $matches = []; if (preg_match($pattern, $this->uri->uri_string(), $matches)) { - return $matches[1]; + // If GET params exist + if ($this->_get_args) + { + // array_slice() or saving _get_args_only when calling _parse_get + $only_get_args = array_diff($this->_get_args,$this->_query_args); + $arg_keys = array_keys($only_get_args); + $last_key = end($arg_keys); + $this->_get_args[$last_key] = $matches[1]; + $this->_args[$last_key] = $matches[1]; + } + + return $matches[2]; } // Get the format via the GET parameter labelled 'format' From 470fcee4536d88c428c0dfb333363091ded1e389 Mon Sep 17 00:00:00 2001 From: softwarespot Date: Wed, 22 Jul 2015 01:44:02 +0300 Subject: [PATCH 13/15] Fixed whitespace missing between comma --- application/libraries/REST_Controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/libraries/REST_Controller.php b/application/libraries/REST_Controller.php index f4efc6a6..c170dca6 100644 --- a/application/libraries/REST_Controller.php +++ b/application/libraries/REST_Controller.php @@ -820,7 +820,7 @@ protected function _detect_output_format() if ($this->_get_args) { // array_slice() or saving _get_args_only when calling _parse_get - $only_get_args = array_diff($this->_get_args,$this->_query_args); + $only_get_args = array_diff($this->_get_args, $this->_query_args); $arg_keys = array_keys($only_get_args); $last_key = end($arg_keys); $this->_get_args[$last_key] = $matches[1]; From f28a9c9285b87dec8dab15741da5add5285aafea Mon Sep 17 00:00:00 2001 From: softwarespot Date: Wed, 22 Jul 2015 07:12:14 +0300 Subject: [PATCH 14/15] Re-tweaked the example Contribution partially from @ivantcholakov --- application/controllers/api/Example.php | 44 ++++++++++++++++++++----- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/application/controllers/api/Example.php b/application/controllers/api/Example.php index 2fafbc6c..3a09c699 100644 --- a/application/controllers/api/Example.php +++ b/application/controllers/api/Example.php @@ -30,15 +30,26 @@ function __construct() $this->methods['user_delete']['limit'] = 50; // 50 requests per hour per user/key } - public function user_get() + public function user_get($id = NULL) { - if (!$this->get('id')) + // If the id has not been passed via the URL e.g. example/user/:id, then + // check the id query parameter id=? instead + if ($id === NULL) + { + $id = $this->get('id'); + } + + // Cast as an int + $id = (int) $id; + + // If not a valid id + if ($id <= 0) { // Set the response and exit $this->response(NULL, REST_Controller::HTTP_BAD_REQUEST); // BAD_REQUEST (400) being the HTTP response code } - // $user = $this->some_model->getSomething($this->get('id')); + // $user = $this->some_model->getSomething($id); $users = [ 1 => ['id' => 1, 'name' => 'John', 'email' => 'john@example.com', 'fact' => 'Loves coding'], 2 => ['id' => 2, 'name' => 'Jim', 'email' => 'jim@example.com', 'fact' => 'Developed on CodeIgniter'], @@ -46,7 +57,7 @@ public function user_get() ]; // Get the user from the array, by retrieving the id from the GET request - $user = isset($users[$this->get('id')]) ? $users[$this->get('id')] : NULL; + $user = isset($users[$id]) ? $users[$id] : NULL; if ($user) { @@ -63,9 +74,9 @@ public function user_get() public function user_post() { - // $this->some_model->update_user($this->get('id')); + // $this->some_model->update_user( ... ); $message = [ - 'id' => $this->get('id'), + 'id' => 100, // Automatically generated by the model 'name' => $this->post('name'), 'email' => $this->post('email'), 'message' => 'Added a resource' @@ -76,9 +87,26 @@ public function user_post() public function user_delete() { - // $this->some_model->delete_something($this->get()); + // If the id has not been passed via the URL e.g. example/user/:id, then + // check the id query parameter id=? instead + if ($id === NULL) + { + $id = $this->get('id'); + } + + // Cast as an int + $id = (int) $id; + + // If not a valid id + if ($id <= 0) + { + // Set the response and exit + $this->response(NULL, REST_Controller::HTTP_BAD_REQUEST); // BAD_REQUEST (400) being the HTTP response code + } + + // $this->some_model->delete_something($id); $message = [ - 'id' => $this->get('id'), + 'id' => $id, 'message' => 'Deleted the resource' ]; From 805b82c035b57188f1ed3594b33afe7411513faf Mon Sep 17 00:00:00 2001 From: softwarespot Date: Wed, 22 Jul 2015 09:17:06 +0300 Subject: [PATCH 15/15] Implemented code by @ivantcholakov Finally fixes #515 --- application/libraries/REST_Controller.php | 49 ++++++++++++----------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/application/libraries/REST_Controller.php b/application/libraries/REST_Controller.php index c170dca6..ebbc64b8 100644 --- a/application/libraries/REST_Controller.php +++ b/application/libraries/REST_Controller.php @@ -807,36 +807,39 @@ protected function _detect_input_format() protected function _detect_output_format() { // Concatenate formats to a regex pattern e.g. \.(csv|json|xml) - $pattern = '/([^\/]+)\.(' . implode('|', array_keys($this->_supported_formats)) . ')$/'; - - // Check if a file extension is used e.g. http://example.com/api/resource/1.json?param1=param2 => http://example.com/api/resource/id/1.json?param1=param2 - // Match[0] = Full match i.e. value.format - // match[1] = value - // Match[2] = Format (with no dot) + $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)) { - // If GET params exist - if ($this->_get_args) + return $matches[1]; + } + + if (empty($this->_get_args) === FALSE) + { + // Get the format parameter named as 'format' + if (isset($this->_get_args['format']) === TRUE) { - // array_slice() or saving _get_args_only when calling _parse_get - $only_get_args = array_diff($this->_get_args, $this->_query_args); - $arg_keys = array_keys($only_get_args); - $last_key = end($arg_keys); - $this->_get_args[$last_key] = $matches[1]; - $this->_args[$last_key] = $matches[1]; - } + $format = strtolower($this->_get_args['format']); - return $matches[2]; - } + if (isset($this->_supported_formats[$format]) === TRUE) + { + return $format; + } + } - // Get the format via the GET parameter labelled 'format' - $format = isset($this->_get_args['format']) ? strtolower($this->_get_args['format']) : NULL; + // A special case: users/1.json + elseif (count($this->_get_args) === 1 && reset($this->_get_args) === NULL) + { + $pattern = '/\.(' . implode('|', array_keys($this->_supported_formats)) . ')$/'; + $matches = []; - // A format has been passed as an argument in the URL and it is supported - if ($format !== NULL && isset($this->_supported_formats[$format])) - { - return $format; + if (preg_match($pattern, key($this->_get_args), $matches)) + { + return $matches[1]; + } + } } // Get the HTTP_ACCEPT server variable