diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..5d609ac7 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @chriskacerguis diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index a7b75f10..00000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -github: chriskacerguis \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..34ee3d3c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,35 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Please provide either a cleanly formatted code snippet or a link to repo / gist with code that I can use to reproduce: + +```php + public function set_response($data = null, $http_code = null) + { + $this->response($data, $http_code, true); + } +``` + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots / Error Messages** +If applicable, add screenshots and/or error messages to help explain your problem. + +**Environment (please complete the following information):** + - PHP Version: [e.g. 7.2.1] + - CodeIgniter Version [e.g. 4.0.1] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/README.md b/README.md index f7f4e305..59776fe9 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # CodeIgniter RestServer -[![StyleCI](https://github.styleci.io/repos/230589/shield?branch=master)](https://github.styleci.io/repos/230589) +A fully RESTful server implementation for CodeIgniter 3 using one library, one config file and one controller. -A fully RESTful server implementation for CodeIgniter using one library, one config file and one controller. +> [!IMPORTANT] +> I have published the first "beta" of codeigniter-restserver 4. See the "development" branch. Please be sure to note the system requirments. ## Requirements @@ -108,3 +109,56 @@ class Api extends RestController { } } ``` + +## Extending supported formats + +If you need to be able to support more formats for replies, you can extend the +`Format` class to add the required `to_...` methods + +1. Extend the `RestController` class (in `libraries/MY_REST_Controller.php`) +```php +format = new Format(); + } +} +``` + +2. Extend the `Format` class (can be created as a CodeIgniter library in `libraries/Format.php`). +Following is an example to add support for PDF output + +```php +_data; + } + + if (is_array($data) || substr($data, 0, 4) != '%PDF') { + $html = $this->to_html($data); + + // Use your PDF lib of choice. For example mpdf + $mpdf = new \Mpdf\Mpdf(); + $mpdf->WriteHTML($html); + return $mpdf->Output('', 'S'); + } + + return $data; + } +} +``` diff --git a/src/Format.php b/src/Format.php index ce5b2de6..4c55a430 100644 --- a/src/Format.php +++ b/src/Format.php @@ -11,7 +11,7 @@ * Help convert between various formats such as XML, JSON, CSV, etc. * * @author Phil Sturgeon, Chris Kacerguis, @softwarespot - * @license http://www.dbad-license.org/ + * @license MIT (See LICENSE) */ class Format { @@ -183,7 +183,6 @@ public function to_xml($data = null, $structure = null, $basenode = 'xml') } foreach ($data as $key => $value) { - //change false/true to 0/1 if (is_bool($value)) { $value = (int) $value; @@ -216,7 +215,7 @@ public function to_xml($data = null, $structure = null, $basenode = 'xml') $this->to_xml($value, $node, $key); } else { // add single node. - $value = htmlspecialchars(html_entity_decode($value, ENT_QUOTES, 'UTF-8'), ENT_QUOTES, 'UTF-8'); + $value = htmlspecialchars(html_entity_decode($value ?? '', ENT_QUOTES, 'UTF-8'), ENT_QUOTES, 'UTF-8'); $structure->addChild($key, $value); } diff --git a/src/RestController.php b/src/RestController.php index 2147f83b..7f292a98 100644 --- a/src/RestController.php +++ b/src/RestController.php @@ -3,6 +3,8 @@ namespace chriskacerguis\RestServer; use Exception; +use RecursiveArrayIterator; +use RecursiveIteratorIterator; use stdClass; defined('BASEPATH') or exit('No direct script access allowed'); @@ -11,7 +13,7 @@ * CodeIgniter Rest Controller * A fully RESTful server implementation for CodeIgniter using one library, one config file and one controller. * - * @link https://github.com/chriskacerguis/ci-restserver + * @link https://github.com/chriskacerguis/codeigniter-restserver * * @version 4.0.0 */ @@ -32,6 +34,11 @@ class RestController extends \CI_Controller */ protected $methods = []; + /** + * Defines https status. + */ + protected $http_status = []; + /** * List of allowed HTTP methods. * @@ -225,7 +232,7 @@ class RestController extends \CI_Controller /** * @var Format */ - private $format; + protected $format; /** * @var bool @@ -433,8 +440,8 @@ private function do_auth($method = false) return true; } - if (file_exists(__DIR__.'/auth-'.$method.'.php')) { - include __DIR__.'/auth-'.$method.'.php'; + if (file_exists(__DIR__.'/auth/'.$method.'.php')) { + include __DIR__.'/auth/'.$method.'.php'; } } @@ -443,11 +450,15 @@ private function do_auth($method = false) */ private function get_local_config($config_file) { - if (!$this->load->config($config_file, false)) { - $config = []; - include __DIR__.'/'.$config_file.'.php'; - foreach ($config as $key => $value) { - $this->config->set_item($key, $value); + if (file_exists(APPPATH.'config/'.$config_file.'.php')) { + $this->load->config($config_file, false); + } else { + if (file_exists(__DIR__.'/'.$config_file.'.php')) { + $config = []; + include __DIR__.'/'.$config_file.'.php'; + foreach ($config as $key => $value) { + $this->config->set_item($key, $value); + } } } } @@ -625,10 +636,17 @@ public function response($data = null, $http_code = null, $continue = false) // 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(Format::class, 'to_'.$this->response->format)) { + $formatter = null; + if ($this->format && method_exists($this->format, 'to_'.$this->response->format)) { + $formatter = $this->format::factory($data); + } elseif (method_exists(Format::class, 'to_'.$this->response->format)) { + $formatter = Format::factory($data); + } + + if ($formatter !== null) { // CORB protection // First, get the output content. - $output = Format::factory($data)->{'to_'.$this->response->format}(); + $output = $formatter->{'to_'.$this->response->format}(); // Set the format header // Then, check if the client asked for a callback, and if the output contains this callback : @@ -823,7 +841,9 @@ protected function _detect_method() $method = $this->input->server('HTTP_X_HTTP_METHOD_OVERRIDE'); } - $method = strtolower($method); + if ($method !== null) { + $method = strtolower($method); + } } if (empty($method)) { @@ -853,12 +873,16 @@ protected function _detect_api_key() $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 ($key = isset($this->_args[$api_key_variable]) ? $this->_args[$api_key_variable] : $this->input->server($key_name)) { + $this->rest->key = $key; + 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')}; + if ($this->config->item('rest_keys_expire') === true && $row->{$this->config->item('rest_keys_expiry_column')} < time()) { + return false; + } isset($row->user_id) && $this->rest->user_id = $row->user_id; isset($row->level) && $this->rest->level = $row->level; @@ -942,15 +966,17 @@ 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, - ]); + $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(); @@ -1400,6 +1426,10 @@ public function head($key = null, $xss_clean = null) public function post($key = null, $xss_clean = null) { if ($key === null) { + foreach (new RecursiveIteratorIterator(new RecursiveArrayIterator($this->_post_args), RecursiveIteratorIterator::CATCH_GET_CHILD) as $key => $value) { + $this->_post_args[$key] = $this->_xss_clean($this->_post_args[$key], $xss_clean); + } + return $this->_post_args; } @@ -1702,6 +1732,9 @@ protected function _check_php_session() $this->_check_whitelist_auth(); } + // Load library session of CodeIgniter + $this->load->library('session'); + // Get the auth_source config item $key = $this->config->item('auth_source'); @@ -1782,12 +1815,13 @@ protected function _prepare_digest_auth() $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 - if (isset($digest['username']) === false || $this->_check_login($digest['username'], true) === false) { + $username = $this->_check_login($digest['username'], true); + if (isset($digest['username']) === false || $username === false) { $this->_force_login($unique_id); } $md5 = md5(strtoupper($this->request->method).':'.$digest['uri']); - $valid_response = md5($digest['username'].':'.$digest['nonce'].':'.$digest['nc'].':'.$digest['cnonce'].':'.$digest['qop'].':'.$md5); + $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) { @@ -1864,7 +1898,8 @@ protected function _force_login($nonce = '') header( 'WWW-Authenticate: Digest realm="'.$rest_realm .'", qop="auth", nonce="'.$nonce - .'", opaque="'.md5($rest_realm).'"'); + .'", opaque="'.md5($rest_realm).'"' + ); } if ($this->config->item('strict_api_and_auth') === true) { @@ -1894,9 +1929,12 @@ 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, - ]); + $this->config->item('rest_logs_table'), + $payload, + [ + 'id' => $this->_insert_id, + ] + ); } /** @@ -1917,9 +1955,12 @@ 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, - ]); + $this->config->item('rest_logs_table'), + $payload, + [ + 'id' => $this->_insert_id, + ] + ); } /** @@ -1936,10 +1977,12 @@ protected function _check_access() // Fetch controller based on path and controller name $controller = implode( - '/', [ - $this->router->directory, - $this->router->class, - ]); + '/', + [ + $this->router->directory, + $this->router->class, + ] + ); // Remove any double slashes for safety $controller = str_replace('//', '/', $controller); diff --git a/src/rest.php b/src/rest.php index 887141c6..7c8c4c9b 100644 --- a/src/rest.php +++ b/src/rest.php @@ -319,9 +319,24 @@ | `is_private_key` TINYINT(1) NOT NULL DEFAULT '0', | `ip_addresses` TEXT NULL DEFAULT NULL, | `date_created` INT(11) NOT NULL, +| `expires` INT(11) NOT NULL | PRIMARY KEY (`id`) | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; | +| For PostgreSQL +| CREATE TABLE keys ( +| id SERIAL, +| user_id INT NOT NULL, +| key VARCHAR(40) NOT NULL, +| level INT NOT NULL, +| ignore_limits SMALLINT NOT NULL DEFAULT '0', +| is_private_key SMALLINT NOT NULL DEFAULT '0', +| ip_addresses TEXT NULL DEFAULT NULL, +| date_created INT NOT NULL, +| expires INT NOT NULL, +| PRIMARY KEY (id) +| ) ; +| | */ $config['rest_enable_keys'] = false; @@ -335,6 +350,19 @@ | */ $config['rest_key_column'] = 'key'; +/* +|-------------------------------------------------------------------------- +| REST Table Key Expiry Config and Column Name +|-------------------------------------------------------------------------- +| +| Configure wether or not api keys should expire, and the column name to +| match e.g. expires +| Note: the value in the column will be treated as a unix timestamp and +| compared with php function time() +| +*/ +$config['rest_keys_expire'] = false; +$config['rest_keys_expiry_column'] = 'expires'; /* |-------------------------------------------------------------------------- @@ -402,6 +430,20 @@ | PRIMARY KEY (`id`) | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; | +| For PostgreSQL +| CREATE TABLE logs ( +| id SERIAL, +| 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 NOT NULL, +| rtime DOUBLE PRECISION DEFAULT NULL, +| authorized boolean NOT NULL, +| response_code smallint DEFAULT '0', +| PRIMARY KEY (id) +| ) ; */ $config['rest_enable_logging'] = false; @@ -435,6 +477,31 @@ | PRIMARY KEY (`id`) | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; | +| For PostgreSQL +| CREATE TABLE access ( +| id SERIAL, +| key VARCHAR(40) NOT NULL DEFAULT '', +| all_access SMALLINT NOT NULL DEFAULT '0', +| controller VARCHAR(50) NOT NULL DEFAULT '', +| date_created TIMESTAMP(0) DEFAULT NULL, +| date_modified TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, +| PRIMARY KEY (id) +| ) ; +| CREATE OR REPLACE FUNCTION upd_timestamp() RETURNS TRIGGER +| LANGUAGE plpgsql +| AS +| $$ +| BEGIN +| NEW.modified = CURRENT_TIMESTAMP; +| RETURN NEW; +| END; +| $$; +| CREATE TRIGGER trigger_access +| BEFORE UPDATE +| ON access +| FOR EACH ROW +| EXECUTE PROCEDURE upd_timestamp(); +| */ $config['rest_enable_access'] = false; @@ -479,6 +546,16 @@ | PRIMARY KEY (`id`) | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; | +| For PostgreSQL +| CREATE TABLE limits ( +| id SERIAL, +| uri VARCHAR(255) NOT NULL, +| count INT NOT NULL, +| hour_started INT NOT NULL, +| api_key VARCHAR(40) NOT NULL, +| PRIMARY KEY (id) +| ) ; +| | To specify the limits within the controller's __construct() method, add per-method | limits with: | @@ -556,11 +633,11 @@ | */ $config['allowed_cors_headers'] = [ - 'Origin', - 'X-Requested-With', - 'Content-Type', - 'Accept', - 'Access-Control-Request-Method', + 'Origin', + 'X-Requested-With', + 'Content-Type', + 'Accept', + 'Access-Control-Request-Method', ]; /* @@ -572,12 +649,12 @@ | */ $config['allowed_cors_methods'] = [ - 'GET', - 'POST', - 'OPTIONS', - 'PUT', - 'PATCH', - 'DELETE', + 'GET', + 'POST', + 'OPTIONS', + 'PUT', + 'PATCH', + 'DELETE', ]; /*