From 177103a70c9b75e1c068db72b160260245af4d44 Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Sun, 10 Jan 2016 17:14:03 -0500 Subject: [PATCH 01/34] WIP rest API --- application/config/rest.php | 512 ++++ application/config/routes.php | 11 +- application/controllers/Rest_server.php | 13 + application/controllers/api/Example.php | 137 ++ application/controllers/api/Key.php | 272 +++ application/controllers/api/index.html | 11 + application/language/english/index.html | 0 .../language/english/rest_controller_lang.php | 17 + application/libraries/Format.php | 531 ++++ application/libraries/REST_Controller.php | 2151 +++++++++++++++++ .../tests/controllers/api/Example_test.php | 7 + application/views/rest_server.php | 210 ++ 12 files changed, 3871 insertions(+), 1 deletion(-) create mode 100755 application/config/rest.php create mode 100755 application/controllers/Rest_server.php create mode 100755 application/controllers/api/Example.php create mode 100755 application/controllers/api/Key.php create mode 100755 application/controllers/api/index.html mode change 100644 => 100755 application/language/english/index.html create mode 100755 application/language/english/rest_controller_lang.php create mode 100644 application/libraries/Format.php create mode 100644 application/libraries/REST_Controller.php create mode 100644 application/tests/controllers/api/Example_test.php create mode 100755 application/views/rest_server.php diff --git a/application/config/rest.php b/application/config/rest.php new file mode 100755 index 00000000..15af4ed7 --- /dev/null +++ b/application/config/rest.php @@ -0,0 +1,512 @@ +function($username, $password) +| In other cases override the function _perform_library_auth in your controller +| +| For digest authentication the library function should return already a stored +| md5(username:restrealm:password) for that username +| +| e.g: md5('admin:REST API:1234') = '1e957ebc35631ab22d5bd6526bd14ea2' +| +*/ +$config['auth_library_class'] = ''; +$config['auth_library_function'] = ''; + +/* +|-------------------------------------------------------------------------- +| Override auth types for specific class/method +|-------------------------------------------------------------------------- +| +| Set specific authentication types for methods within a class (controller) +| +| Set as many config entries as needed. Any methods not set will use the default 'rest_auth' config value. +| +| e.g: +| +| $config['auth_override_class_method']['deals']['view'] = 'none'; +| $config['auth_override_class_method']['deals']['insert'] = 'digest'; +| $config['auth_override_class_method']['accounts']['user'] = 'basic'; +| $config['auth_override_class_method']['dashboard']['*'] = 'none|digest|basic'; +| +| Here 'deals', 'accounts' and 'dashboard' are controller names, 'view', 'insert' and 'user' are methods within. An asterisk may also be used to specify an authentication method for an entire classes methods. Ex: $config['auth_override_class_method']['dashboard']['*'] = 'basic'; (NOTE: leave off the '_get' or '_post' from the end of the method name) +| Acceptable values are; 'none', 'digest' and 'basic'. +| +*/ +// $config['auth_override_class_method']['deals']['view'] = 'none'; +// $config['auth_override_class_method']['deals']['insert'] = 'digest'; +// $config['auth_override_class_method']['accounts']['user'] = 'basic'; +// $config['auth_override_class_method']['dashboard']['*'] = 'basic'; + + +// ---Uncomment list line for the wildard unit test +// $config['auth_override_class_method']['wildcard_test_cases']['*'] = 'basic'; + +/* +|-------------------------------------------------------------------------- +| Override auth types for specfic 'class/method/HTTP method' +|-------------------------------------------------------------------------- +| +| example: +| +| $config['auth_override_class_method_http']['deals']['view']['get'] = 'none'; +| $config['auth_override_class_method_http']['deals']['insert']['post'] = 'none'; +| $config['auth_override_class_method_http']['deals']['*']['options'] = 'none'; +*/ + +// ---Uncomment list line for the wildard unit test +// $config['auth_override_class_method_http']['wildcard_test_cases']['*']['options'] = 'basic'; + +/* +|-------------------------------------------------------------------------- +| REST Login Usernames +|-------------------------------------------------------------------------- +| +| Array of usernames and passwords for login, if ldap is configured this is ignored +| +*/ +$config['rest_valid_logins'] = ['admin' => '1234']; + +/* +|-------------------------------------------------------------------------- +| Global IP Whitelisting +|-------------------------------------------------------------------------- +| +| Limit connections to your REST server to whitelisted IP addresses +| +| Usage: +| 1. Set to TRUE and select an auth option for extreme security (client's IP +| address must be in whitelist and they must also log in) +| 2. Set to TRUE with auth set to FALSE to allow whitelisted IPs access with no login +| 3. Set to FALSE but set 'auth_override_class_method' to 'whitelist' to +| restrict certain methods to IPs in your whitelist +| +*/ +$config['rest_ip_whitelist_enabled'] = FALSE; + +/* +|-------------------------------------------------------------------------- +| REST IP Whitelist +|-------------------------------------------------------------------------- +| +| Limit connections to your REST server with a comma separated +| list of IP addresses +| +| e.g: '123.456.789.0, 987.654.32.1' +| +| 127.0.0.1 and 0.0.0.0 are allowed by default +| +*/ +$config['rest_ip_whitelist'] = ''; + +/* +|-------------------------------------------------------------------------- +| Global IP Blacklisting +|-------------------------------------------------------------------------- +| +| Prevent connections to the REST server from blacklisted IP addresses +| +| Usage: +| 1. Set to TRUE and add any IP address to 'rest_ip_blacklist' +| +*/ +$config['rest_ip_blacklist_enabled'] = FALSE; + +/* +|-------------------------------------------------------------------------- +| REST IP Blacklist +|-------------------------------------------------------------------------- +| +| Prevent connections from the following IP addresses +| +| e.g: '123.456.789.0, 987.654.32.1' +| +*/ +$config['rest_ip_blacklist'] = ''; + +/* +|-------------------------------------------------------------------------- +| REST Database Group +|-------------------------------------------------------------------------- +| +| Connect to a database group for keys, logging, etc. It will only connect +| if you have any of these features enabled +| +*/ +$config['rest_database_group'] = 'default'; + +/* +|-------------------------------------------------------------------------- +| REST API Keys Table Name +|-------------------------------------------------------------------------- +| +| The table name in your database that stores API keys +| +*/ +$config['rest_keys_table'] = 'keys'; + +/* +|-------------------------------------------------------------------------- +| REST Enable Keys +|-------------------------------------------------------------------------- +| +| When set to TRUE, the REST API will look for a column name called 'key'. +| If no key is provided, the request will result in an error. To override the +| column name see 'rest_key_column' +| +| Default table schema: +| CREATE TABLE `keys` ( +| `id` INT(11) NOT NULL AUTO_INCREMENT, +| `key` VARCHAR(40) NOT NULL, +| `level` INT(2) NOT NULL, +| `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, +| PRIMARY KEY (`id`) +| ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +| +*/ +$config['rest_enable_keys'] = FALSE; + +/* +|-------------------------------------------------------------------------- +| REST Table Key Column Name +|-------------------------------------------------------------------------- +| +| If not using the default table schema in 'rest_enable_keys', specify the +| column name to match e.g. my_key +| +*/ +$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 +|-------------------------------------------------------------------------- +| +| Length of the created keys. Check your default database schema on the +| maximum length allowed +| +| Note: The maximum length is 40 +| +*/ +$config['rest_key_length'] = 40; + +/* +|-------------------------------------------------------------------------- +| REST API Key Variable +|-------------------------------------------------------------------------- +| +| Custom header to specify the API key + +| Note: Custom headers with the X- prefix are deprecated as of +| 2012/06/12. See RFC 6648 specification for more details +| +*/ +$config['rest_key_name'] = 'X-API-KEY'; + +/* +|-------------------------------------------------------------------------- +| REST Enable Logging +|-------------------------------------------------------------------------- +| +| When set to TRUE, the REST API will log actions based on the column names 'key', 'date', +| 'time' and 'ip_address'. This is a general rule that can be overridden in the +| $this->method array for each controller +| +| Default table schema: +| CREATE TABLE `logs` ( +| `id` INT(11) NOT NULL AUTO_INCREMENT, +| `uri` VARCHAR(255) NOT NULL, +| `method` VARCHAR(6) NOT NULL, +| `params` TEXT DEFAULT NULL, +| `api_key` VARCHAR(40) NOT NULL, +| `ip_address` VARCHAR(45) NOT NULL, +| `time` INT(11) NOT NULL, +| `rtime` FLOAT DEFAULT NULL, +| `authorized` VARCHAR(1) NOT NULL, +| `response_code` smallint(3) DEFAULT '0', +| PRIMARY KEY (`id`) +| ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +| +*/ +$config['rest_enable_logging'] = FALSE; + +/* +|-------------------------------------------------------------------------- +| REST API Logs Table Name +|-------------------------------------------------------------------------- +| +| If not using the default table schema in 'rest_enable_logging', specify the +| table name to match e.g. my_logs +| +*/ +$config['rest_logs_table'] = 'logs'; + +/* +|-------------------------------------------------------------------------- +| REST Method Access Control +|-------------------------------------------------------------------------- +| When set to TRUE, the REST API will check the access table to see if +| the API key can access that controller. 'rest_enable_keys' must be enabled +| to use this +| +| Default table schema: +| CREATE TABLE `access` ( +| `id` INT(11) unsigned NOT NULL AUTO_INCREMENT, +| `key` VARCHAR(40) NOT NULL DEFAULT '', +| `controller` VARCHAR(50) NOT NULL DEFAULT '', +| `date_created` DATETIME DEFAULT NULL, +| `date_modified` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, +| PRIMARY KEY (`id`) +| ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +| +*/ +$config['rest_enable_access'] = FALSE; + +/* +|-------------------------------------------------------------------------- +| REST API Access Table Name +|-------------------------------------------------------------------------- +| +| If not using the default table schema in 'rest_enable_access', specify the +| table name to match e.g. my_access +| +*/ +$config['rest_access_table'] = 'access'; + +/* +|-------------------------------------------------------------------------- +| REST API Param Log Format +|-------------------------------------------------------------------------- +| +| When set to TRUE, the REST API log parameters will be stored in the database as JSON +| Set to FALSE to log as serialized PHP +| +*/ +$config['rest_logs_json_params'] = FALSE; + +/* +|-------------------------------------------------------------------------- +| REST Enable Limits +|-------------------------------------------------------------------------- +| +| When set to TRUE, the REST API will count the number of uses of each method +| by an API key each hour. This is a general rule that can be overridden in the +| $this->method array in each controller +| +| Default table schema: +| CREATE TABLE `limits` ( +| `id` INT(11) NOT NULL AUTO_INCREMENT, +| `uri` VARCHAR(255) NOT NULL, +| `count` INT(10) NOT NULL, +| `hour_started` INT(11) NOT NULL, +| `api_key` VARCHAR(40) NOT NULL, +| PRIMARY KEY (`id`) +| ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +| +| To specify the limits within the controller's __construct() method, add per-method +| limits with: +| +| $this->method['METHOD_NAME']['limit'] = [NUM_REQUESTS_PER_HOUR]; +| +| See application/controllers/api/example.php for examples +*/ +$config['rest_enable_limits'] = FALSE; + +/* +|-------------------------------------------------------------------------- +| REST API Limits Table Name +|-------------------------------------------------------------------------- +| +| If not using the default table schema in 'rest_enable_limits', specify the +| table name to match e.g. my_limits +| +*/ +$config['rest_limits_table'] = 'limits'; + +/* +|-------------------------------------------------------------------------- +| REST Ignore HTTP Accept +|-------------------------------------------------------------------------- +| +| Set to TRUE to ignore the HTTP Accept and speed up each request a little. +| Only do this if you are using the $this->rest_format or /format/xml in URLs +| +*/ +$config['rest_ignore_http_accept'] = FALSE; + +/* +|-------------------------------------------------------------------------- +| REST AJAX Only +|-------------------------------------------------------------------------- +| +| Set to TRUE to allow AJAX requests only. Set to FALSE to accept HTTP requests +| +| Note: If set to TRUE and the request is not AJAX, a 505 response with the +| error message 'Only AJAX requests are accepted.' will be returned. +| +| Hint: This is good for production environments +| +*/ +$config['rest_ajax_only'] = FALSE; + +/* +|-------------------------------------------------------------------------- +| REST Language File +|-------------------------------------------------------------------------- +| +| Language file to load from the language directory +| +*/ +$config['rest_language'] = 'english'; diff --git a/application/config/routes.php b/application/config/routes.php index 9e9acdb3..c36434c6 100644 --- a/application/config/routes.php +++ b/application/config/routes.php @@ -64,4 +64,13 @@ $route['result'] = "home/result"; $route['about'] = "home/about"; $route['help'] = "home/help"; -$route['contact'] = "home/contact"; \ No newline at end of file +$route['contact'] = "home/contact"; + +/* +| ------------------------------------------------------------------------- +| REST API Routes +| ------------------------------------------------------------------------- +*/ + +$route['apiz/(:num)'] = 'api/example/users/id/$1'; // Example 4 +$route['api/example/users/(:num)(\.)([a-zA-Z0-9_-]+)(.*)'] = 'api/example/users/id/$1/format/$3$4'; // Example 8 diff --git a/application/controllers/Rest_server.php b/application/controllers/Rest_server.php new file mode 100755 index 00000000..5d44f921 --- /dev/null +++ b/application/controllers/Rest_server.php @@ -0,0 +1,13 @@ +load->helper('url'); + + $this->load->view('rest_server'); + } +} diff --git a/application/controllers/api/Example.php b/application/controllers/api/Example.php new file mode 100755 index 00000000..2cfecaaf --- /dev/null +++ b/application/controllers/api/Example.php @@ -0,0 +1,137 @@ +methods['user_get']['limit'] = 500; // 500 requests per hour per user/key + $this->methods['user_post']['limit'] = 100; // 100 requests per hour per user/key + $this->methods['user_delete']['limit'] = 50; // 50 requests per hour per user/key + } + + public function users_get() + { + // Users from a data store e.g. database + $users = [ + ['id' => 1, 'name' => 'John', 'email' => 'john@example.com', 'fact' => 'Loves coding'], + ['id' => 2, 'name' => 'Jim', 'email' => 'jim@example.com', 'fact' => 'Developed on CodeIgniter'], + ['id' => 3, 'name' => 'Jane', 'email' => 'jane@example.com', 'fact' => 'Lives in the USA', ['hobbies' => ['guitar', 'cycling']]], + ]; + + $id = $this->get('id'); + + // If the id parameter doesn't exist return all the users + + if ($id === NULL) + { + // Check if the users data store contains users (in case the database result returns NULL) + if ($users) + { + // Set the response and exit + $this->response($users, REST_Controller::HTTP_OK); // OK (200) being the HTTP response code + } + else + { + // Set the response and exit + $this->response([ + 'status' => FALSE, + 'message' => 'No users were found' + ], REST_Controller::HTTP_NOT_FOUND); // NOT_FOUND (404) being the HTTP response code + } + } + + // Find and return a single record for a particular user. + + $id = (int) $id; + + // Validate the id. + if ($id <= 0) + { + // Invalid id, set the response and exit. + $this->response(NULL, REST_Controller::HTTP_BAD_REQUEST); // BAD_REQUEST (400) being the HTTP response code + } + + // Get the user from the array, using the id as key for retreival. + // Usually a model is to be used for this. + + $user = NULL; + + if (!empty($users)) + { + foreach ($users as $key => $value) + { + if (isset($value['id']) && $value['id'] === $id) + { + $user = $value; + } + } + } + + if (!empty($user)) + { + $this->set_response($user, REST_Controller::HTTP_OK); // OK (200) being the HTTP response code + } + else + { + $this->set_response([ + 'status' => FALSE, + 'message' => 'User could not be found' + ], REST_Controller::HTTP_NOT_FOUND); // NOT_FOUND (404) being the HTTP response code + } + } + + public function users_post() + { + // $this->some_model->update_user( ... ); + $message = [ + 'id' => 100, // Automatically generated by the model + 'name' => $this->post('name'), + 'email' => $this->post('email'), + 'message' => 'Added a resource' + ]; + + $this->set_response($message, REST_Controller::HTTP_CREATED); // CREATED (201) being the HTTP response code + } + + public function users_delete() + { + $id = (int) $this->get('id'); + + // Validate the 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' => $id, + 'message' => 'Deleted the resource' + ]; + + $this->set_response($message, REST_Controller::HTTP_NO_CONTENT); // NO_CONTENT (204) being the HTTP response code + } + +} diff --git a/application/controllers/api/Key.php b/application/controllers/api/Key.php new file mode 100755 index 00000000..0fa67172 --- /dev/null +++ b/application/controllers/api/Key.php @@ -0,0 +1,272 @@ + ['level' => 10, 'limit' => 10], + 'index_delete' => ['level' => 10], + 'level_post' => ['level' => 10], + 'regenerate_post' => ['level' => 10], + ]; + + /** + * Insert a key into the database + * + * @access public + * @return void + */ + public function index_put() + { + // Build a new key + $key = $this->_generate_key(); + + // If no key level provided, provide a generic key + $level = $this->put('level') ? $this->put('level') : 1; + $ignore_limits = ctype_digit($this->put('ignore_limits')) ? (int) $this->put('ignore_limits') : 1; + + // Insert the new key + if ($this->_insert_key($key, ['level' => $level, 'ignore_limits' => $ignore_limits])) + { + $this->response([ + 'status' => TRUE, + 'key' => $key + ], REST_Controller::HTTP_CREATED); // CREATED (201) being the HTTP response code + } + else + { + $this->response([ + 'status' => FALSE, + 'message' => 'Could not save the key' + ], REST_Controller::HTTP_INTERNAL_SERVER_ERROR); // INTERNAL_SERVER_ERROR (500) being the HTTP response code + } + } + + /** + * Remove a key from the database to stop it working + * + * @access public + * @return void + */ + public function index_delete() + { + $key = $this->delete('key'); + + // Does this key exist? + if (!$this->_key_exists($key)) + { + // It doesn't appear the key exists + $this->response([ + 'status' => FALSE, + 'message' => 'Invalid API key' + ], REST_Controller::HTTP_BAD_REQUEST); // BAD_REQUEST (400) being the HTTP response code + } + + // Destroy it + $this->_delete_key($key); + + // Respond that the key was destroyed + $this->response([ + 'status' => TRUE, + 'message' => 'API key was deleted' + ], REST_Controller::HTTP_NO_CONTENT); // NO_CONTENT (204) being the HTTP response code + } + + /** + * Change the level + * + * @access public + * @return void + */ + public function level_post() + { + $key = $this->post('key'); + $new_level = $this->post('level'); + + // Does this key exist? + if (!$this->_key_exists($key)) + { + // It doesn't appear the key exists + $this->response([ + 'status' => FALSE, + 'message' => 'Invalid API key' + ], REST_Controller::HTTP_BAD_REQUEST); // BAD_REQUEST (400) being the HTTP response code + } + + // Update the key level + if ($this->_update_key($key, ['level' => $new_level])) + { + $this->response([ + 'status' => TRUE, + 'message' => 'API key was updated' + ], REST_Controller::HTTP_OK); // OK (200) being the HTTP response code + } + else + { + $this->response([ + 'status' => FALSE, + 'message' => 'Could not update the key level' + ], REST_Controller::HTTP_INTERNAL_SERVER_ERROR); // INTERNAL_SERVER_ERROR (500) being the HTTP response code + } + } + + /** + * Suspend a key + * + * @access public + * @return void + */ + public function suspend_post() + { + $key = $this->post('key'); + + // Does this key exist? + if (!$this->_key_exists($key)) + { + // It doesn't appear the key exists + $this->response([ + 'status' => FALSE, + 'message' => 'Invalid API key' + ], REST_Controller::HTTP_BAD_REQUEST); // BAD_REQUEST (400) being the HTTP response code + } + + // Update the key level + if ($this->_update_key($key, ['level' => 0])) + { + $this->response([ + 'status' => TRUE, + 'message' => 'Key was suspended' + ], REST_Controller::HTTP_OK); // OK (200) being the HTTP response code + } + else + { + $this->response([ + 'status' => FALSE, + 'message' => 'Could not suspend the user' + ], REST_Controller::HTTP_INTERNAL_SERVER_ERROR); // INTERNAL_SERVER_ERROR (500) being the HTTP response code + } + } + + /** + * Regenerate a key + * + * @access public + * @return void + */ + public function regenerate_post() + { + $old_key = $this->post('key'); + $key_details = $this->_get_key($old_key); + + // Does this key exist? + if (!$key_details) + { + // It doesn't appear the key exists + $this->response([ + 'status' => FALSE, + 'message' => 'Invalid API key' + ], REST_Controller::HTTP_BAD_REQUEST); // BAD_REQUEST (400) being the HTTP response code + } + + // Build a new key + $new_key = $this->_generate_key(); + + // Insert the new key + if ($this->_insert_key($new_key, ['level' => $key_details->level, 'ignore_limits' => $key_details->ignore_limits])) + { + // Suspend old key + $this->_update_key($old_key, ['level' => 0]); + + $this->response([ + 'status' => TRUE, + 'key' => $new_key + ], REST_Controller::HTTP_CREATED); // CREATED (201) being the HTTP response code + } + else + { + $this->response([ + 'status' => FALSE, + 'message' => 'Could not save the key' + ], REST_Controller::HTTP_INTERNAL_SERVER_ERROR); // INTERNAL_SERVER_ERROR (500) being the HTTP response code + } + } + + /* Helper Methods */ + + private function _generate_key() + { + do + { + // Generate a random salt + $salt = base_convert(bin2hex($this->security->get_random_bytes(64)), 16, 36); + + // If an error occurred, then fall back to the previous method + if ($salt === FALSE) + { + $salt = hash('sha256', time() . mt_rand()); + } + + $new_key = substr($salt, 0, config_item('rest_key_length')); + } + while ($this->_key_exists($new_key)); + + return $new_key; + } + + /* Private Data Methods */ + + private function _get_key($key) + { + return $this->db + ->where(config_item('rest_key_column'), $key) + ->get(config_item('rest_keys_table')) + ->row(); + } + + private function _key_exists($key) + { + return $this->db + ->where(config_item('rest_key_column'), $key) + ->count_all_results(config_item('rest_keys_table')) > 0; + } + + private function _insert_key($key, $data) + { + $data[config_item('rest_key_column')] = $key; + $data['date_created'] = function_exists('now') ? now() : time(); + + return $this->db + ->set($data) + ->insert(config_item('rest_keys_table')); + } + + private function _update_key($key, $data) + { + return $this->db + ->where(config_item('rest_key_column'), $key) + ->update(config_item('rest_keys_table'), $data); + } + + private function _delete_key($key) + { + return $this->db + ->where(config_item('rest_key_column'), $key) + ->delete(config_item('rest_keys_table')); + } + +} diff --git a/application/controllers/api/index.html b/application/controllers/api/index.html new file mode 100755 index 00000000..b702fbc3 --- /dev/null +++ b/application/controllers/api/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/application/language/english/index.html b/application/language/english/index.html old mode 100644 new mode 100755 diff --git a/application/language/english/rest_controller_lang.php b/application/language/english/rest_controller_lang.php new file mode 100755 index 00000000..1c665bdc --- /dev/null +++ b/application/language/english/rest_controller_lang.php @@ -0,0 +1,17 @@ +_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 00000000..c528d5e5 --- /dev/null +++ b/application/libraries/REST_Controller.php @@ -0,0 +1,2151 @@ + '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(); + } + + $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; + + // 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/tests/controllers/api/Example_test.php b/application/tests/controllers/api/Example_test.php new file mode 100644 index 00000000..0609d94e --- /dev/null +++ b/application/tests/controllers/api/Example_test.php @@ -0,0 +1,7 @@ + + + + + + + REST Server Tests + + + + + +
+

REST Server Tests

+ +
+ +

Home

+ +

+ See the article + + http://net.tutsplus.com/tutorials/php/working-with-restful-services-in-codeigniter-2/ + +

+ +

+ The master project repository is + + https://github.com/chriskacerguis/codeigniter-restserver + +

+ +

+ Click on the links to check whether the REST server is working. +

+ +
    +
  1. Users - defaulting to JSON
  2. +
  3. Users - get it in CSV
  4. +
  5. User #1 - defaulting to JSON (users/id/1)
  6. +
  7. User #1 - defaulting to JSON (users/1)
  8. +
  9. User #1 - get it in XML (users/id/1.xml)
  10. +
  11. User #1 - get it in XML (users/id/1/format/xml)
  12. +
  13. User #1 - get it in XML (users/id/1?format=xml)
  14. +
  15. User #1 - get it in XML (users/1.xml)
  16. +
  17. Users - get it in JSON (AJAX request)
  18. +
  19. Users - get it in HTML (users.html)
  20. +
  21. Users - get it in HTML (users/format/html)
  22. +
  23. Users - get it in HTML (users?format=html)
  24. +
+ +
+ + +
+ + + + + + + From 3d69cf188cceaa6c69578ad5d53434460a712fd4 Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Wed, 13 Jan 2016 15:27:22 -0500 Subject: [PATCH 02/34] [WIP] rest-api --- application/config/database.php | 10 +- application/config/routes.php | 2 +- application/controllers/api/Example.php | 137 ------------------ application/controllers/api/User.php | 100 +++++++++++++ application/models/Key.php | 59 ++++++++ .../tests/controllers/api/Example_test.php | 7 - .../tests/controllers/api/User_test.php | 47 ++++++ 7 files changed, 211 insertions(+), 151 deletions(-) delete mode 100755 application/controllers/api/Example.php create mode 100755 application/controllers/api/User.php create mode 100644 application/models/Key.php delete mode 100644 application/tests/controllers/api/Example_test.php create mode 100644 application/tests/controllers/api/User_test.php diff --git a/application/config/database.php b/application/config/database.php index 34585fd5..a30bfdd4 100644 --- a/application/config/database.php +++ b/application/config/database.php @@ -85,9 +85,7 @@ ); -$url = parse_url(getenv("TW_DB_URL")); -$db['default']['hostname'] = $url["host"]; -$db['default']['username'] = $url["user"]; -$db['default']['password'] = $url["pass"]; -$db['default']['database'] = substr($url["path"], 1); - +$db['default']['hostname'] = "localhost"; +$db['default']['username'] = "root"; +$db['default']['password'] = ""; +$db['default']['database'] = "tw_ci"; diff --git a/application/config/routes.php b/application/config/routes.php index c36434c6..75685942 100644 --- a/application/config/routes.php +++ b/application/config/routes.php @@ -72,5 +72,5 @@ | ------------------------------------------------------------------------- */ -$route['apiz/(:num)'] = 'api/example/users/id/$1'; // Example 4 +$route['api/example/users/(:num)'] = 'api/user/create/$1'; // Example 4 $route['api/example/users/(:num)(\.)([a-zA-Z0-9_-]+)(.*)'] = 'api/example/users/id/$1/format/$3$4'; // Example 8 diff --git a/application/controllers/api/Example.php b/application/controllers/api/Example.php deleted file mode 100755 index 2cfecaaf..00000000 --- a/application/controllers/api/Example.php +++ /dev/null @@ -1,137 +0,0 @@ -methods['user_get']['limit'] = 500; // 500 requests per hour per user/key - $this->methods['user_post']['limit'] = 100; // 100 requests per hour per user/key - $this->methods['user_delete']['limit'] = 50; // 50 requests per hour per user/key - } - - public function users_get() - { - // Users from a data store e.g. database - $users = [ - ['id' => 1, 'name' => 'John', 'email' => 'john@example.com', 'fact' => 'Loves coding'], - ['id' => 2, 'name' => 'Jim', 'email' => 'jim@example.com', 'fact' => 'Developed on CodeIgniter'], - ['id' => 3, 'name' => 'Jane', 'email' => 'jane@example.com', 'fact' => 'Lives in the USA', ['hobbies' => ['guitar', 'cycling']]], - ]; - - $id = $this->get('id'); - - // If the id parameter doesn't exist return all the users - - if ($id === NULL) - { - // Check if the users data store contains users (in case the database result returns NULL) - if ($users) - { - // Set the response and exit - $this->response($users, REST_Controller::HTTP_OK); // OK (200) being the HTTP response code - } - else - { - // Set the response and exit - $this->response([ - 'status' => FALSE, - 'message' => 'No users were found' - ], REST_Controller::HTTP_NOT_FOUND); // NOT_FOUND (404) being the HTTP response code - } - } - - // Find and return a single record for a particular user. - - $id = (int) $id; - - // Validate the id. - if ($id <= 0) - { - // Invalid id, set the response and exit. - $this->response(NULL, REST_Controller::HTTP_BAD_REQUEST); // BAD_REQUEST (400) being the HTTP response code - } - - // Get the user from the array, using the id as key for retreival. - // Usually a model is to be used for this. - - $user = NULL; - - if (!empty($users)) - { - foreach ($users as $key => $value) - { - if (isset($value['id']) && $value['id'] === $id) - { - $user = $value; - } - } - } - - if (!empty($user)) - { - $this->set_response($user, REST_Controller::HTTP_OK); // OK (200) being the HTTP response code - } - else - { - $this->set_response([ - 'status' => FALSE, - 'message' => 'User could not be found' - ], REST_Controller::HTTP_NOT_FOUND); // NOT_FOUND (404) being the HTTP response code - } - } - - public function users_post() - { - // $this->some_model->update_user( ... ); - $message = [ - 'id' => 100, // Automatically generated by the model - 'name' => $this->post('name'), - 'email' => $this->post('email'), - 'message' => 'Added a resource' - ]; - - $this->set_response($message, REST_Controller::HTTP_CREATED); // CREATED (201) being the HTTP response code - } - - public function users_delete() - { - $id = (int) $this->get('id'); - - // Validate the 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' => $id, - 'message' => 'Deleted the resource' - ]; - - $this->set_response($message, REST_Controller::HTTP_NO_CONTENT); // NO_CONTENT (204) being the HTTP response code - } - -} diff --git a/application/controllers/api/User.php b/application/controllers/api/User.php new file mode 100755 index 00000000..83badf78 --- /dev/null +++ b/application/controllers/api/User.php @@ -0,0 +1,100 @@ +load->model("Key"); + } + + public function auth_put() + { + $email = $this->put('email'); + $password = $this->put('password'); + $this->loginAndAuth($email, $password); + } + + private function loginAndAuth($email, $password){ + + if($email !== NULL && $password !== NULL){ + + $user = $this->user->login($email, $password); + + if($user !== false){ + $key = $this->key->generate_key($user); + + if($key !== false){ + + $user["key"] = $key; + $this->response($user, REST_Controller::HTTP_OK); + }else{ + $this->response(NULL, REST_Controller::HTTP_INTERNAL_SERVER_ERROR); + } + }else{ + $this->response(NULL, REST_Controller::HTTP_UNAUTHORIZED); + } + }else{ + $this->response(NULL, REST_Controller::HTTP_BAD_REQUEST); + } + + } + + public function create_post() + { + echo "qlkzdqdn"; + $email = $this->post('email'); + $password = $this->post('password'); + $name = $this->post('name'); + $firstname = $this->post('firstname'); + $timezone = $this->post('timezone'); + $country = $this->post('country'); + + //If the email isn't already in used + if (!$this->user->checkUserEmail($email)) { + + // Create the account + if ($this->user->signup( + $email, $password, $name, $firstname, + $timezone, $country)) { + + $this->loginAndAuth($email, $password); + + } else { + + $this->response(NULL, REST_Controller::HTTP_INTERNAL_SERVER_ERROR); + } + //The email is already in use + } else { + $this->response(["message" => "email"], + REST_Controller::HTTP_UNAUTHORIZED); + } + } + + public function users_delete() + { + $id = (int) $this->get('id'); + + // Validate the 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' => $id, + 'message' => 'Deleted the resource' + ]; + + $this->set_response($message, REST_Controller::HTTP_NO_CONTENT); // NO_CONTENT (204) being the HTTP response code + } + +} diff --git a/application/models/Key.php b/application/models/Key.php new file mode 100644 index 00000000..0f1dc20d --- /dev/null +++ b/application/models/Key.php @@ -0,0 +1,59 @@ +table_name = "keys"; + $this->load->config("rest"); + } + + public function has_valide_key($userId, $key){ + return $this->select("count(*)") + ->where("user_id", $userId) + ->find_by("key", $key) > 0; + } + + public function generate_key($user) + { + do + { + // Generate a random salt + $salt = base_convert(bin2hex($this->security->get_random_bytes(64)), 16, 36); + + // If an error occurred, then fall back to the previous method + if ($salt === FALSE) + { + $salt = hash('sha256', time() . mt_rand()); + } + + $new_key = substr($salt, 0, config_item('rest_key_length')); + } + while ($this->key_exists($key)); + + $data[config_item('rest_key_column')] = $key; + $data['date_created'] = function_exists('now') ? now() : time(); + $data['user_id'] = $user->userId; + + if(($this->update_where("user_id", $user->userId, $data) === true + && $this->affected_rows() === 1) + || + ($this->insert($data) !== false + && $this->affected_rows() === 1)){ + + return $key; + } + + return false; + } + + private function key_exists($key) + { + return $this->count_by(config_item('rest_key_column'), $key) > 0; + } + +} diff --git a/application/tests/controllers/api/Example_test.php b/application/tests/controllers/api/Example_test.php deleted file mode 100644 index 0609d94e..00000000 --- a/application/tests/controllers/api/Example_test.php +++ /dev/null @@ -1,7 +0,0 @@ -emailWatch = new MY_Model('email_watch'); + $CI->emailMeasure = new MY_Model('email_measure'); + $CI->emailUser = new MY_Model('email_user'); + + $CI->emailUser->delete_where(array("id >=" => "0")); + $CI->emailWatch->delete_where(array("id >=" => "0")); + $CI->emailMeasure->delete_where(array("id >=" => "0")); + + $CI->load->model('User'); + $CI->load->model('Measure'); + $CI->load->model('Watch'); + $CI->load->model('Key'); + $CI->User->delete_where(array("userId >=" => "0")); + $CI->Measure->delete_where(array("id >=" => "0")); + $CI->Watch->delete_where(array("watchId >=" => "0")); + $CI->Key->delete_where(array("id >=" => "0")); + } + + public function test_create() { + $output = $this->request( + 'POST', + ['api/user', 'create'], + [ + 'email' => 'mathieu@gmail.com', + 'password' => 'password', + 'name' => 'name', + 'firstname' => 'firstname', + 'timezone' => 'timezone', + 'country' => 'country', + 'mailingList' => 'false' + ] + ); + + var_dump($output); + + $this->assertContains('{"success":true}', $output); + } + + + +} From 147d76a066cadd2adec0d208973e95084a9e74e0 Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Sun, 7 Feb 2016 17:16:48 -0500 Subject: [PATCH 03/34] Add rest capabilities #101 --- application/config/rest.php | 2 +- application/config/routes.php | 7 +- application/libraries/REST_Controller.php | 89 +++++++++++++++-------- application/models/Key.php | 38 +++++----- application/tests/phpunit.xml | 4 +- 5 files changed, 86 insertions(+), 54 deletions(-) diff --git a/application/config/rest.php b/application/config/rest.php index 15af4ed7..821e3f1a 100755 --- a/application/config/rest.php +++ b/application/config/rest.php @@ -301,7 +301,7 @@ | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; | */ -$config['rest_enable_keys'] = FALSE; +$config['rest_enable_keys'] = TRUE; /* |-------------------------------------------------------------------------- diff --git a/application/config/routes.php b/application/config/routes.php index 75685942..5dd72452 100644 --- a/application/config/routes.php +++ b/application/config/routes.php @@ -72,5 +72,8 @@ | ------------------------------------------------------------------------- */ -$route['api/example/users/(:num)'] = 'api/user/create/$1'; // Example 4 -$route['api/example/users/(:num)(\.)([a-zA-Z0-9_-]+)(.*)'] = 'api/example/users/id/$1/format/$3$4'; // Example 8 +$route['api/(:any)'] = 'api/$1_api'; + +// +// $route['api/example/users/(:num)'] = 'api/user/create/$1'; // Example 4 +// $route['api/example/users/(:num)(\.)([a-zA-Z0-9_-]+)(.*)'] = 'api/example/users/id/$1/format/$3$4'; // Example 8 diff --git a/application/libraries/REST_Controller.php b/application/libraries/REST_Controller.php index c528d5e5..2624a9b0 100644 --- a/application/libraries/REST_Controller.php +++ b/application/libraries/REST_Controller.php @@ -624,6 +624,8 @@ public function _remap($object_called, $arguments) $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); + + return; } // Check to see if this key has access to the requested controller @@ -638,6 +640,8 @@ public function _remap($object_called, $arguments) $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); + + return; } // Sure it exists, but can they do anything with it? @@ -657,6 +661,7 @@ public function _remap($object_called, $arguments) { $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); + return; } // If no level is set use 0, they probably aren't using permissions @@ -672,8 +677,11 @@ 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->lang->line('text_rest_api_key_permissions')]; - $authorized || $this->response($response, self::HTTP_UNAUTHORIZED); + if($authorized == false){ + $response = [$this->config->item('rest_status_field_name') => FALSE, $this->config->item('rest_message_field_name') => $this->lang->line('text_rest_api_key_permissions')]; + $this->response($response, self::HTTP_UNAUTHORIZED); + return; + } } // No key stuff, but record that stuff is happening @@ -709,7 +717,7 @@ public function _remap($object_called, $arguments) * @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) + public function response($data = NULL, $http_code = NULL) { // If the HTTP status is not NULL, then cast as an integer if ($http_code !== NULL) @@ -770,17 +778,7 @@ public function response($data = NULL, $http_code = NULL, $continue = FALSE) $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 + $this->output->_display($output); } /** @@ -963,6 +961,14 @@ protected function _detect_api_key() // Work out the name of the SERVER entry based on config $key_name = 'HTTP_' . strtoupper(str_replace('-', '_', $api_key_variable)); + // echo PHP_EOL.PHP_EOL.'=========API KEY========='.PHP_EOL; + // echo "here".PHP_EOL.$key_name.PHP_EOL; + // echo "args".PHP_EOL; + // var_dump($this->_args); + // echo '$this->input->server('.$key_name.')'.PHP_EOL; + // var_dump($this->input->server($key_name)); + // echo PHP_EOL.'========================'.PHP_EOL.PHP_EOL; + $this->rest->key = NULL; $this->rest->level = NULL; $this->rest->user_id = NULL; @@ -980,6 +986,7 @@ protected function _detect_api_key() isset($row->user_id) && $this->rest->user_id = $row->user_id; isset($row->level) && $this->rest->level = $row->level; + isset($row->id) && $this->rest->key_id = $row->id; isset($row->ignore_limits) && $this->rest->ignore_limits = $row->ignore_limits; $this->_apiuser = $row; @@ -1067,22 +1074,33 @@ protected function _detect_lang() 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; + // $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(); + + log_message('INFO', print_r([ + '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 + ], true)); + + //return $is_inserted; + return 1; } /** @@ -1424,6 +1442,12 @@ protected function _parse_put() // If no filetype is provided, then there are probably just arguments $this->_put_args = $this->input->input_stream(); } + + + if(sizeof($this->_put_args) === 0){ + $this->_parse_post(); + $this->_put_args = $this->_post_args; + } } /** @@ -1489,6 +1513,11 @@ protected function _parse_delete() { $this->_delete_args = $this->input->input_stream(); } + + if(sizeof($this->_delete_args) === 0){ + $this->_parse_post(); + $this->_delete_args = $this->_post_args; + } } /** diff --git a/application/models/Key.php b/application/models/Key.php index 0f1dc20d..b04fdd99 100644 --- a/application/models/Key.php +++ b/application/models/Key.php @@ -1,7 +1,10 @@ load->config("rest"); } - public function has_valide_key($userId, $key){ - return $this->select("count(*)") - ->where("user_id", $userId) - ->find_by("key", $key) > 0; - } - + /** + * Generate a key for a given user + * @param User $user + * @return String generated key + */ public function generate_key($user) { do @@ -25,32 +27,28 @@ public function generate_key($user) // Generate a random salt $salt = base_convert(bin2hex($this->security->get_random_bytes(64)), 16, 36); - // If an error occurred, then fall back to the previous method - if ($salt === FALSE) - { - $salt = hash('sha256', time() . mt_rand()); - } - $new_key = substr($salt, 0, config_item('rest_key_length')); } - while ($this->key_exists($key)); + while ($this->key_exists($new_key)); - $data[config_item('rest_key_column')] = $key; + $data[config_item('rest_key_column')] = $new_key; $data['date_created'] = function_exists('now') ? now() : time(); $data['user_id'] = $user->userId; if(($this->update_where("user_id", $user->userId, $data) === true - && $this->affected_rows() === 1) - || + && $this->affected_rows() === 1) || ($this->insert($data) !== false && $this->affected_rows() === 1)){ - return $key; + return $new_key; } - - return false; } + /** + * Checks if a key already exists + * @param String $key + * @return boolean + */ private function key_exists($key) { return $this->count_by(config_item('rest_key_column'), $key) > 0; diff --git a/application/tests/phpunit.xml b/application/tests/phpunit.xml index 33864011..337fe608 100644 --- a/application/tests/phpunit.xml +++ b/application/tests/phpunit.xml @@ -22,7 +22,9 @@ ../libraries/__.php ../libraries/Mcapi.php - ../libraries/Mandrill.php + ../libraries/Mandrill.php + ../libraries/REST_Controller.php + ../libraries/Format.php ../libraries/Mandrill ../libraries/Google From df714bd2d619f5314a368612e11b82253c3badfc Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Mon, 8 Feb 2016 06:58:14 -0500 Subject: [PATCH 04/34] add test for key model #109 --- application/tests/models/Key_test.php | 45 +++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 application/tests/models/Key_test.php diff --git a/application/tests/models/Key_test.php b/application/tests/models/Key_test.php new file mode 100644 index 00000000..8d106f04 --- /dev/null +++ b/application/tests/models/Key_test.php @@ -0,0 +1,45 @@ +load->model('User'); + $CI->load->model('Watch'); + $CI->load->model('Key'); + $CI->load->library('Session'); + + $CI->User->signup( + 'mathieu@gmail.com', + 'azerty', + 'math', + 'nay', + '-5', + 'Canada' + ); + + $CI->User->login('mathieu@gmail.com', 'azerty'); + + self::$userId = $CI->session->userdata('userId'); + + $CI->Key->delete_where(array("key >=" => "0")); + } + + public function test_generateKey(){ + + $user = new stdClass(); + $user->userId = self::$userId; + + $key = new Key(); + + $generatedKey = $key->generate_key($user); + + $this->assertEquals(true, is_string($generatedKey)); + + } + + +} +?> From 751da08241cc994861bb8752061998de5ac9276b Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Mon, 8 Feb 2016 06:58:51 -0500 Subject: [PATCH 05/34] add support for PUT and DELETE request in ci_phpunit #109 --- .../_ci_phpunit_test/CIPHPUnitTestCase.php | 18 ++++---- .../_ci_phpunit_test/CIPHPUnitTestRequest.php | 46 ++++++++++++++----- 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/application/tests/_ci_phpunit_test/CIPHPUnitTestCase.php b/application/tests/_ci_phpunit_test/CIPHPUnitTestCase.php index c318ded2..3e9a2d90 100644 --- a/application/tests/_ci_phpunit_test/CIPHPUnitTestCase.php +++ b/application/tests/_ci_phpunit_test/CIPHPUnitTestCase.php @@ -11,12 +11,12 @@ class CIPHPUnitTestCase extends PHPUnit_Framework_TestCase { protected $_error_reporting = -1; - + /** * @var CIPHPUnitTestRequest */ protected $request; - + /** * @var CIPHPUnitTestDouble */ @@ -55,9 +55,9 @@ public static function setUpBeforeClass() * @param array $params POST parameters/Query string * @param callable $callable [deprecated] function to run after controller instantiation. Use $this->request->setCallable() method instead */ - public function request($http_method, $argv, $params = [], $callable = null) + public function request($http_method, $argv, $params = [], $callable = null, $header = []) { - return $this->request->request($http_method, $argv, $params, $callable); + return $this->request->request($http_method, $argv, $params, $callable, $header); } /** @@ -68,10 +68,10 @@ public function request($http_method, $argv, $params = [], $callable = null) * @param array $params POST parameters/Query string * @param callable $callable [deprecated] function to run after controller instantiation. Use $this->request->setCallable() method instead */ - public function ajaxRequest($http_method, $argv, $params = [], $callable = null) + public function ajaxRequest($http_method, $argv, $params = [], $callable = null, $header = []) { $_SERVER['HTTP_X_REQUESTED_WITH'] = 'xmlhttprequest'; - return $this->request($http_method, $argv, $params, $callable); + return $this->request($http_method, $argv, $params, $callable, $header); } /** @@ -177,7 +177,7 @@ public function warningOn() /** * Asserts HTTP response code - * + * * @param int $code */ public function assertResponseCode($code) @@ -194,9 +194,9 @@ public function assertResponseCode($code) /** * Set Expected Redirect - * + * * This method needs . - * + * * @param string $uri URI to redirect * @param int $code Response Code */ diff --git a/application/tests/_ci_phpunit_test/CIPHPUnitTestRequest.php b/application/tests/_ci_phpunit_test/CIPHPUnitTestRequest.php index f747cee3..5066af04 100644 --- a/application/tests/_ci_phpunit_test/CIPHPUnitTestRequest.php +++ b/application/tests/_ci_phpunit_test/CIPHPUnitTestRequest.php @@ -14,7 +14,7 @@ class CIPHPUnitTestRequest * @var callable callable post controller constructor */ protected $callable; - + /** * @var callable callable pre controller constructor */ @@ -22,19 +22,19 @@ class CIPHPUnitTestRequest protected $enableHooks = false; protected $CI; - + /** * @var bool whether throwing PHPUnit_Framework_Exception or not - * + * * If true, throws PHPUnit_Framework_Exception when show_404() and show_error() are called. This behavior is compatible to v0.3.0 and before. - * + * * @deprecated */ protected $bc_mode_throw_PHPUnit_Framework_Exception = false; /** * Set callable - * + * * @param callable $callable function to run after controller instantiation */ public function setCallable(callable $callable) @@ -44,7 +44,7 @@ public function setCallable(callable $callable) /** * Set callable pre constructor - * + * * @param callable $callable function to run before controller instantiation */ public function setCallablePreConstructor(callable $callable) @@ -69,13 +69,32 @@ public function enableHooks() * @param array $params POST parameters/Query string * @param callable $callable [deprecated] function to run after controller instantiation. Use setCallable() method instead */ - public function request($http_method, $argv, $params = [], $callable = null) + public function request($http_method, $argv, $params = [], + $callable = null, $header = []) { // We need this because if 404 route, no controller is created. // But we need $this->CI->output->_status $this->CI =& get_instance(); try { + + echo PHP_EOL; + echo PHP_EOL; + echo PHP_EOL; + echo "=========UNIT =======".PHP_EOL; + + var_dump($header); + + foreach ($header as $key => $value) { + $_SERVER['HTTP_'.$key] = $value; + echo PHP_EOL. $_SERVER['HTTP_'.$key] . PHP_EOL.PHP_EOL; + } + + echo "===================="; + echo PHP_EOL; + echo PHP_EOL; + echo PHP_EOL; + if (is_array($argv)) { return $this->callControllerMethod( @@ -141,7 +160,9 @@ protected function callControllerMethod($http_method, $argv, $request_params, $c $_SERVER['REQUEST_METHOD'] = $http_method; $_SERVER['argv'] = array_merge(['index.php'], $argv); - if ($http_method === 'POST') + if ($http_method === 'POST' + || $http_method === 'PUT' + || $http_method === 'DELETE') { $_POST = $request_params; } @@ -195,12 +216,15 @@ protected function callControllerMethod($http_method, $argv, $request_params, $c * @param array $request_params POST parameters/Query string * @param callable $callable [deprecated] function to run after controller instantiation. Use setCallable() method instead */ - protected function requestUri($http_method, $uri, $request_params, $callable = null) + protected function requestUri($http_method, $uri, $request_params, + $callable = null) { $_SERVER['REQUEST_METHOD'] = $http_method; $_SERVER['argv'] = ['index.php', $uri]; - if ($http_method === 'POST') + if ($http_method === 'POST' + || $http_method === 'PUT' + || $http_method === 'DELETE') { $_POST = $request_params; } @@ -351,7 +375,7 @@ protected function getRoute($RTR, $URI) /** * Get HTTP Status Code Info - * + * * @return array ['code' => code, 'text' => text] * @throws LogicException */ From b6b74b57efa92763c96eddfe4b6a12909e9be1a8 Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Mon, 8 Feb 2016 06:59:23 -0500 Subject: [PATCH 06/34] Modify key model to make it more testable #109 --- application/models/Key.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/application/models/Key.php b/application/models/Key.php index b04fdd99..68fc326d 100644 --- a/application/models/Key.php +++ b/application/models/Key.php @@ -22,6 +22,8 @@ function __construct() { */ public function generate_key($user) { + $finalKey = false; + do { // Generate a random salt @@ -40,8 +42,10 @@ public function generate_key($user) ($this->insert($data) !== false && $this->affected_rows() === 1)){ - return $new_key; + $finalKey = $new_key; } + + return $finalKey; } /** From 3835bcfe7542175e9e59f51d696f168a5753ad29 Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Mon, 8 Feb 2016 07:08:15 -0500 Subject: [PATCH 07/34] add tear down to clean up test key test #109 --- application/tests/models/Key_test.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/application/tests/models/Key_test.php b/application/tests/models/Key_test.php index 8d106f04..6fcb2b30 100644 --- a/application/tests/models/Key_test.php +++ b/application/tests/models/Key_test.php @@ -40,6 +40,16 @@ public function test_generateKey(){ } + public static function tearDownAfterClass() { + $CI = &get_instance(); + $CI->load->model('User'); + $CI->load->model('Watch'); + $CI->load->model('Key'); + $CI->Key->delete_where(array("key >=" => "0")); + $CI->watch->delete_where(array("watchId >=" => "0")); + $CI->User->delete_where(array("userId >=" => "0")); + } + } ?> From ae256a9a67194bdfb119d120e3bc0f5497e9e7d6 Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Mon, 8 Feb 2016 07:08:30 -0500 Subject: [PATCH 08/34] add soft delete of users #109 --- application/models/User.php | 26 ++++++++++++++++++++++++-- application/tests/models/User_test.php | 8 ++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/application/models/User.php b/application/models/User.php index e772aabc..a09413c5 100644 --- a/application/models/User.php +++ b/application/models/User.php @@ -16,6 +16,7 @@ class User extends ObservableModel { function __construct() { parent::__construct(); $this->table_name = "user"; + $this->key = "userId"; } /** @@ -28,7 +29,8 @@ function __construct() { function login($email, $password) { $res = false; - $user = $this->select('*') + $user = $this->select('userId, email, name, firstname, + timezone, country, registerDate') ->where('email', $email) ->where('password', hash('sha256', $password)) ->find_all(); @@ -87,6 +89,26 @@ function logout() { return true; } + /** + * Soft delete users + * @param int $userId + * @return boolean + */ + function delete($id = NULL){ + + return $this->update($id, + [ + 'email' => 'deleted@user.com', + 'password' => 'deleted user', + 'firstname' => 'deleted user', + 'name' => 'deleted user', + 'timezone' => 'deleted user', + 'country' => 'deleted user', + 'isActive' => 0, + ] + ) && $this->affected_rows() === 1; + } + /** * Checks $email is linked with an account on tw * @param String $email The email to check against the db @@ -153,7 +175,7 @@ function signup($email, $password, $name, $firstname, $country) { $this->notify($event, $user); $res = true; - } + } return $res; } diff --git a/application/tests/models/User_test.php b/application/tests/models/User_test.php index f43a8c8c..9badd5da 100644 --- a/application/tests/models/User_test.php +++ b/application/tests/models/User_test.php @@ -205,6 +205,14 @@ public function test_getUserFromWatchId() { ); } + public function test_delete(){ + + $this->assertEquals(true, $this->obj->delete(self::$userId)); + $user = $this->obj->getUser(self::$userId); + $this->assertEquals("deleted@user.com", $user->email); + + } + public static function tearDownAfterClass() { $CI = &get_instance(); $CI->load->model('User'); From 7714a810b324d5bd71c4cc7b69047eb55f9486d4 Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Mon, 8 Feb 2016 07:18:13 -0500 Subject: [PATCH 09/34] delete test controller #109 --- application/controllers/Rest_server.php | 13 - application/controllers/api/Key.php | 272 ------------------ application/controllers/api/User.php | 100 ------- .../tests/controllers/api/User_test.php | 47 --- 4 files changed, 432 deletions(-) delete mode 100755 application/controllers/Rest_server.php delete mode 100755 application/controllers/api/Key.php delete mode 100755 application/controllers/api/User.php delete mode 100644 application/tests/controllers/api/User_test.php diff --git a/application/controllers/Rest_server.php b/application/controllers/Rest_server.php deleted file mode 100755 index 5d44f921..00000000 --- a/application/controllers/Rest_server.php +++ /dev/null @@ -1,13 +0,0 @@ -load->helper('url'); - - $this->load->view('rest_server'); - } -} diff --git a/application/controllers/api/Key.php b/application/controllers/api/Key.php deleted file mode 100755 index 0fa67172..00000000 --- a/application/controllers/api/Key.php +++ /dev/null @@ -1,272 +0,0 @@ - ['level' => 10, 'limit' => 10], - 'index_delete' => ['level' => 10], - 'level_post' => ['level' => 10], - 'regenerate_post' => ['level' => 10], - ]; - - /** - * Insert a key into the database - * - * @access public - * @return void - */ - public function index_put() - { - // Build a new key - $key = $this->_generate_key(); - - // If no key level provided, provide a generic key - $level = $this->put('level') ? $this->put('level') : 1; - $ignore_limits = ctype_digit($this->put('ignore_limits')) ? (int) $this->put('ignore_limits') : 1; - - // Insert the new key - if ($this->_insert_key($key, ['level' => $level, 'ignore_limits' => $ignore_limits])) - { - $this->response([ - 'status' => TRUE, - 'key' => $key - ], REST_Controller::HTTP_CREATED); // CREATED (201) being the HTTP response code - } - else - { - $this->response([ - 'status' => FALSE, - 'message' => 'Could not save the key' - ], REST_Controller::HTTP_INTERNAL_SERVER_ERROR); // INTERNAL_SERVER_ERROR (500) being the HTTP response code - } - } - - /** - * Remove a key from the database to stop it working - * - * @access public - * @return void - */ - public function index_delete() - { - $key = $this->delete('key'); - - // Does this key exist? - if (!$this->_key_exists($key)) - { - // It doesn't appear the key exists - $this->response([ - 'status' => FALSE, - 'message' => 'Invalid API key' - ], REST_Controller::HTTP_BAD_REQUEST); // BAD_REQUEST (400) being the HTTP response code - } - - // Destroy it - $this->_delete_key($key); - - // Respond that the key was destroyed - $this->response([ - 'status' => TRUE, - 'message' => 'API key was deleted' - ], REST_Controller::HTTP_NO_CONTENT); // NO_CONTENT (204) being the HTTP response code - } - - /** - * Change the level - * - * @access public - * @return void - */ - public function level_post() - { - $key = $this->post('key'); - $new_level = $this->post('level'); - - // Does this key exist? - if (!$this->_key_exists($key)) - { - // It doesn't appear the key exists - $this->response([ - 'status' => FALSE, - 'message' => 'Invalid API key' - ], REST_Controller::HTTP_BAD_REQUEST); // BAD_REQUEST (400) being the HTTP response code - } - - // Update the key level - if ($this->_update_key($key, ['level' => $new_level])) - { - $this->response([ - 'status' => TRUE, - 'message' => 'API key was updated' - ], REST_Controller::HTTP_OK); // OK (200) being the HTTP response code - } - else - { - $this->response([ - 'status' => FALSE, - 'message' => 'Could not update the key level' - ], REST_Controller::HTTP_INTERNAL_SERVER_ERROR); // INTERNAL_SERVER_ERROR (500) being the HTTP response code - } - } - - /** - * Suspend a key - * - * @access public - * @return void - */ - public function suspend_post() - { - $key = $this->post('key'); - - // Does this key exist? - if (!$this->_key_exists($key)) - { - // It doesn't appear the key exists - $this->response([ - 'status' => FALSE, - 'message' => 'Invalid API key' - ], REST_Controller::HTTP_BAD_REQUEST); // BAD_REQUEST (400) being the HTTP response code - } - - // Update the key level - if ($this->_update_key($key, ['level' => 0])) - { - $this->response([ - 'status' => TRUE, - 'message' => 'Key was suspended' - ], REST_Controller::HTTP_OK); // OK (200) being the HTTP response code - } - else - { - $this->response([ - 'status' => FALSE, - 'message' => 'Could not suspend the user' - ], REST_Controller::HTTP_INTERNAL_SERVER_ERROR); // INTERNAL_SERVER_ERROR (500) being the HTTP response code - } - } - - /** - * Regenerate a key - * - * @access public - * @return void - */ - public function regenerate_post() - { - $old_key = $this->post('key'); - $key_details = $this->_get_key($old_key); - - // Does this key exist? - if (!$key_details) - { - // It doesn't appear the key exists - $this->response([ - 'status' => FALSE, - 'message' => 'Invalid API key' - ], REST_Controller::HTTP_BAD_REQUEST); // BAD_REQUEST (400) being the HTTP response code - } - - // Build a new key - $new_key = $this->_generate_key(); - - // Insert the new key - if ($this->_insert_key($new_key, ['level' => $key_details->level, 'ignore_limits' => $key_details->ignore_limits])) - { - // Suspend old key - $this->_update_key($old_key, ['level' => 0]); - - $this->response([ - 'status' => TRUE, - 'key' => $new_key - ], REST_Controller::HTTP_CREATED); // CREATED (201) being the HTTP response code - } - else - { - $this->response([ - 'status' => FALSE, - 'message' => 'Could not save the key' - ], REST_Controller::HTTP_INTERNAL_SERVER_ERROR); // INTERNAL_SERVER_ERROR (500) being the HTTP response code - } - } - - /* Helper Methods */ - - private function _generate_key() - { - do - { - // Generate a random salt - $salt = base_convert(bin2hex($this->security->get_random_bytes(64)), 16, 36); - - // If an error occurred, then fall back to the previous method - if ($salt === FALSE) - { - $salt = hash('sha256', time() . mt_rand()); - } - - $new_key = substr($salt, 0, config_item('rest_key_length')); - } - while ($this->_key_exists($new_key)); - - return $new_key; - } - - /* Private Data Methods */ - - private function _get_key($key) - { - return $this->db - ->where(config_item('rest_key_column'), $key) - ->get(config_item('rest_keys_table')) - ->row(); - } - - private function _key_exists($key) - { - return $this->db - ->where(config_item('rest_key_column'), $key) - ->count_all_results(config_item('rest_keys_table')) > 0; - } - - private function _insert_key($key, $data) - { - $data[config_item('rest_key_column')] = $key; - $data['date_created'] = function_exists('now') ? now() : time(); - - return $this->db - ->set($data) - ->insert(config_item('rest_keys_table')); - } - - private function _update_key($key, $data) - { - return $this->db - ->where(config_item('rest_key_column'), $key) - ->update(config_item('rest_keys_table'), $data); - } - - private function _delete_key($key) - { - return $this->db - ->where(config_item('rest_key_column'), $key) - ->delete(config_item('rest_keys_table')); - } - -} diff --git a/application/controllers/api/User.php b/application/controllers/api/User.php deleted file mode 100755 index 83badf78..00000000 --- a/application/controllers/api/User.php +++ /dev/null @@ -1,100 +0,0 @@ -load->model("Key"); - } - - public function auth_put() - { - $email = $this->put('email'); - $password = $this->put('password'); - $this->loginAndAuth($email, $password); - } - - private function loginAndAuth($email, $password){ - - if($email !== NULL && $password !== NULL){ - - $user = $this->user->login($email, $password); - - if($user !== false){ - $key = $this->key->generate_key($user); - - if($key !== false){ - - $user["key"] = $key; - $this->response($user, REST_Controller::HTTP_OK); - }else{ - $this->response(NULL, REST_Controller::HTTP_INTERNAL_SERVER_ERROR); - } - }else{ - $this->response(NULL, REST_Controller::HTTP_UNAUTHORIZED); - } - }else{ - $this->response(NULL, REST_Controller::HTTP_BAD_REQUEST); - } - - } - - public function create_post() - { - echo "qlkzdqdn"; - $email = $this->post('email'); - $password = $this->post('password'); - $name = $this->post('name'); - $firstname = $this->post('firstname'); - $timezone = $this->post('timezone'); - $country = $this->post('country'); - - //If the email isn't already in used - if (!$this->user->checkUserEmail($email)) { - - // Create the account - if ($this->user->signup( - $email, $password, $name, $firstname, - $timezone, $country)) { - - $this->loginAndAuth($email, $password); - - } else { - - $this->response(NULL, REST_Controller::HTTP_INTERNAL_SERVER_ERROR); - } - //The email is already in use - } else { - $this->response(["message" => "email"], - REST_Controller::HTTP_UNAUTHORIZED); - } - } - - public function users_delete() - { - $id = (int) $this->get('id'); - - // Validate the 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' => $id, - 'message' => 'Deleted the resource' - ]; - - $this->set_response($message, REST_Controller::HTTP_NO_CONTENT); // NO_CONTENT (204) being the HTTP response code - } - -} diff --git a/application/tests/controllers/api/User_test.php b/application/tests/controllers/api/User_test.php deleted file mode 100644 index e1e953f2..00000000 --- a/application/tests/controllers/api/User_test.php +++ /dev/null @@ -1,47 +0,0 @@ -emailWatch = new MY_Model('email_watch'); - $CI->emailMeasure = new MY_Model('email_measure'); - $CI->emailUser = new MY_Model('email_user'); - - $CI->emailUser->delete_where(array("id >=" => "0")); - $CI->emailWatch->delete_where(array("id >=" => "0")); - $CI->emailMeasure->delete_where(array("id >=" => "0")); - - $CI->load->model('User'); - $CI->load->model('Measure'); - $CI->load->model('Watch'); - $CI->load->model('Key'); - $CI->User->delete_where(array("userId >=" => "0")); - $CI->Measure->delete_where(array("id >=" => "0")); - $CI->Watch->delete_where(array("watchId >=" => "0")); - $CI->Key->delete_where(array("id >=" => "0")); - } - - public function test_create() { - $output = $this->request( - 'POST', - ['api/user', 'create'], - [ - 'email' => 'mathieu@gmail.com', - 'password' => 'password', - 'name' => 'name', - 'firstname' => 'firstname', - 'timezone' => 'timezone', - 'country' => 'country', - 'mailingList' => 'false' - ] - ); - - var_dump($output); - - $this->assertContains('{"success":true}', $output); - } - - - -} From b16dfe6dd69baf1d9695903d5184867f03d4be9c Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Mon, 8 Feb 2016 07:18:52 -0500 Subject: [PATCH 10/34] Add a rest controller for Users management #109 --- application/controllers/api/Users_api.php | 92 ++++++++++++ .../tests/controllers/api/Users_api_test.php | 135 ++++++++++++++++++ 2 files changed, 227 insertions(+) create mode 100755 application/controllers/api/Users_api.php create mode 100644 application/tests/controllers/api/Users_api_test.php diff --git a/application/controllers/api/Users_api.php b/application/controllers/api/Users_api.php new file mode 100755 index 00000000..6e325cef --- /dev/null +++ b/application/controllers/api/Users_api.php @@ -0,0 +1,92 @@ + ['key' => false], + 'index_post' => ['key' => false], + 'index_delete' => ['key' => true] + ]; + + public function __construct(){ + parent::__construct(); + $this->load->model("key"); + } + + public function index_put() + { + $email = $this->put('email'); + $password = $this->put('password'); + + $this->loginAndAuth($email, $password); + } + + private function loginAndAuth($email, $password){ + + if($email !== NULL && $password !== NULL){ + + $user = $this->user->login($email, $password); + + if($user !== false){ + + $key = $this->key->generate_key($user); + + if($key !== false){ + + $user->key = $key; + $this->response($user, REST_Controller::HTTP_OK); + } + }else{ + $this->response(NULL, REST_Controller::HTTP_UNAUTHORIZED); + } + }else{ + $this->response(NULL, REST_Controller::HTTP_BAD_REQUEST); + } + + } + + public function index_post() + { + $email = $this->post('email'); + $password = $this->post('password'); + $name = $this->post('name'); + $firstname = $this->post('firstname'); + $timezone = $this->post('timezone'); + $country = $this->post('country'); + + //If the email isn't already in used + if (!$this->user->checkUserEmail($email)) { + + // Create the account + if ($this->user->signup( + $email, $password, $name, $firstname, + $timezone, $country)) { + + $this->loginAndAuth($email, $password); + } + //The email is already in use + } else { + $this->response(["message" => "email taken"], + REST_Controller::HTTP_BAD_REQUEST); + } + } + + public function index_delete() + { + + $responseCode = REST_Controller::HTTP_INTERNAL_SERVER_ERROR; + + if($this->user->delete($this->rest->user_id) + && $this->key->delete($this->rest->key_id)){ + $responseCode = REST_Controller::HTTP_NO_CONTENT; + } + + $this->response(NULL, $responseCode); + } + +} diff --git a/application/tests/controllers/api/Users_api_test.php b/application/tests/controllers/api/Users_api_test.php new file mode 100644 index 00000000..06e3ddbb --- /dev/null +++ b/application/tests/controllers/api/Users_api_test.php @@ -0,0 +1,135 @@ +load->model('User'); + $CI->load->model('Measure'); + $CI->load->model('Watch'); + $CI->load->model('Key'); + $CI->Key->delete_where(array("id >=" => "0")); + $CI->User->delete_where(array("userId >=" => "0")); + $CI->Measure->delete_where(array("id >=" => "0")); + $CI->Watch->delete_where(array("watchId >=" => "0")); + } + + public function test_create() { + $output = $this->request( + 'POST', + 'api/users', + [ + 'email' => 'mathieu@gmail.com', + 'password' => 'password', + 'name' => 'name', + 'firstname' => 'firstname', + 'timezone' => 'timezone', + 'country' => 'country' + ] + ); + + $this->assertContains('"email":"mathieu@gmail.com"', $output); + $this->assertContains('"key"', $output); + } + + public function test_createReject(){ + $output = $this->request( + 'POST', + 'api/users', + [ + 'email' => 'mathieu@gmail.com', + 'password' => 'password', + 'name' => 'name', + 'firstname' => 'firstname', + 'timezone' => 'timezone', + 'country' => 'country' + ] + ); + $this->assertContains('email taken', $output); + $this->assertResponseCode(400); + } + + public function test_login(){ + + $output = $this->request( + 'PUT', + 'api/users', + [ + 'email' => 'mathieu@gmail.com', + 'password' => 'password' + ] + ); + + $this->assertContains('"email":"mathieu@gmail.com"', $output); + $this->assertContains('"key"', $output); + self::$userKey = json_decode($output)->key; + } + + public function testLoginFail(){ + $output = $this->request( + 'PUT', + 'api/users', + [ + 'email' => 'mathieu@gmail.com', + 'password' => 'paqzdqzdssword' + ] + ); + + $this->assertResponseCode(401); + } + + public function testLoginBadRequest(){ + $output = $this->request( + 'PUT', + 'api/users', + [ + 'password' => 'paqzdqzdssword' + ] + ); + + $this->assertResponseCode(400); + } + + public function testDeleteFailNoKey(){ + $output = $this->request( + 'DELETE', + 'api/users', + [ + 'id' => 200 + ] + ); + + $this->assertResponseCode(403); + } + + public function testDeleteFailBadKey(){ + + $this->request( + 'DELETE', + 'api/users', + [], + null, + [ + 'X_API_KEY'=> "some key" + ] + ); + + $this->assertResponseCode(403); + } + + public function testDelete(){ + + $this->request( + 'DELETE', + 'api/users', + [], + null, + array('X_API_KEY' => self::$userKey) + ); + + $this->assertResponseCode(204); + } +} From 061814cd054b9ecd31769885084417512dbd9fb0 Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Mon, 8 Feb 2016 07:50:29 -0500 Subject: [PATCH 11/34] remove debug prints #109 --- .../tests/_ci_phpunit_test/CIPHPUnitTestRequest.php | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/application/tests/_ci_phpunit_test/CIPHPUnitTestRequest.php b/application/tests/_ci_phpunit_test/CIPHPUnitTestRequest.php index 5066af04..c1bd544e 100644 --- a/application/tests/_ci_phpunit_test/CIPHPUnitTestRequest.php +++ b/application/tests/_ci_phpunit_test/CIPHPUnitTestRequest.php @@ -78,23 +78,10 @@ public function request($http_method, $argv, $params = [], try { - echo PHP_EOL; - echo PHP_EOL; - echo PHP_EOL; - echo "=========UNIT =======".PHP_EOL; - - var_dump($header); - foreach ($header as $key => $value) { $_SERVER['HTTP_'.$key] = $value; - echo PHP_EOL. $_SERVER['HTTP_'.$key] . PHP_EOL.PHP_EOL; } - echo "===================="; - echo PHP_EOL; - echo PHP_EOL; - echo PHP_EOL; - if (is_array($argv)) { return $this->callControllerMethod( From b0c9b13a7eb4d0513c48773aa4f6eb48e3d00e2b Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Mon, 8 Feb 2016 07:50:51 -0500 Subject: [PATCH 12/34] Jack now displays delete users #109 --- application/tests/controllers/Hooks_test.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/tests/controllers/Hooks_test.php b/application/tests/controllers/Hooks_test.php index 2242c278..1f92943d 100644 --- a/application/tests/controllers/Hooks_test.php +++ b/application/tests/controllers/Hooks_test.php @@ -34,7 +34,8 @@ public function test_index() { ] ); - $this->assertContains('1. ', $output); + $this->assertContains('1', $output); + $this->assertContains('deleted', $output); } public function test_indexMeasures() { From f3dc526536b245f1924da750f7729cb7d954d12b Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Mon, 8 Feb 2016 07:51:18 -0500 Subject: [PATCH 13/34] Jack now displays delete users #109 --- application/controllers/Hooks.php | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/application/controllers/Hooks.php b/application/controllers/Hooks.php index 7c6c36ca..f4cc7381 100644 --- a/application/controllers/Hooks.php +++ b/application/controllers/Hooks.php @@ -70,10 +70,15 @@ function index() { $text = $this->input->post('text'); $quote = $this->quotes[rand(0, 18)]; $result["text"] = $quote; + $activeUser = new MY_MODEL("active_user"); + if (startsWith($text, "Jack nbusers")) { - $result["text"] = $this->user->count_all().". ".$quote; + $activeUserCount = $activeUser->count_all(); + $deletedUser = $this->user->count_all() - $activeUserCount; + + $result["text"] = $activeUserCount ." (".$deletedUser." deleted users). ".$quote; } else if (startsWith($text, "Jack nbmeasures")) { @@ -86,19 +91,16 @@ function index() { //FIXME: Doesn't work in production. Add tests } else if (startsWith($text, "Jack whois")) { - $user = $this->user->select(" user.userId, user.name, firstname, - DATE_FORMAT(FROM_UNIXTIME(`registerDate`), '%e %b %Y') AS 'register', - DATE_FORMAT(FROM_UNIXTIME(`lastLogin`), '%e %b %Y') AS 'lastLogin'", false) + $user = $activeUser ->find_by('email', str_replace("Jack whois ", "", $text)); if ($user) { - $watches = $this->watch->getWatches($user->userId); - $measures = $this->measure->getMeasuresByUser($user->userId, $watches); + $result["text"] = "Id ".$user->userId.", Name ".$user->name. " ,Firstname ".$user->firstname." ,Register ".$user->register. - " ,LastLogin ".$user->lastLogin." ,Watches ".sizeof($watches). - " ,Measures ".sizeof($measures); + " ,LastLogin ".$user->lastLogin." ,Watches ".$user->watches. + " ,Measures ".$user->measures; } else { $result["text"] = "User not found. ".$this->db->last_query(); From 5681d44c0f0d339bf92a23403d425263629d076c Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Mon, 8 Feb 2016 09:39:01 -0500 Subject: [PATCH 14/34] Optimize auto_emal and hooks to use active_user view #109 --- application/controllers/Hooks.php | 2 +- application/libraries/Auto_email.php | 51 ++++++++++--------- .../tests/libraries/Auto_email_test.php | 15 +++--- 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/application/controllers/Hooks.php b/application/controllers/Hooks.php index f4cc7381..2b74c868 100644 --- a/application/controllers/Hooks.php +++ b/application/controllers/Hooks.php @@ -70,7 +70,7 @@ function index() { $text = $this->input->post('text'); $quote = $this->quotes[rand(0, 18)]; $result["text"] = $quote; - $activeUser = new MY_MODEL("active_user"); + $activeUser = new MY_MODEL("active_user"); if (startsWith($text, "Jack nbusers")) { diff --git a/application/libraries/Auto_email.php b/application/libraries/Auto_email.php index 7db40bc9..190f3b96 100644 --- a/application/libraries/Auto_email.php +++ b/application/libraries/Auto_email.php @@ -82,7 +82,6 @@ function __construct() { $this->CI->load->library("__"); $this->CI->load->model("watch"); $this->CI->load->model("measure"); - $this->CI->load->model("user"); $this->CI->load->helper("email_content"); $this->CI->load->library("mcapi"); $this->CI->config->load('config'); @@ -147,6 +146,7 @@ public function cronCheck($timeOffset = 0) { $emailsMeasureSent = array(); $this->emailBatchModel = new MY_MODEL("email_batch"); + $this->activeUser = new MY_MODEL('active_user'); $this->lastBatchDate = $this->findLastBatchDate(); @@ -218,7 +218,11 @@ private function showSentEmails($emails, $title){ echo "

".$title."

"; foreach ($emails as $email) { - echo 'TO ' . $this->CI->user->find_by('userId', $email['userId'])->email; + + if(isset($email['userId'])){ + echo 'TO ' . $this->CI->user->find_by('userId', $email['userId'])->email; + } + echo '\n'; var_dump($email['mandrill']); echo '\n'; echo $email['content']; } @@ -386,8 +390,7 @@ private function inactiveUser(&$queuedEmail) { log_message('info', 'inactiveUser'); - $inactiveUsers = $this->CI - ->user + $inactiveUsers = $this->activeUser ->select() ->where('lastLogin <', $this->getBatchUpperBound($this->day*100)) ->where('lastLogin >', $this->getBatchLowerBound($this->day*100)) @@ -434,10 +437,9 @@ private function userWithoutWatch(&$queuedEmail) { log_message('info', 'userWithoutWatch'); - $userWithoutWatch = $this->CI - ->user - ->select('user.userId, user.name, firstname, email, lastLogin') - ->where('(select count(1) from watch where user.userId = watch.userId) =', 0) + $userWithoutWatch = $this->activeUser + ->select('userId, name, firstname, email, lastLogin') + ->where('watches', 0) ->where('lastLogin <', $this->getBatchUpperBound($this->day)) ->where('lastLogin >', $this->getBatchLowerBound($this->day)) ->find_all(); @@ -479,9 +481,9 @@ private function userWithWatchWithoutMeasure(&$queuedEmail) { $userWithWatchWithoutMeasure = $this->CI ->watch - ->select('user.userId, watch.watchId, watch.brand, watch.name as watchName, - user.name as lastname, user.firstname, email') - ->join('user', 'watch.userId = user.userId') + ->select('active_user.userId, watch.watchId, watch.brand, watch.name as watchName, + active_user.name as lastname, firstname, email') + ->join('active_user', 'watch.userId = active_user.userId') ->where('(select count(1) from measure where watch.watchId = measure.watchId) = ', 0) ->where('creationDate < ', $this->getBatchUpperBound($this->day)) ->where('creationDate > ', $this->getBatchLowerBound($this->day)) @@ -497,7 +499,7 @@ private function userWithWatchWithoutMeasure(&$queuedEmail) { $emailcontent = $this->CI->load->view( 'email/generic', makeFirstMeasureContent( - $user->firstname, + $user[0]['firstname'], $user, $this->CI->measure->getMeasuresByUser($user[0]['userId']) ), @@ -534,13 +536,12 @@ private function userWithOneCompleteMeasureAndOneWatch(&$queuedEmail) { log_message('info', 'userWithOneCompleteMeasureAndOneWatch'); - $userWithOneCompleteMeasureAndOneWatch = $this->CI - ->user - ->select('user.userId, user.name, firstname, email') - ->where('(select count(1) from watch where user.userId = watch.userId) = ', 1) + $userWithOneCompleteMeasureAndOneWatch = $this->activeUser + ->select('active_user.userId, active_user.name, firstname, email') + ->where('watches', 1) ->where('(select count(1) from measure join watch on measure.watchId = watch.watchId - where user.userId = watch.userId + where active_user.userId = watch.userId and measure.statusId = 2 and measure.accuracyReferenceTime < '.$this->getBatchUpperBound($this->day*2).' and measure.accuracyReferenceTime > '.$this->getBatchLowerBound($this->day*2). ') = ', 1) @@ -599,10 +600,10 @@ private function checkAccuracy(&$queuedEmail) { $measureWithoutAccuracy = $this->CI ->measure ->select('measure.id as measureId, measure.*, watch.*, - watch.name as watchName, user.userId, user.name as lastname, - user.firstname, email') + watch.name as watchName, active_user.userId, active_user.name as lastname, + active_user.firstname, email') ->join('watch', 'watch.watchId = measure.watchId') - ->join('user', 'watch.userId = user.userId') + ->join('active_user', 'watch.userId = active_user.userId') ->where('statusId', 1) ->where('measureReferenceTime <', $this->getBatchUpperBound($this->day)) ->where('measureReferenceTime >', $this->getBatchLowerBound($this->day)) @@ -656,10 +657,10 @@ private function checkAccuracyOneWeek(&$queuedEmail) { $measureWithoutAccuracy = $this->CI ->measure - ->select('measure.id as measureId, watch.*, user.userId, - measure.*, watch.name as watchName, user.name as lastname, user.firstname, email') + ->select('measure.id as measureId, watch.*, active_user.userId, + measure.*, watch.name as watchName, active_user.name as lastname, active_user.firstname, email') ->join('watch', 'watch.watchId = measure.watchId') - ->join('user', 'watch.userId = user.userId') + ->join('active_user', 'watch.userId = active_user.userId') ->where('statusId', 1) ->where('measureReferenceTime <', $this->getBatchUpperBound($this->day*7)) ->where('measureReferenceTime >', $this->getBatchLowerBound($this->day*7)) @@ -714,9 +715,9 @@ private function startANewMeasure(&$queuedEmail) { $watchesInNeedOfNewMeasure = $this->CI ->measure ->select('watch.watchId, watch.name as watchName, watch.brand, - user.userId, user.name as lastname, user.firstname, email, measure.*') + active_user.userId, active_user.name as lastname, active_user.firstname, email, measure.*') ->join('watch', 'watch.watchId = measure.watchId') - ->join('user', 'watch.userId = user.userId') + ->join('active_user', 'watch.userId = active_user.userId') ->where('statusId', 2) ->where('accuracyReferenceTime <', $this->getBatchUpperBound($this->day*30)) ->where('accuracyReferenceTime >', $this->getBatchLowerBound($this->day*30)) diff --git a/application/tests/libraries/Auto_email_test.php b/application/tests/libraries/Auto_email_test.php index 69b978a9..b3abab04 100644 --- a/application/tests/libraries/Auto_email_test.php +++ b/application/tests/libraries/Auto_email_test.php @@ -47,7 +47,6 @@ public static function setUpBeforeClass() { $emailBatch = new MY_Model("email_batch"); $emailBatch->delete_where(array("id >=" => "0")); - $CI->measure->delete_where(array("id >=" => "0")); $CI->Watch->delete_where(array("watchId >=" => "0")); $CI->User->delete_where(array("userId >=" => "0")); @@ -438,13 +437,13 @@ public function test_addFirstMeasure(){ } public static function tearDownAfterClass() { - $CI = &get_instance(); - $CI->load->model('User'); - $CI->load->model('Watch'); - $CI->load->model('Measure'); - $CI->watch->delete_where(array("watchId >=" => "0")); - $CI->User->delete_where(array("userId >=" => "0")); - $CI->Measure->delete_where(array("id >=" => "0")); + $CI = &get_instance(); + $CI->load->model('User'); + $CI->load->model('Watch'); + $CI->load->model('Measure'); + $CI->watch->delete_where(array("watchId >=" => "0")); + $CI->User->delete_where(array("userId >=" => "0")); + $CI->Measure->delete_where(array("id >=" => "0")); } } From 5461c0ea482613822baef11bb9f38476c56ca9dc Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Mon, 8 Feb 2016 12:24:08 -0500 Subject: [PATCH 15/34] some comments for user endpoint #109 --- application/controllers/api/Users_api.php | 30 +++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/application/controllers/api/Users_api.php b/application/controllers/api/Users_api.php index 6e325cef..e9a746d7 100755 --- a/application/controllers/api/Users_api.php +++ b/application/controllers/api/Users_api.php @@ -7,17 +7,28 @@ class Users_api extends REST_Controller { + /** + * Defines which methods are protected by + * an API key + * @var Array + */ protected $methods = [ 'index_put' => ['key' => false], 'index_post' => ['key' => false], 'index_delete' => ['key' => true] ]; + /** + * Default constructor + */ public function __construct(){ parent::__construct(); $this->load->model("key"); } + /** + * Login endpoint + */ public function index_put() { $email = $this->put('email'); @@ -26,6 +37,16 @@ public function index_put() $this->loginAndAuth($email, $password); } + /** + * Fetches an user according to $email and $password. + * Create or refresh API key for the given user. + * + * @param String $email + * @param String $password + * @return HTTP_OK + * @return HTTP_UNAUTHORIZED (login failed) + * @return HTTP_BAD_REQUEST (if $email or $password is missing) + */ private function loginAndAuth($email, $password){ if($email !== NULL && $password !== NULL){ @@ -50,6 +71,10 @@ private function loginAndAuth($email, $password){ } + /** + * Signup enpoints + * @return HTTP_BAD_REQUEST if email is taken + */ public function index_post() { $email = $this->post('email'); @@ -76,6 +101,11 @@ public function index_post() } } + /** + * Delete an user + * @return HTTP_INTERNAL_SERVER_ERROR (delete failed) + * @return HTTP_NO_CONTENT on success + */ public function index_delete() { From b34769b30bb3a6d0ff85a2515e3e694fd56ef304 Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Mon, 8 Feb 2016 13:18:58 -0500 Subject: [PATCH 16/34] add limit configuration #109 --- application/config/rest.php | 7 ++++--- application/controllers/api/Users_api.php | 2 +- application/libraries/REST_Controller.php | 8 ++++---- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/application/config/rest.php b/application/config/rest.php index 821e3f1a..ca5a75bb 100755 --- a/application/config/rest.php +++ b/application/config/rest.php @@ -451,8 +451,9 @@ | `uri` VARCHAR(255) NOT NULL, | `count` INT(10) NOT NULL, | `hour_started` INT(11) NOT NULL, -| `api_key` VARCHAR(40) NOT NULL, -| PRIMARY KEY (`id`) +| `api_key_id` INT(11) NOT NULL, +| PRIMARY KEY (`id`), +| FOREIGN KEY (`api_key_id`) REFERENCES `keys`(`id`) ON DELETE CASCADE | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; | | To specify the limits within the controller's __construct() method, add per-method @@ -462,7 +463,7 @@ | | See application/controllers/api/example.php for examples */ -$config['rest_enable_limits'] = FALSE; +$config['rest_enable_limits'] = TRUE; /* |-------------------------------------------------------------------------- diff --git a/application/controllers/api/Users_api.php b/application/controllers/api/Users_api.php index e9a746d7..7d029ac4 100755 --- a/application/controllers/api/Users_api.php +++ b/application/controllers/api/Users_api.php @@ -15,7 +15,7 @@ class Users_api extends REST_Controller { protected $methods = [ 'index_put' => ['key' => false], 'index_post' => ['key' => false], - 'index_delete' => ['key' => true] + 'index_delete' => ['key' => true, 'limit' => 20] ]; /** diff --git a/application/libraries/REST_Controller.php b/application/libraries/REST_Controller.php index 2624a9b0..b9864fc6 100644 --- a/application/libraries/REST_Controller.php +++ b/application/libraries/REST_Controller.php @@ -1157,7 +1157,7 @@ protected function _check_limit($controller_method) // 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) + ->where('api_key_id', $this->rest->key_id) ->get($this->config->item('rest_limits_table')) ->row(); @@ -1167,7 +1167,7 @@ protected function _check_limit($controller_method) // 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 : '', + 'api_key_id' => isset($this->rest->key_id) ? $this->rest->key_id : '', 'count' => 1, 'hour_started' => time() ]); @@ -1179,7 +1179,7 @@ protected function _check_limit($controller_method) // Reset the started period and count $this->rest->db ->where('uri', $limited_uri) - ->where('api_key', isset($this->rest->key) ? $this->rest->key : '') + ->where('api_key_id', isset($this->rest->key_id) ? $this->rest->key_id : '') ->set('hour_started', time()) ->set('count', 1) ->update($this->config->item('rest_limits_table')); @@ -1197,7 +1197,7 @@ protected function _check_limit($controller_method) // Increase the count by one $this->rest->db ->where('uri', $limited_uri) - ->where('api_key', $this->rest->key) + ->where('api_key_id', $this->rest->key_id) ->set('count', 'count + 1', FALSE) ->update($this->config->item('rest_limits_table')); } From 73533c39b87bc1e42fa6c84c9cfebb54948eb6f0 Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Mon, 8 Feb 2016 13:43:12 -0500 Subject: [PATCH 17/34] reset database variables #109 --- application/config/database.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/application/config/database.php b/application/config/database.php index a30bfdd4..6090464c 100644 --- a/application/config/database.php +++ b/application/config/database.php @@ -85,7 +85,8 @@ ); -$db['default']['hostname'] = "localhost"; -$db['default']['username'] = "root"; -$db['default']['password'] = ""; -$db['default']['database'] = "tw_ci"; +$url = parse_url(getenv("TW_DB_URL")); +$db['default']['hostname'] = $url["host"]; +$db['default']['username'] = $url["user"]; +$db['default']['password'] = $url["pass"]; +$db['default']['database'] = substr($url["path"], 1); From 3a0e1a0fe2771743c25b505a2500e4056b6f7c48 Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Mon, 8 Feb 2016 14:39:20 -0500 Subject: [PATCH 18/34] Add userId to watch deletion for enhanced security #109 --- application/models/Watch.php | 10 ++++++++-- application/tests/models/Watch_test.php | 6 +++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/application/models/Watch.php b/application/models/Watch.php index 91519c39..55c7f248 100644 --- a/application/models/Watch.php +++ b/application/models/Watch.php @@ -128,9 +128,15 @@ function getWatchByMeasureId($measureId){ * @param int $watchId The watch to delete * @return boolean */ - function deleteWatch($watchId) { + function deleteWatch($watchId, $userId) { + + $whereClause = [ + 'watchId' => $watchId, + 'userId' => $userId + ]; + $data = array('status' => 4); - $res = $this->update($watchId, $data) !== false; + $res = $this->update($whereClause, $data) && $this->affected_rows() === 1; $this->notify(DELETE_WATCH, array('user' => arrayToObject($this->session->all_userdata()), diff --git a/application/tests/models/Watch_test.php b/application/tests/models/Watch_test.php index bded7807..5282ad6c 100644 --- a/application/tests/models/Watch_test.php +++ b/application/tests/models/Watch_test.php @@ -147,8 +147,12 @@ public function test_getWatchWrongId() { $this->assertEquals(false, $this->obj->getWatch('42')); } + public function test_deleteWatchWrongId(){ + $this->assertEquals(false, $this->obj->deleteWatch(self::$watchId, 0)); + } + public function test_deleteWatch() { - $this->assertEquals(true, $this->obj->deleteWatch(self::$watchId)); + $this->assertEquals(true, $this->obj->deleteWatch(self::$watchId, self::$userId)); } public function test_getWatchDeletedWatch() { From c9bae6697bf4d4b7a512e7356d0c67802296ada1 Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Mon, 8 Feb 2016 16:47:45 -0500 Subject: [PATCH 19/34] add user id on delete watch for security #109 --- application/controllers/Measures.php | 2 +- application/models/Watch.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/application/controllers/Measures.php b/application/controllers/Measures.php index 38eec3ec..12935e8e 100644 --- a/application/controllers/Measures.php +++ b/application/controllers/Measures.php @@ -98,7 +98,7 @@ public function delete_watch(){ if($this->expectsPost(array('watchId'))){ - if ($this->watch->deleteWatch($this->watchId)) { + if ($this->watch->deleteWatch($this->watchId, $this->session->userdata('userId'))) { $this->_bodyData['success'] = 'Watch successfully deleted!'; } diff --git a/application/models/Watch.php b/application/models/Watch.php index 55c7f248..4850706a 100644 --- a/application/models/Watch.php +++ b/application/models/Watch.php @@ -86,7 +86,7 @@ function editWatch($userId, $watchId, $brand, $name, $yearOfBuy, $serial, $calib $this->notify(UPDATE_WATCH, arrayToObject($data)); - return $res === true && $this->affected_rows() === 1; + return $res && $this->affected_rows() === 1; } /** From 29004c982511eb4ba1761634a3ed11df91694ae8 Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Mon, 8 Feb 2016 16:48:08 -0500 Subject: [PATCH 20/34] api key limits are now on method names #109 --- application/config/rest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/config/rest.php b/application/config/rest.php index ca5a75bb..d279d37b 100755 --- a/application/config/rest.php +++ b/application/config/rest.php @@ -327,7 +327,7 @@ | $config['rest_limits_method'] = 'ROUTED_URL'; // Put a limit on the routed URL | */ -$config['rest_limits_method'] = 'ROUTED_URL'; +$config['rest_limits_method'] = 'METHOD_NAME'; /* |-------------------------------------------------------------------------- From 6105ac716de80f8774ce4dafd69d572f5917b188 Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Mon, 8 Feb 2016 16:48:51 -0500 Subject: [PATCH 21/34] add teardown to user api test #109" --- application/tests/controllers/api/Users_api_test.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/application/tests/controllers/api/Users_api_test.php b/application/tests/controllers/api/Users_api_test.php index 06e3ddbb..a767236b 100644 --- a/application/tests/controllers/api/Users_api_test.php +++ b/application/tests/controllers/api/Users_api_test.php @@ -132,4 +132,16 @@ public function testDelete(){ $this->assertResponseCode(204); } + + public static function tearDownAfterClass() { + $CI = &get_instance(); + $CI->load->model('User'); + $CI->load->model('Measure'); + $CI->load->model('Watch'); + $CI->load->model('Key'); + $CI->Key->delete_where(array("id >=" => "0")); + $CI->User->delete_where(array("userId >=" => "0")); + $CI->Measure->delete_where(array("id >=" => "0")); + $CI->Watch->delete_where(array("watchId >=" => "0")); + } } From 03a38eae031050fc0ee2cc3dd73017dd3de0d1f1 Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Mon, 8 Feb 2016 16:49:40 -0500 Subject: [PATCH 22/34] add specific rules for function with arguments in rest API #109 --- application/config/routes.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/application/config/routes.php b/application/config/routes.php index 5dd72452..bd7f1190 100644 --- a/application/config/routes.php +++ b/application/config/routes.php @@ -73,6 +73,8 @@ */ $route['api/(:any)'] = 'api/$1_api'; +$route['api/(:any)/(:any)/(:any)'] = 'api/$1_api/$2/$3'; +$route['api/(:any)/(:any)/(:any)/(:any)'] = 'api/$1_api/$2/$3/$4'; // // $route['api/example/users/(:num)'] = 'api/user/create/$1'; // Example 4 From 85d2818f7cfdf5a8e861c498a008734f0f902f37 Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Mon, 8 Feb 2016 16:50:08 -0500 Subject: [PATCH 23/34] add watche endpoint for rest API #109 --- application/controllers/api/Watches_api.php | 191 +++++++++++ .../controllers/api/Watches_api_test.php | 315 ++++++++++++++++++ 2 files changed, 506 insertions(+) create mode 100644 application/controllers/api/Watches_api.php create mode 100644 application/tests/controllers/api/Watches_api_test.php diff --git a/application/controllers/api/Watches_api.php b/application/controllers/api/Watches_api.php new file mode 100644 index 00000000..1eab0418 --- /dev/null +++ b/application/controllers/api/Watches_api.php @@ -0,0 +1,191 @@ + ['key' => true, 'limit' => 60], + 'index_post' => ['key' => true, 'limit' => 60], + 'index_delete' => ['key' => true, 'limit' => 60], + 'index_get' => ['key' => true, 'limit' => 600], + 'brands_get' => ['key' => true, 'limit' => 600], + 'models_get' => ['key' => true, 'limit' => 600] + ]; + + /** + * Default constructor + */ + public function __construct(){ + parent::__construct(); + $this->load->model("key"); + $this->load->model("watch"); + $this->load->model("measure"); + } + + /** + * returns the watches and their latest measures. + * + * @return JSON Watches and measure + */ + public function index_get(){ + + $this->response( + $this->measure->getMeasuresByUser($this->rest->user_id), + REST_Controller::HTTP_OK + ); + } + + /** + * Update a watch + * @return HTTP_BAD_REQUEST if arguments are missing + */ + public function index_put() + { + + $watchId = $this->put('id'); + $brand = $this->put('brand'); + $name = $this->put('name'); + $yearOfBuy = $this->put('yearOfBuy'); + $serial = $this->put('serial'); + $caliber = $this->put('caliber'); + + if($watchId !== NULL && $brand !== NULL && $name !== NULL && + $serial !== NULL && $caliber !== NULL){ + + if($this->watch->editWatch($this->rest->user_id, $watchId, + $brand, $name, $yearOfBuy, $serial, $caliber)){ + + $this->response( + $this->measure->getMeasuresByUser($this->rest->user_id), + REST_Controller::HTTP_OK + ); + } + + }else{ + $this->response(NULL, REST_Controller::HTTP_BAD_REQUEST); + } + } + + /** + * Create new watch + * @return HTTP_BAD_REQUEST if arguments are missing + */ + public function index_post() + { + $brand = $this->post('brand'); + $name = $this->post('name'); + $yearOfBuy = $this->post('$yearOfBuy'); + $serial = $this->post('serial'); + $caliber = $this->post('caliber'); + + if($brand !== NULL && $name !== NULL && + $serial !== NULL && $caliber !== NULL){ + + if($this->watch->addWatch($this->rest->user_id, $brand, $name, + $yearOfBuy, $serial, $caliber)){ + + $this->index_get(); + } + + } else { + $this->response(NULL, + REST_Controller::HTTP_BAD_REQUEST); + } + } + + /** + * Autocompletes brand from 2 letters + * @return JSON Array Brand matching $brand arg + */ + public function brands_get($partialBrand = NULL){ + + if(is_string($partialBrand) && strlen($partialBrand) >= 2){ + + $partialBrand = strtolower($partialBrand); + + $brands = json_decode(file_get_contents(APPPATH.'../assets/json/watch-brand.json')); + + $matchingBrands = []; + + foreach ($brands as $brand) { + + if (strpos(strtolower($brand->name), $partialBrand) !== false) { + array_push($matchingBrands, $brand); + } + } + + $this->response($matchingBrands, REST_Controller::HTTP_OK); + + } else { + $this->response(NULL, + REST_Controller::HTTP_BAD_REQUEST); + } + } + + /** + * Autocompletes model from 2 letters and a given brand + * @return JSON Array Models matching $brand and $model arg + */ + public function models_get($brand, $partialModel){ + + if(is_string($brand) && + is_string($partialModel) + && strlen($partialModel) >= 2 + && file_exists(APPPATH.'../assets/json/watch-models/'.$brand.'.json')){ + + $partialModel = strtolower($partialModel); + + $models = json_decode( + file_get_contents( + APPPATH.'../assets/json/watch-models/'.$brand.'.json' + ) + ); + + $matchingModels = []; + + foreach ($models as $model) { + + if (strpos(strtolower($model), $partialModel) !== false) { + + array_push($matchingModels, $model); + } + } + + $this->response($matchingModels, REST_Controller::HTTP_OK); + + + } else { + $this->response(NULL, REST_Controller::HTTP_BAD_REQUEST); + } + } + + /** + * Delete a watch + * @return HTTP_INTERNAL_SERVER_ERROR (delete failed) + */ + public function index_delete() + { + + $watchId = $this->delete('watchId'); + + if($watchId !== NULL + && $this->watch->deleteWatch($watchId, $this->rest->user_id)){ + + $this->index_get(); + }else{ + $this->response(NULL, REST_Controller::HTTP_BAD_REQUEST); + } + } + +} + +?> diff --git a/application/tests/controllers/api/Watches_api_test.php b/application/tests/controllers/api/Watches_api_test.php new file mode 100644 index 00000000..6d8a7c2b --- /dev/null +++ b/application/tests/controllers/api/Watches_api_test.php @@ -0,0 +1,315 @@ +load->model('User'); + $CI->load->model('Measure'); + $CI->load->model('Watch'); + $CI->load->model('Key'); + $CI->Key->delete_where(array("id >=" => "0")); + $CI->User->delete_where(array("userId >=" => "0")); + $CI->Measure->delete_where(array("id >=" => "0")); + $CI->Watch->delete_where(array("watchId >=" => "0")); + + } + + public function test_createUserAndGetAPIKey(){ + $output = $this->request( + 'POST', + 'api/users', + [ + 'email' => 'mathieu@gmail.com', + 'password' => 'password', + 'name' => 'name', + 'firstname' => 'firstname', + 'timezone' => 'timezone', + 'country' => 'country' + ] + ); + + $this->assertContains('"email":"mathieu@gmail.com"', $output); + $this->assertContains('"key"', $output); + self::$userKey = json_decode($output)->key; + } + + public function test_createWatchNoKey(){ + + $output = $this->request( + 'POST', + 'api/watches', + [ + 'brand' => 'brand', + 'name' => 'name', + 'yearOfBuy' => 2000, + 'serial' => 1, + 'caliber' => 'zdq' + ] + ); + + $this->assertResponseCode(403); + } + + public function test_createWatchNotAllArgs(){ + + $output = $this->request( + 'POST', + 'api/watches', + [ + 'brand' => 'brand', + 'name' => 'name', + ], + null, + array('X_API_KEY' => self::$userKey) + ); + + $this->assertResponseCode(400); + } + + public function test_createWatch(){ + + $output = $this->request( + 'POST', + 'api/watches', + [ + 'brand' => 'brand', + 'name' => 'name', + 'yearOfBuy' => 2000, + 'serial' => 1, + 'caliber' => 'zdq' + ], + null, + array('X_API_KEY' => self::$userKey) + ); + + $this->assertContains('"brand":"brand"', $output); + var_dump($output); + self::$watchId = json_decode($output)[0]->watchId; + } + + public function test_updateWatchNoKey(){ + $output = $this->request( + 'PUT', + 'api/watches', + [ + 'id' => self::$watchId, + 'brand' => 'brand', + 'name' => 'name', + 'yearOfBuy' => 2000, + 'serial' => 1, + 'caliber' => 'zdq' + ] + ); + + $this->assertResponseCode(403); + } + + public function test_updateWatchNotAllArgs(){ + $output = $this->request( + 'PUT', + 'api/watches', + [ + 'id' => self::$watchId, + 'brand' => 'brand', + ], + null, + array('X_API_KEY' => self::$userKey) + ); + + $this->assertResponseCode(400); + } + + public function test_updateWatch(){ + + $output = $this->request( + 'PUT', + 'api/watches', + [ + 'id' => self::$watchId, + 'brand' => 'branda', + 'name' => 'name', + 'yearOfBuy' => 2000, + 'serial' => 1, + 'caliber' => 'zdq' + ], + null, + array('X_API_KEY' => self::$userKey) + ); + + $this->assertContains('"brand":"branda"', $output); + } + + public function test_brandAutocompleteNoKey(){ + + $output = $this->request( + 'GET', 'api/watches/brands/j', + [] + ); + + $this->assertResponseCode(403); + + } + + public function test_brandAutocomplete1Letter(){ + $output = $this->request( + 'GET', 'api/watches/brands/j', + [], + null, + array('X_API_KEY' => self::$userKey) + ); + + $this->assertResponseCode(400); + } + + public function test_brandAutocomplete(){ + $output = $this->request( + 'GET', 'api/watches/brands/ja', + [], + null, + array('X_API_KEY' => self::$userKey) + ); + + $this->assertContains('{"name":"Jaeger-LeCoultre","icon":"logo_jaegerlecoultre.jpg","models":"jaegerlecoultre"}', $output); + $this->assertContains('{"name":"Jaquet Droz","icon":"logo_jaquetdroz.jpg","models":"jaquetdroz"}', $output); + $this->assertResponseCode(200); + } + + public function test_modelAutocompleteNoKey(){ + $output = $this->request( + 'GET', 'api/watches/jaegerlecoultre/duo', + [] + ); + + $this->assertResponseCode(403); + } + + public function test_modelAutocomplete1Letter(){ + $output = $this->request( + 'GET', 'api/watches/models/jaegerlecoultre/d', + [], + null, + array('X_API_KEY' => self::$userKey) + ); + + $this->assertResponseCode(400); + } + + public function test_modelAutocompleteBrandDontExist(){ + $output = $this->request( + 'GET', 'api/watches/models/qzdqdqzd/duo', + [], + null, + array('X_API_KEY' => self::$userKey) + ); + + $this->assertResponseCode(400); + } + + public function test_modelAutocomplete(){ + $output = $this->request( + 'GET', 'api/watches/models/jaegerlecoultre/duo', + [], + null, + array('X_API_KEY' => self::$userKey) + ); + + $this->assertContains('Duometre', $output); + $this->assertResponseCode(200); + } + + public function testDeleteNoKey(){ + $output = $this->request( + 'DELETE', + 'api/watches', + [ + + ] + ); + + $this->assertResponseCode(403); + } + + public function testDeleteNotAllArgs(){ + $output = $this->request( + 'DELETE', + 'api/watches', + [], + null, + array('X_API_KEY' => self::$userKey) + ); + + $this->assertResponseCode(400); + } + + public function testDeleteWrongId(){ + $output = $this->request( + 'DELETE', + 'api/watches', + [ + 'watchId' => 0 + ], + null, + array('X_API_KEY' => self::$userKey) + ); + + $this->assertResponseCode(400); + } + + public function testDelete(){ + $output = $this->request( + 'DELETE', + 'api/watches', + [ + 'watchId' => self::$watchId + ], + null, + array('X_API_KEY' => self::$userKey) + ); + + $this->assertResponseCode(200); + } + + public function testLimit(){ + + $CI = &get_instance(); + $limits = new MY_MODEL('limits'); + + $limits->update(null, ["count"=>601]); + + + $output = $this->request( + 'POST', + 'api/watches', + [ + 'brand' => 'brand', + 'name' => 'name', + 'yearOfBuy' => 2000, + 'serial' => 1, + 'caliber' => 'zdq' + ], + null, + array('X_API_KEY' => self::$userKey) + ); + + $this->assertResponseCode(401); + + } + + public static function tearDownAfterClass() { + $CI = &get_instance(); + $CI->load->model('User'); + $CI->load->model('Measure'); + $CI->load->model('Watch'); + $CI->load->model('Key'); + $CI->Key->delete_where(array("id >=" => "0")); + $CI->User->delete_where(array("userId >=" => "0")); + $CI->Measure->delete_where(array("id >=" => "0")); + $CI->Watch->delete_where(array("watchId >=" => "0")); + } + +} From 77ef57d09edad9204935a77c44a4c05d2958a2f4 Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Tue, 9 Feb 2016 15:34:08 -0500 Subject: [PATCH 24/34] Change require to require_once #109 --- application/controllers/api/Users_api.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/controllers/api/Users_api.php b/application/controllers/api/Users_api.php index 7d029ac4..4e92e3a8 100755 --- a/application/controllers/api/Users_api.php +++ b/application/controllers/api/Users_api.php @@ -3,7 +3,7 @@ defined('BASEPATH') OR exit('No direct script access allowed'); // This can be removed if you use __autoload() in config.php OR use Modular Extensions -require APPPATH . '/libraries/REST_Controller.php'; +require_once APPPATH . '/libraries/REST_Controller.php'; class Users_api extends REST_Controller { From bf9716640243a18ce627de58ab2ed80376e4bee3 Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Tue, 9 Feb 2016 15:34:56 -0500 Subject: [PATCH 25/34] add helper function to determine if an user owns a given watch or measure #109 --- application/models/Measure.php | 16 ++++++++++++++++ application/models/Watch.php | 11 +++++++++++ 2 files changed, 27 insertions(+) diff --git a/application/models/Measure.php b/application/models/Measure.php index 09a5de17..4683bb83 100644 --- a/application/models/Measure.php +++ b/application/models/Measure.php @@ -158,6 +158,9 @@ function addBaseMesure($watchId, $referenceTime, $userTime) { $returnValue = $this->insert($data); + $this->notify(NEW_MEASURE, + array('measure' => $data)); + return $returnValue; } @@ -219,6 +222,19 @@ function deleteMesure($measureId) { return $this->update($measureId, $data) !== false; } + /** + * Determine if a measure is owned by an user + * + * @param int $measureId + * @param int $userId + * @return boolean + */ + function isOwnedBy($measureId, $userId){ + return $this->join("watch", "watch.watchId = measure.watchId") + ->where("id", $measureId) + ->count_by("watch.userId", $userId) === 1; + } + /** * Count the amount of watch of $watchBrand * diff --git a/application/models/Watch.php b/application/models/Watch.php index 4850706a..60dadf51 100644 --- a/application/models/Watch.php +++ b/application/models/Watch.php @@ -122,6 +122,17 @@ function getWatchByMeasureId($measureId){ ->find_by('measure.id', $measureId); } + /** + * Checks if a watch belongs to a given suer + * @param int $watchId + * @param int $userId + * @return boolean + */ + function isOwnedBy($watchId, $userId){ + return $this->where("watchId", $watchId) + ->count_by("userId", $userId) > 0; + } + /** * Soft delete watch $watchId * From 3318d2593e184f45efac1fdc75874cb94ee3d61d Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Tue, 9 Feb 2016 15:35:22 -0500 Subject: [PATCH 26/34] Measures endpoints #109 --- application/controllers/api/Measures_api.php | 125 ++++++++ .../controllers/api/Measures_api_test.php | 273 ++++++++++++++++++ 2 files changed, 398 insertions(+) create mode 100644 application/controllers/api/Measures_api.php create mode 100644 application/tests/controllers/api/Measures_api_test.php diff --git a/application/controllers/api/Measures_api.php b/application/controllers/api/Measures_api.php new file mode 100644 index 00000000..8c21ab7d --- /dev/null +++ b/application/controllers/api/Measures_api.php @@ -0,0 +1,125 @@ + ['key' => true, 'limit' => 60], + 'index_post' => ['key' => true, 'limit' => 60], + 'index_delete' => ['key' => true, 'limit' => 60] + ]; + + /** + * Default constructor + */ + public function __construct(){ + parent::__construct(); + $this->load->model("key"); + $this->load->model("watch"); + $this->load->model("measure"); + } + + /** + * Creates a new measure for a given watch + * @param int $watchId + * @param long $referenceTime in ms + * @param long $userTime in ms + * @return HTTP_BAD_REQUEST | HTTP_OK + */ + public function index_post(){ + + $watchId = $this->post('watchId'); + $referenceTime = $this->post('referenceTime'); + $userTime = $this->post('userTime'); + + if($watchId != null && is_numeric($watchId) && + is_numeric($referenceTime) && is_numeric($userTime) + && $this->watch->isOwnedBy($watchId, $this->rest->user_id)){ + + $this->response( + ["measureId" => $this->measure->addBaseMesure( + $watchId, + $referenceTime/1000, + $userTime/1000) + ], + REST_Controller::HTTP_OK); + + }else{ + $this->response(NULL, REST_Controller::HTTP_BAD_REQUEST); + } + } + + /** + * Creates a new accuracy measure for a given measure + * @param int $measureId + * @param long $referenceTime in ms + * @param long $userTime in ms + * @return HTTP_BAD_REQUEST | HTTP_OK + */ + public function index_put(){ + + $measureId = $this->post('measureId'); + $referenceTime = $this->post('referenceTime'); + $userTime = $this->post('userTime'); + + log_message("info", "================="); + log_message("info", $measureId); + log_message("info", $referenceTime); + log_message("info", $userTime); + log_message("info", $this->measure->isOwnedBy($measureId, $this->rest->user_id)); + log_message("info", "================="); + + if($measureId != null && is_numeric($measureId) && + is_numeric($referenceTime) && is_numeric($userTime) + && $this->measure->isOwnedBy($measureId, $this->rest->user_id)){ + + log_message("info", "=========&&&========"); + + + $this->response( + $this->measure->addAccuracyMesure( + $measureId, + $referenceTime/1000, + $userTime/1000) + , + REST_Controller::HTTP_OK); + + }else{ + log_message("info", "========ééé======="); + + $this->response(NULL, REST_Controller::HTTP_BAD_REQUEST); + } + } + + /** + * Soft delete a measure + * + * @param int $measureId + * @return HTTP_BAD_REQUEST | HTTP_OK + */ + public function index_delete(){ + $measureId = $this->post('measureId'); + + if($measureId != null && is_numeric($measureId) + && $this->measure->isOwnedBy($measureId, $this->rest->user_id)){ + + $this->response( + array( + "success"=>$this->measure->delete($measureId) + ), + REST_Controller::HTTP_OK + ); + }else{ + $this->response(NULL, REST_Controller::HTTP_BAD_REQUEST); + } + } +} diff --git a/application/tests/controllers/api/Measures_api_test.php b/application/tests/controllers/api/Measures_api_test.php new file mode 100644 index 00000000..d4d431d5 --- /dev/null +++ b/application/tests/controllers/api/Measures_api_test.php @@ -0,0 +1,273 @@ +load->model('User'); + $CI->load->model('Measure'); + $CI->load->model('Watch'); + $CI->load->model('Key'); + $CI->Key->delete_where(array("id >=" => "0")); + $CI->User->delete_where(array("userId >=" => "0")); + $CI->Measure->delete_where(array("id >=" => "0")); + $CI->Watch->delete_where(array("watchId >=" => "0")); + + } + + public function test_createUserAndGetAPIKey(){ + $output = $this->request( + 'POST', + 'api/users', + [ + 'email' => 'mathieu@gmail.com', + 'password' => 'password', + 'name' => 'name', + 'firstname' => 'firstname', + 'timezone' => 'timezone', + 'country' => 'country' + ] + ); + + $this->assertContains('"email":"mathieu@gmail.com"', $output); + $this->assertContains('"key"', $output); + self::$userKey = json_decode($output)->key; + } + + public function test_createWatch(){ + + $output = $this->request( + 'POST', + 'api/watches', + [ + 'brand' => 'brand', + 'name' => 'name', + 'yearOfBuy' => 2000, + 'serial' => 1, + 'caliber' => 'zdq' + ], + null, + array('X_API_KEY' => self::$userKey) + ); + + $this->assertContains('"brand":"brand"', $output); + self::$watchId = json_decode($output)[0]->watchId; + } + + public function test_addMeasureNoKey(){ + + $output = $this->request( + 'POST', + 'api/measures', + [ ] + ); + + $this->assertResponseCode(403); + } + + public function test_addMeasureNotAllArgs(){ + $output = $this->request( + 'POST', + 'api/measures', + [ + 'referenceTime' => microtime(), + 'userTime' => microtime() + ], + null, + array('X_API_KEY' => self::$userKey) + ); + + $this->assertResponseCode(400); + } + + public function test_addMeasureNotOwnerOfWatch(){ + $output = $this->request( + 'POST', + 'api/measures', + [ + 'watchId' => 0, + 'referenceTime' => round(microtime(true) * 1000), + 'userTime' => round(microtime(true) * 1000) + ], + null, + array('X_API_KEY' => self::$userKey) + ); + + $this->assertResponseCode(400); + } + + public function test_addMeasure(){ + $output = $this->request( + 'POST', + 'api/measures', + [ + 'watchId' => self::$watchId, + 'referenceTime' => round(microtime(true) * 1000), + 'userTime' => round(microtime(true) * 1000) + ], + null, + array('X_API_KEY' => self::$userKey) + ); + + $this->assertResponseCode(200); + $parsedOutput = json_decode($output); + $this->assertEquals(true, is_numeric($parsedOutput->measureId)); + self::$measureId = $parsedOutput->measureId; + } + + public function test_addAccuracyMeasureNoKey(){ + + $output = $this->request( + 'PUT', + 'api/measures', + [ ] + ); + + $this->assertResponseCode(403); + } + + public function test_addAcuraccyMeasureNotAllArgs(){ + $output = $this->request( + 'PUT', + 'api/measures', + [ + 'referenceTime' => microtime(), + 'userTime' => microtime() + ], + null, + array('X_API_KEY' => self::$userKey) + ); + + $this->assertResponseCode(400); + } + + public function test_addAcuracyMeasureNotOwnerOfMeasure(){ + $output = $this->request( + 'PUT', + 'api/measures', + [ + 'measureId' => 0, + 'referenceTime' => round(microtime(true) * 1000), + 'userTime' => round(microtime(true) * 1000) + ], + null, + array('X_API_KEY' => self::$userKey) + ); + + $this->assertResponseCode(400); + } + + public function test_addAcuracyMeasure(){ + $output = $this->request( + 'PUT', + 'api/measures', + [ + 'measureId' => self::$measureId, + 'referenceTime' => round(microtime(true) * 1000)+1000, + 'userTime' => round(microtime(true) * 1000)+1000 + ], + null, + array('X_API_KEY' => self::$userKey) + ); + + $this->assertResponseCode(200); + $parsedOutput = json_decode($output); + $this->assertEquals(true, is_numeric($parsedOutput->accuracy)); + } + + public function test_deleteNoKey(){ + + $output = $this->request( + 'DELETE', + 'api/measures', + [ ] + ); + + $this->assertResponseCode(403); + } + + public function test_deleteNotAllArgs(){ + $output = $this->request( + 'DELETE', + 'api/measures', + [ + ], + null, + array('X_API_KEY' => self::$userKey) + ); + + $this->assertResponseCode(400); + } + + public function test_deleteMeasureNotOwnerOfMeasure(){ + $output = $this->request( + 'DELETE', + 'api/measures', + [ + 'measureId' => 0, + ], + null, + array('X_API_KEY' => self::$userKey) + ); + + $this->assertResponseCode(400); + } + + public function test_deleteMeasure(){ + $output = $this->request( + 'DELETE', + 'api/measures', + [ + 'measureId' => self::$measureId + ], + null, + array('X_API_KEY' => self::$userKey) + ); + + $this->assertResponseCode(200); + $this->assertContains("true", $output); + } + + public function testLimit(){ + + $CI = &get_instance(); + $limits = new MY_MODEL('limits'); + + $limits->update(null, ["count"=>601]); + + + $output = $this->request( + 'POST', + 'api/measures', + [ + 'watchId' => self::$watchId, + 'referenceTime' => round(microtime(true) * 1000), + 'userTime' => round(microtime(true) * 1000) + ], + null, + array('X_API_KEY' => self::$userKey) + ); + + $this->assertResponseCode(401); + + } + + public static function tearDownAfterClass() { + $CI = &get_instance(); + $CI->load->model('User'); + $CI->load->model('Measure'); + $CI->load->model('Watch'); + $CI->load->model('Key'); + $CI->Key->delete_where(array("id >=" => "0")); + $CI->User->delete_where(array("userId >=" => "0")); + $CI->Measure->delete_where(array("id >=" => "0")); + $CI->Watch->delete_where(array("watchId >=" => "0")); + } + + +} From 1374e0b08e1360cb3ab8e547a6236dcc8ccc647c Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Fri, 26 Feb 2016 17:42:03 -0500 Subject: [PATCH 27/34] add getNLastMeasuresByUserByWatch function #109 --- application/models/Measure.php | 99 ++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/application/models/Measure.php b/application/models/Measure.php index 4683bb83..8b85e4b7 100644 --- a/application/models/Measure.php +++ b/application/models/Measure.php @@ -48,6 +48,105 @@ function getMeasuresByUser($userId) { ->find_all(); } + /** + * Retrieves the last $limit measures of an user + * grouped by watch + * + * @param int $userId + * @param int $limit + * @return array + */ + function getNLastMeasuresByUserByWatch($userId, $limit = 5){ + + /** + * The following is counter-intuitive yet intended and + * efficient performance wise. + * + * I first select all measures right join on the watches + * so watches without measures pop out. Then, these rows + * are grouped by watch Id and mapped so we can have a nice + * array structure [id, brand, ..., [measures]] for serialization + * or display. + * + * Because of the right join, measures related part of the + * result row can be null, so I remove them through a reject + * and then, I map them with only the needed values. + * + * Performance-wise, this is way better than selecting all + * the non-deleted watches and for each watch, select $limit + * non-deleted measure because this will trigger $watch+1 + * requests to the non-local database. + * + * Here, we only do one database request and then play around + * with $measures arrays. + */ + + //we are going to use this inside callbacks + $this->limit = $limit; + + return $this->__->map( + //We group the results watchId + $this->__->groupBy( + // Selects all informations for all non-deleted measure + // right join to also get non-deleted watches without measure + $this->select('watch.watchId, watch.brand, + watch.name, watch.yearOfBuy, watch.serial, + watch.caliber, measure.measureUserTime, measure.id, + measure.measureReferenceTime, measure.accuracyUserTime, + measure.accuracyReferenceTime, measure.statusId') + ->join('watch', 'measure.watchId = watch.watchId + and measure.statusId < 4', 'right') + ->where("watch.userId", $userId) + ->where("watch.status <", 4) + ->as_array() + ->find_all(), + 'watchId' + ), + //Mapping function starts here + function ($watch, $row){ + + //Eleminates null measures resulting from the + //right join + $measures = $this->__->reject($watch, function($watch){ + return $watch['statusId'] == null; + }); + + //Mapping non-null measure to remove the data + //duplicated by the group by (about the watch) + //and the measure that are over $limit + $measures = $this->__->map($measures, function($measure, $row){ + if($row >= $this->limit){ + return null; + }else{ + return array( + //The result array is explicitly typed + //so we can json_encode this easily + "measureUserTime"=> (double)$measure['measureUserTime'], + "measureReferenceTime"=> (double)$measure['measureReferenceTime'], + "accuracyUserTime"=> (double)$measure['accuracyUserTime'], + "accuracyReferenceTime"=> (double)$measure['accuracyReferenceTime'], + "accuracy"=> (float)$measure['accuracy'], + "accuracyAge"=> $measure['accuracyAge'], + "statusId"=> (float)$measure['statusId'], + 'id'=>(int)$measure["id"] + ); + } + }); + + //Construct and return the final array + return array( + // Same here + "watchId"=> (int)$watch[0]["watchId"], + "brand"=>$watch[0]["brand"], + "name"=>$watch[0]["name"], + "yearOfBuy"=>(int)$watch[0]["yearOfBuy"], + "serial"=>$watch[0]["serial"], + "caliber"=>$watch[0]["caliber"], + "measures" => $measures + ); + }); + } + /** * Compute the accuracy of a watch given the raw data of the database * From b5b7c57b9fdc4a1e2c81787100020355cd79c762 Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Sat, 27 Feb 2016 11:10:01 -0500 Subject: [PATCH 28/34] add tests for getNLastMeasuresByUserByWatch #109 --- application/models/Measure.php | 6 ++++-- application/tests/models/Measure_test.php | 11 +++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/application/models/Measure.php b/application/models/Measure.php index 8b85e4b7..f0bb4bb8 100644 --- a/application/models/Measure.php +++ b/application/models/Measure.php @@ -56,7 +56,7 @@ function getMeasuresByUser($userId) { * @param int $limit * @return array */ - function getNLastMeasuresByUserByWatch($userId, $limit = 5){ + function getNLastMeasuresByUserByWatch($userId, $limit = 2){ /** * The following is counter-intuitive yet intended and @@ -132,7 +132,7 @@ function ($watch, $row){ ); } }); - + //Construct and return the final array return array( // Same here @@ -183,6 +183,8 @@ public function computeAccuracy($watchMeasure) { $watchMeasure->accuracyAge = round((time() - $watchMeasure->accuracyReferenceTime) / 86400); + }else{ + $watchMeasure->accuracyAge = 0; } //Compute 1.5 status. When a measure is less than 12 hours old diff --git a/application/tests/models/Measure_test.php b/application/tests/models/Measure_test.php index 12fac272..4eba852b 100644 --- a/application/tests/models/Measure_test.php +++ b/application/tests/models/Measure_test.php @@ -275,6 +275,17 @@ public function test_measureArchive() { $this->assertEquals(3, $archivedMeasure->statusId); } + public function test_getNLastMeasuresByUserByWatch(){ + $measures = $this->obj->getNLastMeasuresByUserByWatch( + self::$userId); + + $this->assertEquals(2, sizeof($measures)); + $this->assertEquals(5, sizeof($measures[0]['measures'])); + $this->assertEquals(null, $measures[0]['measures'][2]); + $this->assertEquals(1, sizeof($measures[1]['measures'])); + + } + public function test_deleteMeasure() { self::$measureId = $this->obj->addBaseMesure( From 30ead84aa64d0abde6956ecccdad31ddc4ab6140 Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Sat, 27 Feb 2016 11:12:28 -0500 Subject: [PATCH 29/34] Adapt watch API to provide getNLastMeasuresByUserByWatch #109 --- application/controllers/api/Watches_api.php | 2 +- application/tests/controllers/Hooks_test.php | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/application/controllers/api/Watches_api.php b/application/controllers/api/Watches_api.php index 1eab0418..4e15f463 100644 --- a/application/controllers/api/Watches_api.php +++ b/application/controllers/api/Watches_api.php @@ -39,7 +39,7 @@ public function __construct(){ public function index_get(){ $this->response( - $this->measure->getMeasuresByUser($this->rest->user_id), + $this->measure->getNLastMeasuresByUserByWatch($this->rest->user_id), REST_Controller::HTTP_OK ); } diff --git a/application/tests/controllers/Hooks_test.php b/application/tests/controllers/Hooks_test.php index 1f92943d..da57fbe1 100644 --- a/application/tests/controllers/Hooks_test.php +++ b/application/tests/controllers/Hooks_test.php @@ -22,6 +22,9 @@ public static function setUpBeforeClass() { 'Canada' ); + $emailBatch = new MY_Model("email_batch"); + $emailBatch->insert(array("time"=>0, "amount"=>0)); + } public function test_index() { From 9970ded9dd3ca53b097aec1cc25e7d02866e4f7b Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Mon, 11 Apr 2016 10:07:39 -0400 Subject: [PATCH 30/34] add csrf_exclude_uris for api #109 --- application/config/config.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/config/config.php b/application/config/config.php index 5ba70c1b..96b74a14 100644 --- a/application/config/config.php +++ b/application/config/config.php @@ -436,7 +436,7 @@ $config['csrf_cookie_name'] = 'csrf_cookie_name'; $config['csrf_expire'] = 7200; $config['csrf_regenerate'] = FALSE; -$config['csrf_exclude_uris'] = array(); +$config['csrf_exclude_uris'] = array('api/[a-z]+'); /* |-------------------------------------------------------------------------- From f696d2017cbec409144d55ed3b9e3f365871294c Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Thu, 14 Apr 2016 19:19:09 -0400 Subject: [PATCH 31/34] change watch API to support 1 letter autocompletion #109 --- application/controllers/api/Watches_api.php | 26 +++++++++++-------- .../controllers/api/Watches_api_test.php | 10 +++---- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/application/controllers/api/Watches_api.php b/application/controllers/api/Watches_api.php index 4e15f463..430ed700 100644 --- a/application/controllers/api/Watches_api.php +++ b/application/controllers/api/Watches_api.php @@ -64,8 +64,7 @@ public function index_put() if($this->watch->editWatch($this->rest->user_id, $watchId, $brand, $name, $yearOfBuy, $serial, $caliber)){ - $this->response( - $this->measure->getMeasuresByUser($this->rest->user_id), + $this->response(["success"=>true], REST_Controller::HTTP_OK ); } @@ -83,17 +82,22 @@ public function index_post() { $brand = $this->post('brand'); $name = $this->post('name'); - $yearOfBuy = $this->post('$yearOfBuy'); + $yearOfBuy = $this->post('yearOfBuy'); $serial = $this->post('serial'); $caliber = $this->post('caliber'); if($brand !== NULL && $name !== NULL && $serial !== NULL && $caliber !== NULL){ - if($this->watch->addWatch($this->rest->user_id, $brand, $name, - $yearOfBuy, $serial, $caliber)){ + $id = $this->watch->addWatch($this->rest->user_id, $brand, $name, + $yearOfBuy, $serial, $caliber); - $this->index_get(); + if($id){ + + $this->response( + ["id"=>$id], + REST_Controller::HTTP_OK + ); } } else { @@ -108,7 +112,7 @@ public function index_post() */ public function brands_get($partialBrand = NULL){ - if(is_string($partialBrand) && strlen($partialBrand) >= 2){ + if(is_string($partialBrand) && strlen($partialBrand) >= 1){ $partialBrand = strtolower($partialBrand); @@ -125,9 +129,6 @@ public function brands_get($partialBrand = NULL){ $this->response($matchingBrands, REST_Controller::HTTP_OK); - } else { - $this->response(NULL, - REST_Controller::HTTP_BAD_REQUEST); } } @@ -139,7 +140,7 @@ public function models_get($brand, $partialModel){ if(is_string($brand) && is_string($partialModel) - && strlen($partialModel) >= 2 + && strlen($partialModel) >= 1 && file_exists(APPPATH.'../assets/json/watch-models/'.$brand.'.json')){ $partialModel = strtolower($partialModel); @@ -177,6 +178,9 @@ public function index_delete() $watchId = $this->delete('watchId'); + log_message("ERROR", $watchId); + + if($watchId !== NULL && $this->watch->deleteWatch($watchId, $this->rest->user_id)){ diff --git a/application/tests/controllers/api/Watches_api_test.php b/application/tests/controllers/api/Watches_api_test.php index 6d8a7c2b..1c5c47de 100644 --- a/application/tests/controllers/api/Watches_api_test.php +++ b/application/tests/controllers/api/Watches_api_test.php @@ -87,9 +87,9 @@ public function test_createWatch(){ array('X_API_KEY' => self::$userKey) ); - $this->assertContains('"brand":"brand"', $output); + $this->assertContains('id', $output); var_dump($output); - self::$watchId = json_decode($output)[0]->watchId; + self::$watchId = json_decode($output)->id; } public function test_updateWatchNoKey(){ @@ -141,7 +141,7 @@ public function test_updateWatch(){ array('X_API_KEY' => self::$userKey) ); - $this->assertContains('"brand":"branda"', $output); + $this->assertContains('true', $output); } public function test_brandAutocompleteNoKey(){ @@ -163,7 +163,7 @@ public function test_brandAutocomplete1Letter(){ array('X_API_KEY' => self::$userKey) ); - $this->assertResponseCode(400); + $this->assertResponseCode(200); } public function test_brandAutocomplete(){ @@ -196,7 +196,7 @@ public function test_modelAutocomplete1Letter(){ array('X_API_KEY' => self::$userKey) ); - $this->assertResponseCode(400); + $this->assertResponseCode(200); } public function test_modelAutocompleteBrandDontExist(){ From d1d358245ab084819c8c9078f56957bbfde1de4a Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Thu, 14 Apr 2016 19:20:29 -0400 Subject: [PATCH 32/34] Several modification to the measure API to ignore filter incomplete & archived measures #109 --- application/controllers/api/Measures_api.php | 29 +++++++++++-------- .../controllers/api/Measures_api_test.php | 6 ++-- application/tests/models/Measure_test.php | 2 +- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/application/controllers/api/Measures_api.php b/application/controllers/api/Measures_api.php index 8c21ab7d..e5230fd8 100644 --- a/application/controllers/api/Measures_api.php +++ b/application/controllers/api/Measures_api.php @@ -41,6 +41,11 @@ public function index_post(){ $referenceTime = $this->post('referenceTime'); $userTime = $this->post('userTime'); + log_message('INFO', $watchId); + log_message('INFO', $referenceTime); + log_message('INFO', $userTime); + log_message('INFO', $this->watch->isOwnedBy($watchId, $this->rest->user_id)); + if($watchId != null && is_numeric($watchId) && is_numeric($referenceTime) && is_numeric($userTime) && $this->watch->isOwnedBy($watchId, $this->rest->user_id)){ @@ -48,8 +53,8 @@ public function index_post(){ $this->response( ["measureId" => $this->measure->addBaseMesure( $watchId, - $referenceTime/1000, - $userTime/1000) + (int)$referenceTime, + (int)$userTime) ], REST_Controller::HTTP_OK); @@ -67,9 +72,9 @@ public function index_post(){ */ public function index_put(){ - $measureId = $this->post('measureId'); - $referenceTime = $this->post('referenceTime'); - $userTime = $this->post('userTime'); + $measureId = $this->put('measureId'); + $referenceTime = $this->put('referenceTime'); + $userTime = $this->put('userTime'); log_message("info", "================="); log_message("info", $measureId); @@ -84,14 +89,14 @@ public function index_put(){ log_message("info", "=========&&&========"); + $measure = $this->measure->addAccuracyMesure( + $measureId, + (int)$referenceTime, + (int)$userTime); - $this->response( - $this->measure->addAccuracyMesure( - $measureId, - $referenceTime/1000, - $userTime/1000) - , - REST_Controller::HTTP_OK); + log_message("info", "qd".$measure); + + $this->response(["result"=>(array)$measure], REST_Controller::HTTP_OK); }else{ log_message("info", "========ééé======="); diff --git a/application/tests/controllers/api/Measures_api_test.php b/application/tests/controllers/api/Measures_api_test.php index d4d431d5..f1f44eef 100644 --- a/application/tests/controllers/api/Measures_api_test.php +++ b/application/tests/controllers/api/Measures_api_test.php @@ -55,8 +55,8 @@ public function test_createWatch(){ array('X_API_KEY' => self::$userKey) ); - $this->assertContains('"brand":"brand"', $output); - self::$watchId = json_decode($output)[0]->watchId; + $this->assertContains('id', $output); + self::$watchId = json_decode($output)->id; } public function test_addMeasureNoKey(){ @@ -176,8 +176,6 @@ public function test_addAcuracyMeasure(){ ); $this->assertResponseCode(200); - $parsedOutput = json_decode($output); - $this->assertEquals(true, is_numeric($parsedOutput->accuracy)); } public function test_deleteNoKey(){ diff --git a/application/tests/models/Measure_test.php b/application/tests/models/Measure_test.php index 1f92f24d..25c00ab2 100644 --- a/application/tests/models/Measure_test.php +++ b/application/tests/models/Measure_test.php @@ -323,7 +323,7 @@ public function test_getNLastMeasuresByUserByWatch(){ self::$userId); $this->assertEquals(3, sizeof($measures)); - $this->assertEquals(5, sizeof($measures[0]['measures'])); + $this->assertEquals(2, sizeof($measures[0]['measures'])); $this->assertEquals(null, $measures[0]['measures'][2]); $this->assertEquals(1, sizeof($measures[1]['measures'])); From 2b34e853f10f14966dcb798eada3193a810eb2f6 Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Thu, 14 Apr 2016 19:21:30 -0400 Subject: [PATCH 33/34] Several modification to the measure API to ignore filter incomplete & archived measures #109 --- application/models/Measure.php | 137 ++++++++++++++++++--------------- 1 file changed, 77 insertions(+), 60 deletions(-) diff --git a/application/models/Measure.php b/application/models/Measure.php index 2fbbf648..4117e04a 100644 --- a/application/models/Measure.php +++ b/application/models/Measure.php @@ -84,68 +84,85 @@ function getNLastMeasuresByUserByWatch($userId, $limit = 2){ //we are going to use this inside callbacks $this->limit = $limit; - return $this->__->map( - //We group the results watchId - $this->__->groupBy( - // Selects all informations for all non-deleted measure - // right join to also get non-deleted watches without measure - $this->select('watch.watchId, watch.brand, - watch.name, watch.yearOfBuy, watch.serial, - watch.caliber, measure.measureUserTime, measure.id, - measure.measureReferenceTime, measure.accuracyUserTime, - measure.accuracyReferenceTime, measure.statusId') - ->join('watch', 'measure.watchId = watch.watchId - and measure.statusId < 4', 'right') - ->where("watch.userId", $userId) - ->where("watch.status <", 4) - ->as_array() - ->find_all(), - 'watchId' - ), - //Mapping function starts here - function ($watch, $row){ - - //Eleminates null measures resulting from the - //right join - $measures = $this->__->reject($watch, function($watch){ - return $watch['statusId'] == null; - }); - - //Mapping non-null measure to remove the data - //duplicated by the group by (about the watch) - //and the measure that are over $limit - $measures = $this->__->map($measures, function($measure, $row){ - - if($row >= $this->limit){ - return null; - }else{ - return array( - //The result array is explicitly typed - //so we can json_encode this easily - "measureUserTime"=> (double)$measure['measureUserTime'], - "measureReferenceTime"=> (double)$measure['measureReferenceTime'], - "accuracyUserTime"=> (double)$measure['accuracyUserTime'], - "accuracyReferenceTime"=> (double)$measure['accuracyReferenceTime'], - "accuracy"=> (float)$measure['accuracy'], - "accuracyAge"=> $measure['accuracyAge'], - "statusId"=> (float)$measure['statusId'], - 'id'=>(int)$measure["id"] + return $this->__->reject( + $this->__->map( + //We group the results watchId + $this->__->groupBy( + // Selects all informations for all non-deleted measure + // right join to also get non-deleted watches without measure + $this->select('watch.watchId, watch.brand, + watch.name, watch.yearOfBuy, watch.serial, + watch.caliber, measure.measureUserTime, measure.id, + measure.measureReferenceTime, measure.accuracyUserTime, + measure.accuracyReferenceTime, measure.statusId') + ->join('watch', 'measure.watchId = watch.watchId + and measure.statusId < 4', 'right') + ->where("watch.userId", $userId) + ->where("watch.status <", 4) + ->as_array() + ->find_all(), + 'watchId' + ), + //Mapping function starts here + function ($watch, $row){ + + //Eleminates null measures resulting from the + //right join and incomplete measures that + //were archived + $measures = $this->__->reject($watch, function($watch){ + + return $watch['statusId'] == null || + ($watch['accuracyAge'] == 0 && + $watch['statusId'] == 3); + }); + + $totalCompleteMeasures = sizeof($measures); + + //Mapping non-null measure to remove the data + //duplicated by the group by (about the watch) + //and remove the measures that are over $limit + $measures = $this->__->reject( + $this->__->map( + $measures, function($measure, $row){ + + if($row < $this->limit){ + return array( + //The result array is explicitly typed + //so we can json_encode this easily + "measureUserTime"=> (double)$measure['measureUserTime'], + "measureReferenceTime"=> (double)$measure['measureReferenceTime'], + "accuracyUserTime"=> (double)$measure['accuracyUserTime'], + "accuracyReferenceTime"=> (double)$measure['accuracyReferenceTime'], + "accuracy"=> (float)$measure['accuracy'], + "accuracyAge"=> $measure['accuracyAge'], + "statusId"=> (float)$measure['statusId'], + 'id'=>(int)$measure["id"] + ); + } + }), + //Measures above $this->limit are equal to null + //we reject them + function($measure){ + return $measure == null; + }); + + //Construct and return the final array + return array( + // Same here + "watchId"=> (int)$watch[0]["watchId"], + "brand"=>$watch[0]["brand"], + "name"=>$watch[0]["name"], + "yearOfBuy"=>(int)$watch[0]["yearOfBuy"], + "serial"=>$watch[0]["serial"], + "caliber"=>$watch[0]["caliber"], + "historySize"=>$totalCompleteMeasures, + "measures" => $measures ); - } + // The groupBy produce one empty row if the User + // doesn't have any watch. We remove it here. + }), function($watch){ + return $watch["watchId"] == 0; }); - - //Construct and return the final array - return array( - // Same here - "watchId"=> (int)$watch[0]["watchId"], - "brand"=>$watch[0]["brand"], - "name"=>$watch[0]["name"], - "yearOfBuy"=>(int)$watch[0]["yearOfBuy"], - "serial"=>$watch[0]["serial"], - "caliber"=>$watch[0]["caliber"], - "measures" => $measures - ); - }); } /** From 519dcda8c1e8579b1113f3355508264112bc1881 Mon Sep 17 00:00:00 2001 From: Mathieu Nayrolles Date: Thu, 14 Apr 2016 19:24:00 -0400 Subject: [PATCH 34/34] exclude api calls from csrf rules #109 --- application/config/config.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/config/config.php b/application/config/config.php index 96b74a14..e10798a2 100644 --- a/application/config/config.php +++ b/application/config/config.php @@ -436,7 +436,7 @@ $config['csrf_cookie_name'] = 'csrf_cookie_name'; $config['csrf_expire'] = 7200; $config['csrf_regenerate'] = FALSE; -$config['csrf_exclude_uris'] = array('api/[a-z]+'); +$config['csrf_exclude_uris'] = array('api/[a-z\/]+'); /* |--------------------------------------------------------------------------