diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..0f844cab --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: alixaxel +custom: https://paypal.me/alixaxel/10USD diff --git a/.htaccess b/.htaccess deleted file mode 100644 index 2ac8058d..00000000 --- a/.htaccess +++ /dev/null @@ -1,6 +0,0 @@ - -RewriteEngine on -RewriteCond %{REQUEST_FILENAME} !-f -RewriteCond %{REQUEST_FILENAME} !-d -RewriteRule ^(.*)$ index.php/$1 [L] - \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..1b0003fb --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2014 Alix Axel + +Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 09ad167c..a1ea391e 100644 --- a/README.md +++ b/README.md @@ -1,95 +1,119 @@ -#Arrest MySQL +#ArrestDB -Arrest MySQL is a "plug-n-play" RESTful API for your MySQL database. Arrest MySQL provides a REST API that maps directly to your database stucture with no configuation. +ArrestDB is a "plug-n-play" RESTful API for SQLite, MySQL and PostgreSQL databases. -For example lets suppose you have set up Arrest MySQL at http://api.example.com and your database has a table in it called "customers". To get a list of customers you would simply need to do: +ArrestDB provides a REST API that maps directly to your database stucture with no configuation. -```GET http://api.example.com/customers``` +Lets suppose you have set up ArrestDB at `http://api.example.com/` and that your database has a table named `customers`. +To get a list of all the customers in the table you would simply need to do: -Where "customers" is the table name. As a response you would get a JSON formatted list of customers. Or say you only want to get one customer, then you would do this: + GET http://api.example.com/customers/ -```GET http://api.example.com/customers/123``` +As a response, you would get a JSON formatted list of customers. -Where "123" here is the ID of the customer. For more information on using Arrest MySQL see the Usage section below. +Or, if you only want to get one customer, then you would append the customer `id` to the URL: + + GET http://api.example.com/customers/123/ ##Requirements -1. Apache Server with PHP 5+ -2. MySQL 5+ +- PHP 5.4+ & PDO +- SQLite / MySQL / PostgreSQL -##30 Second Installation +##Installation -Simply put these files into a folder somewhere on your server. Then edit config.php and fill in your database details and you are good to go. +Edit `index.php` and change the `$dsn` variable located at the top, here are some examples: -##Usage +- SQLite: `$dsn = 'sqlite://./path/to/database.sqlite';` +- MySQL: `$dsn = 'mysql://[user[:pass]@]host[:port]/db/;` +- PostgreSQL: `$dsn = 'pgsql://[user[:pass]@]host[:port]/db/;` -If you edit index.php you will see how incredibly simple it is to set up Arrest MySQL. Note that you are left to **provide your own authentication** for your API when using Arrest MySQL. +If you want to restrict access to allow only specific IP addresses, add them to the `$clients` array: ```php -set_table_index('my_table', 'some_index'); - - $arrest->rest(); - -} catch (Exception $e) { - echo $e; -} -?> +$clients = array +( + '127.0.0.1', + '127.0.0.2', + '127.0.0.3', +); ``` -###API Design +After you're done editing the file, place it in a public directory (feel free to change the filename). -The actual API design is very straight forward and follows the design patterns of most other API's. +If you're using Apache, you can use the following `mod_rewrite` rules in a `.htaccess` file: -``` -create > POST /table -read > GET /table[/id] -update > PUT /table/id -delete > DELETE /table/id +```apache + + RewriteEngine On + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^(.*)$ index.php/$1 [L,QSA] + ``` -To put this into practice below are some example of how you would use an Arrest MySQL API: +***Nota bene:*** You must access the file directly, including it from another file won't work. -``` -// Get all rows from the "customers" table -GET http://api.example.com/customers -// Get a single row from the "customers" table (where "123" is the ID) -GET http://api.example.com/customers/123 -// Get 50 rows from the "customers" table -GET http://api.example.com/customers?limit=50 -// Get 50 rows from the "customers" table ordered by the "date" field -GET http://api.example.com/customers?limit=50&order_by=date&order=desc - -// Create a new row in the "customers" table where the POST data corresponds to the database fields -POST http://api.example.com/customers - -// Update customer "123" in the "customers" table where the PUT data corresponds to the database fields -PUT http://api.example.com/customers/123 - -// Delete customer "123" from the "customers" table -DELETE http://api.example.com/customers/123 -``` +##API Design + +The actual API design is very straightforward and follows the design patterns of the majority of APIs. + + (C)reate > POST /table + (R)ead > GET /table[/id] + (R)ead > GET /table[/column/content] + (U)pdate > PUT /table/id + (D)elete > DELETE /table/id + +To put this into practice below are some example of how you would use the ArrestDB API: + + # Get all rows from the "customers" table + GET http://api.example.com/customers/ + + # Get a single row from the "customers" table (where "123" is the ID) + GET http://api.example.com/customers/123/ + + # Get all rows from the "customers" table where the "country" field matches "Australia" (`LIKE`) + GET http://api.example.com/customers/country/Australia/ + + # Get 50 rows from the "customers" table + GET http://api.example.com/customers/?limit=50 + + # Get 50 rows from the "customers" table ordered by the "date" field + GET http://api.example.com/customers/?limit=50&by=date&order=desc + + # Create a new row in the "customers" table where the POST data corresponds to the database fields + POST http://api.example.com/customers/ + + # Update customer "123" in the "customers" table where the PUT data corresponds to the database fields + PUT http://api.example.com/customers/123/ + + # Delete customer "123" from the "customers" table + DELETE http://api.example.com/customers/123/ + +Please note that `GET` calls accept the following query string variables: + +- `by` (column to order by) + - `order` (order direction: `ASC` or `DESC`) +- `limit` (`LIMIT x` SQL clause) + - `offset` (`OFFSET x` SQL clause) + +Additionally, `POST` and `PUT` requests accept JSON-encoded and/or zlib-compressed payloads. -###Responses +> `POST` and `PUT` requests are only able to parse data encoded in `application/x-www-form-urlencoded`. +> Support for `multipart/form-data` payloads will be added in the future. -All responses are in the JSON format. For example a GET response from the "customers" table might look like: +If your client does not support certain methods, you can use the `X-HTTP-Method-Override` header: + +- `PUT` = `POST` + `X-HTTP-Method-Override: PUT` +- `DELETE` = `GET` + `X-HTTP-Method-Override: DELETE` + +Alternatively, you can also override the HTTP method by using the `_method` query string parameter. + +Since 1.5.0, it's also possible to atomically `INSERT` a batch of records by POSTing an array of arrays. + +##Responses + +All responses are in the JSON format. A `GET` response from the `customers` table might look like this: ```json [ @@ -112,52 +136,73 @@ All responses are in the JSON format. For example a GET response from the "custo ] ``` -Successful POST, PUT, and DELETE responses will look like +Successful `POST` responses will look like: ```json { "success": { - "message": "Success", - "code": 200 + "code": 201, + "status": "Created" } } ``` -Errors are in the format: +Successful `PUT` and `DELETE` responses will look like: + +```json +{ + "success": { + "code": 200, + "status": "OK" + } +} +``` + +Errors are expressed in the format: ```json { "error": { - "message": "No Content", - "code": 204 + "code": 400, + "status": "Bad Request" } } ``` The following codes and message are avaiable: -* 200 Success -* 204 No Content -* 404 Not Found +* `200` OK +* `201` Created +* `204` No Content +* `400` Bad Request +* `403` Forbidden +* `404` Not Found +* `409` Conflict +* `503` Service Unavailable -##Credits +Also, if the `callback` query string is set *and* is valid, the returned result will be a [JSON-P response](http://en.wikipedia.org/wiki/JSONP): -Arrest MySQL was created by [Gilbert Pellegrom](http://gilbert.pellegrom.me) from [Dev7studios](http://dev7studios.com). +```javascript +callback(JSON); +``` -Please contribute by [reporting bugs](Arrest-MySQL/issues) and submitting [pull requests](Arrest-MySQL/pulls). +Ajax-like requests will be minified, whereas normal browser requests will be human-readable. -##License (MIT) +##Changelog + +- **1.2.0** ~~support for JSON payloads in `POST` and `PUT` (optionally gzipped)~~ +- **1.3.0** ~~support for JSON-P responses~~ +- **1.4.0** ~~support for HTTP method overrides using the `X-HTTP-Method-Override` header~~ +- **1.5.0** ~~support for bulk inserts in `POST`~~ +- **1.6.0** ~~added support for PostgreSQL~~ +- **1.7.0** ~~fixed PostgreSQL connection bug, other minor improvements~~ +- **1.8.0** ~~fixed POST / PUT bug introduced in 1.5.0~~ +- **1.9.0** ~~updated to PHP 5.4 short array syntax~~ -Copyright © 2013 Dev7studios +##Credits -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation -files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, -modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following conditions: +ArrestDB is a complete rewrite of [Arrest-MySQL](https://github.com/gilbitron/Arrest-MySQL) with several optimizations and additional features. -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +##License (MIT) -THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Copyright (c) 2014 Alix Axel (alix.axel+github@gmail.com). \ No newline at end of file diff --git a/config.php b/config.php deleted file mode 100644 index e0812d9c..00000000 --- a/config.php +++ /dev/null @@ -1,11 +0,0 @@ - 'localhost', // Usually localhost - 'database' => '', // The database name - 'username' => '', // The database username - 'password' => '', // The database password - 'verbose' => false // If true errors will be shown -); - -?> \ No newline at end of file diff --git a/index.php b/index.php index c00d6d77..7d291f18 100644 --- a/index.php +++ b/index.php @@ -1,28 +1,578 @@ set_table_index('my_table', 'some_index'); - - $arrest->rest(); - -} catch (Exception $e) { - echo $e; +$dsn = ''; +$clients = []; + +/** +* The MIT License +* http://creativecommons.org/licenses/MIT/ +* +* ArrestDB 1.9.0 (github.com/alixaxel/ArrestDB/) +* Copyright (c) 2014 Alix Axel +**/ + +if (strcmp(PHP_SAPI, 'cli') === 0) +{ + exit('ArrestDB should not be run from CLI.' . PHP_EOL); +} + +if ((empty($clients) !== true) && (in_array($_SERVER['REMOTE_ADDR'], (array) $clients) !== true)) +{ + exit(ArrestDB::Reply(ArrestDB::$HTTP[403])); +} + +else if (ArrestDB::Query($dsn) === false) +{ + exit(ArrestDB::Reply(ArrestDB::$HTTP[503])); +} + +if (array_key_exists('_method', $_GET) === true) +{ + $_SERVER['REQUEST_METHOD'] = strtoupper(trim($_GET['_method'])); +} + +else if (array_key_exists('HTTP_X_HTTP_METHOD_OVERRIDE', $_SERVER) === true) +{ + $_SERVER['REQUEST_METHOD'] = strtoupper(trim($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])); +} + +ArrestDB::Serve('GET', '/(#any)/(#any)/(#any)', function ($table, $id, $data) +{ + $query = array + ( + sprintf('SELECT * FROM "%s"', $table), + sprintf('WHERE "%s" %s ?', $id, (ctype_digit($data) === true) ? '=' : 'LIKE'), + ); + + if (isset($_GET['by']) === true) + { + if (isset($_GET['order']) !== true) + { + $_GET['order'] = 'ASC'; + } + + $query[] = sprintf('ORDER BY "%s" %s', $_GET['by'], $_GET['order']); + } + + if (isset($_GET['limit']) === true) + { + $query[] = sprintf('LIMIT %u', $_GET['limit']); + + if (isset($_GET['offset']) === true) + { + $query[] = sprintf('OFFSET %u', $_GET['offset']); + } + } + + $query = sprintf('%s;', implode(' ', $query)); + $result = ArrestDB::Query($query, $data); + + if ($result === false) + { + $result = ArrestDB::$HTTP[404]; + } + + else if (empty($result) === true) + { + $result = ArrestDB::$HTTP[204]; + } + + return ArrestDB::Reply($result); +}); + +ArrestDB::Serve('GET', '/(#any)/(#num)?', function ($table, $id = null) +{ + $query = array + ( + sprintf('SELECT * FROM "%s"', $table), + ); + + if (isset($id) === true) + { + $query[] = sprintf('WHERE "%s" = ? LIMIT 1', 'id'); + } + + else + { + if (isset($_GET['by']) === true) + { + if (isset($_GET['order']) !== true) + { + $_GET['order'] = 'ASC'; + } + + $query[] = sprintf('ORDER BY "%s" %s', $_GET['by'], $_GET['order']); + } + + if (isset($_GET['limit']) === true) + { + $query[] = sprintf('LIMIT %u', $_GET['limit']); + + if (isset($_GET['offset']) === true) + { + $query[] = sprintf('OFFSET %u', $_GET['offset']); + } + } + } + + $query = sprintf('%s;', implode(' ', $query)); + $result = (isset($id) === true) ? ArrestDB::Query($query, $id) : ArrestDB::Query($query); + + if ($result === false) + { + $result = ArrestDB::$HTTP[404]; + } + + else if (empty($result) === true) + { + $result = ArrestDB::$HTTP[204]; + } + + else if (isset($id) === true) + { + $result = array_shift($result); + } + + return ArrestDB::Reply($result); +}); + +ArrestDB::Serve('DELETE', '/(#any)/(#num)', function ($table, $id) +{ + $query = array + ( + sprintf('DELETE FROM "%s" WHERE "%s" = ?', $table, 'id'), + ); + + $query = sprintf('%s;', implode(' ', $query)); + $result = ArrestDB::Query($query, $id); + + if ($result === false) + { + $result = ArrestDB::$HTTP[404]; + } + + else if (empty($result) === true) + { + $result = ArrestDB::$HTTP[204]; + } + + else + { + $result = ArrestDB::$HTTP[200]; + } + + return ArrestDB::Reply($result); +}); + +if (in_array($http = strtoupper($_SERVER['REQUEST_METHOD']), ['POST', 'PUT']) === true) +{ + if (preg_match('~^\x78[\x01\x5E\x9C\xDA]~', $data = file_get_contents('php://input')) > 0) + { + $data = gzuncompress($data); + } + + if ((array_key_exists('CONTENT_TYPE', $_SERVER) === true) && (empty($data) !== true)) + { + if (strncasecmp($_SERVER['CONTENT_TYPE'], 'application/json', 16) === 0) + { + $GLOBALS['_' . $http] = json_decode($data, true); + } + + else if ((strncasecmp($_SERVER['CONTENT_TYPE'], 'application/x-www-form-urlencoded', 33) === 0) && (strncasecmp($_SERVER['REQUEST_METHOD'], 'PUT', 3) === 0)) + { + parse_str($data, $GLOBALS['_' . $http]); + } + } + + if ((isset($GLOBALS['_' . $http]) !== true) || (is_array($GLOBALS['_' . $http]) !== true)) + { + $GLOBALS['_' . $http] = []; + } + + unset($data); } -?> \ No newline at end of file +ArrestDB::Serve('POST', '/(#any)', function ($table) +{ + if (empty($_POST) === true) + { + $result = ArrestDB::$HTTP[204]; + } + + else if (is_array($_POST) === true) + { + $queries = []; + + if (count($_POST) == count($_POST, COUNT_RECURSIVE)) + { + $_POST = [$_POST]; + } + + foreach ($_POST as $row) + { + $data = []; + + foreach ($row as $key => $value) + { + $data[sprintf('"%s"', $key)] = $value; + } + + $query = array + ( + sprintf('INSERT INTO "%s" (%s) VALUES (%s)', $table, implode(', ', array_keys($data)), implode(', ', array_fill(0, count($data), '?'))), + ); + + $queries[] = array + ( + sprintf('%s;', implode(' ', $query)), + $data, + ); + } + + if (count($queries) > 1) + { + ArrestDB::Query()->beginTransaction(); + + while (is_null($query = array_shift($queries)) !== true) + { + if (($result = ArrestDB::Query($query[0], $query[1])) === false) + { + ArrestDB::Query()->rollBack(); break; + } + } + + if (($result !== false) && (ArrestDB::Query()->inTransaction() === true)) + { + $result = ArrestDB::Query()->commit(); + } + } + + else if (is_null($query = array_shift($queries)) !== true) + { + $result = ArrestDB::Query($query[0], $query[1]); + } + + if ($result === false) + { + $result = ArrestDB::$HTTP[409]; + } + + else + { + $result = ArrestDB::$HTTP[201]; + } + } + + return ArrestDB::Reply($result); +}); + +ArrestDB::Serve('PUT', '/(#any)/(#num)', function ($table, $id) +{ + if (empty($GLOBALS['_PUT']) === true) + { + $result = ArrestDB::$HTTP[204]; + } + + else if (is_array($GLOBALS['_PUT']) === true) + { + $data = []; + + foreach ($GLOBALS['_PUT'] as $key => $value) + { + $data[$key] = sprintf('"%s" = ?', $key); + } + + $query = array + ( + sprintf('UPDATE "%s" SET %s WHERE "%s" = ?', $table, implode(', ', $data), 'id'), + ); + + $query = sprintf('%s;', implode(' ', $query)); + $result = ArrestDB::Query($query, $GLOBALS['_PUT'], $id); + + if ($result === false) + { + $result = ArrestDB::$HTTP[409]; + } + + else + { + $result = ArrestDB::$HTTP[200]; + } + } + + return ArrestDB::Reply($result); +}); + +exit(ArrestDB::Reply(ArrestDB::$HTTP[400])); + +class ArrestDB +{ + public static $HTTP = [ + 200 => [ + 'success' => [ + 'code' => 200, + 'status' => 'OK', + ], + ], + 201 => [ + 'success' => [ + 'code' => 201, + 'status' => 'Created', + ], + ], + 204 => [ + 'error' => [ + 'code' => 204, + 'status' => 'No Content', + ], + ], + 400 => [ + 'error' => [ + 'code' => 400, + 'status' => 'Bad Request', + ], + ], + 403 => [ + 'error' => [ + 'code' => 403, + 'status' => 'Forbidden', + ], + ], + 404 => [ + 'error' => [ + 'code' => 404, + 'status' => 'Not Found', + ], + ], + 409 => [ + 'error' => [ + 'code' => 409, + 'status' => 'Conflict', + ], + ], + 503 => [ + 'error' => [ + 'code' => 503, + 'status' => 'Service Unavailable', + ], + ], + ]; + + public static function Query($query = null) + { + static $db = null; + static $result = []; + + try + { + if (isset($db, $query) === true) + { + if (strncasecmp($db->getAttribute(\PDO::ATTR_DRIVER_NAME), 'mysql', 5) === 0) + { + $query = strtr($query, '"', '`'); + } + + if (empty($result[$hash = crc32($query)]) === true) + { + $result[$hash] = $db->prepare($query); + } + + $data = array_slice(func_get_args(), 1); + + if (count($data, COUNT_RECURSIVE) > count($data)) + { + $data = iterator_to_array(new \RecursiveIteratorIterator(new \RecursiveArrayIterator($data)), false); + } + + if ($result[$hash]->execute($data) === true) + { + $sequence = null; + + if ((strncmp($db->getAttribute(\PDO::ATTR_DRIVER_NAME), 'pgsql', 5) === 0) && (sscanf($query, 'INSERT INTO %s', $sequence) > 0)) + { + $sequence = sprintf('%s_id_seq', trim($sequence, '"')); + } + + switch (strstr($query, ' ', true)) + { + case 'INSERT': + case 'REPLACE': + return $db->lastInsertId($sequence); + + case 'UPDATE': + case 'DELETE': + return $result[$hash]->rowCount(); + + case 'SELECT': + case 'EXPLAIN': + case 'PRAGMA': + case 'SHOW': + return $result[$hash]->fetchAll(); + } + + return true; + } + + return false; + } + + else if (isset($query) === true) + { + $options = array + ( + \PDO::ATTR_CASE => \PDO::CASE_NATURAL, + \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC, + \PDO::ATTR_EMULATE_PREPARES => false, + \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, + \PDO::ATTR_ORACLE_NULLS => \PDO::NULL_NATURAL, + \PDO::ATTR_STRINGIFY_FETCHES => false, + ); + + if (preg_match('~^sqlite://([[:print:]]++)$~i', $query, $dsn) > 0) + { + $options += array + ( + \PDO::ATTR_TIMEOUT => 3, + ); + + $db = new \PDO(sprintf('sqlite:%s', $dsn[1]), null, null, $options); + $pragmas = array + ( + 'automatic_index' => 'ON', + 'cache_size' => '8192', + 'foreign_keys' => 'ON', + 'journal_size_limit' => '67110000', + 'locking_mode' => 'NORMAL', + 'page_size' => '4096', + 'recursive_triggers' => 'ON', + 'secure_delete' => 'ON', + 'synchronous' => 'NORMAL', + 'temp_store' => 'MEMORY', + 'journal_mode' => 'WAL', + 'wal_autocheckpoint' => '4096', + ); + + if (strncasecmp(PHP_OS, 'WIN', 3) !== 0) + { + $memory = 131072; + + if (($page = intval(shell_exec('getconf PAGESIZE'))) > 0) + { + $pragmas['page_size'] = $page; + } + + if (is_readable('/proc/meminfo') === true) + { + if (is_resource($handle = fopen('/proc/meminfo', 'rb')) === true) + { + while (($line = fgets($handle, 1024)) !== false) + { + if (sscanf($line, 'MemTotal: %d kB', $memory) == 1) + { + $memory = round($memory / 131072) * 131072; break; + } + } + + fclose($handle); + } + } + + $pragmas['cache_size'] = intval($memory * 0.25 / ($pragmas['page_size'] / 1024)); + $pragmas['wal_autocheckpoint'] = $pragmas['cache_size'] / 2; + } + + foreach ($pragmas as $key => $value) + { + $db->exec(sprintf('PRAGMA %s=%s;', $key, $value)); + } + } + + else if (preg_match('~^(mysql|pgsql)://(?:(.+?)(?::(.+?))?@)?([^/:@]++)(?::(\d++))?/(\w++)/?$~i', $query, $dsn) > 0) + { + if (strncasecmp($query, 'mysql', 5) === 0) + { + $options += array + ( + \PDO::ATTR_AUTOCOMMIT => true, + \PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES "utf8" COLLATE "utf8_general_ci", time_zone = "+00:00";', + \PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true, + ); + } + + $db = new \PDO(sprintf('%s:host=%s;port=%s;dbname=%s', $dsn[1], $dsn[4], $dsn[5], $dsn[6]), $dsn[2], $dsn[3], $options); + } + } + } + + catch (\Exception $exception) + { + return false; + } + + return (isset($db) === true) ? $db : false; + } + + public static function Reply($data) + { + $bitmask = 0; + $options = ['UNESCAPED_SLASHES', 'UNESCAPED_UNICODE']; + + if (empty($_SERVER['HTTP_X_REQUESTED_WITH']) === true) + { + $options[] = 'PRETTY_PRINT'; + } + + foreach ($options as $option) + { + $bitmask |= (defined('JSON_' . $option) === true) ? constant('JSON_' . $option) : 0; + } + + if (($result = json_encode($data, $bitmask)) !== false) + { + $callback = null; + + if (array_key_exists('callback', $_GET) === true) + { + $callback = trim(preg_replace('~[^[:alnum:]\[\]_.]~', '', $_GET['callback'])); + + if (empty($callback) !== true) + { + $result = sprintf('%s(%s);', $callback, $result); + } + } + + if (headers_sent() !== true) + { + header(sprintf('Content-Type: application/%s; charset=utf-8', (empty($callback) === true) ? 'json' : 'javascript')); + } + } + + return $result; + } + + public static function Serve($on = null, $route = null, $callback = null) + { + static $root = null; + + if (isset($_SERVER['REQUEST_METHOD']) !== true) + { + $_SERVER['REQUEST_METHOD'] = 'CLI'; + } + + if ((empty($on) === true) || (strcasecmp($_SERVER['REQUEST_METHOD'], $on) === 0)) + { + if (is_null($root) === true) + { + $root = preg_replace('~/++~', '/', substr($_SERVER['PHP_SELF'], strlen($_SERVER['SCRIPT_NAME'])) . '/'); + } + + if (preg_match('~^' . str_replace(['#any', '#num'], ['[^/]++', '[0-9]++'], $route) . '~i', $root, $parts) > 0) + { + return (empty($callback) === true) ? true : exit(call_user_func_array($callback, array_slice($parts, 1))); + } + } + + return false; + } +} diff --git a/lib/arrest-mysql.php b/lib/arrest-mysql.php deleted file mode 100644 index 2b870938..00000000 --- a/lib/arrest-mysql.php +++ /dev/null @@ -1,399 +0,0 @@ - - * $arrest = new ArrestMySQL($db_config); - * $arrest->rest(); - * - * - * Author: Gilbert Pellegrom - * Website: http://dev7studios.com - * Date: Jan 2013 - * Version 1.0 - */ -require('lib/db.php'); - -class ArrestMySQL { - - /** - * The instance of Database - * - * @var Database - */ - private $db; - /** - * The structure of the database - * - * @var array - */ - private $db_structure; - /** - * The URI segments - * - * @var array - */ - private $segments; - /** - * Array of custom table indexes - * - * @var array - */ - private $table_index; - - /** - * Create an instance, optionally setting a base URI - * - * @param array $db_config An array of database config options. Format: - * - * $db_config = array( - * 'server' => 'localhost', - * 'database' => '', - * 'username' => '', - * 'password' => '', - * 'verbose' => false - * ); - * - * @param string $base_uri Optional base URI if not in root folder - * @access public - */ - public function __construct($db_config, $base_uri = '') - { - $this->db = new Database($db_config); - if(!$this->db->init()) throw new Exception($this->db->get_error()); - - $this->db_structure = $this->map_db($db_config['database']); - $this->segments = $this->get_uri_segments($base_uri); - $this->table_index = array(); - } - - /** - * Handle the REST calls and map them to corresponding CRUD - * - * @access public - */ - public function rest() - { - header('Content-type: application/json'); - /* - create > POST /table - read > GET /table[/id] - update > PUT /table/id - delete > DELETE /table/id - */ - switch ($_SERVER['REQUEST_METHOD']) { - case 'POST': - $this->create(); - break; - case 'GET': - $this->read(); - break; - case 'PUT': - $this->update(); - break; - case 'DELETE': - $this->delete(); - break; - } - } - - /** - * Add a custom index (usually primary key) for a table - * - * @param string $table Name of the table - * @param string $field Name of the index field - * @access public - */ - public function set_table_index($table, $field) - { - $this->table_index[$table] = $field; - } - - /** - * Map the stucture of the MySQL db to an array - * - * @param string $database Name of the database - * @return array Returns array of db structure - * @access private - */ - private function map_db($database) - { - // Map db structure to array - $tables_arr = array(); - $this->db->query('SHOW TABLES FROM '. $database); - while($table = $this->db->fetch_array()){ - if(isset($table['Tables_in_'. $database])){ - $table_name = $table['Tables_in_'. $database]; - $tables_arr[$table_name] = array(); - } - } - foreach($tables_arr as $table_name=>$val){ - $this->db->query('SHOW COLUMNS FROM '. $table_name); - $fields = $this->db->fetch_all(); - $tables_arr[$table_name] = $fields; - } - return $tables_arr; - } - - /** - * Get the URI segments from the URL - * - * @param string $base_uri Optional base URI if not in root folder - * @return array Returns array of URI segments - * @access private - */ - private function get_uri_segments($base_uri) - { - // Fix REQUEST_URI if required - if(!isset($_SERVER['REQUEST_URI'])){ - $_SERVER['REQUEST_URI'] = substr($_SERVER['PHP_SELF'], 1); - if(isset($_SERVER['QUERY_STRING'])) $_SERVER['REQUEST_URI'] .= '?'. $_SERVER['QUERY_STRING']; - } - - $url = ''; - $request_url = $_SERVER['REQUEST_URI']; - $script_url = $_SERVER['PHP_SELF']; - $request_url = str_replace($base_uri, '', $request_url); - if($request_url != $script_url) $url = trim(preg_replace('/'. str_replace('/', '\/', str_replace('index.php', '', $script_url)) .'/', '', $request_url, 1), '/'); - $url = rtrim(preg_replace('/\?.*/', '', $url), '/'); - - return explode('/', $url); - } - - /** - * Get a URI segment - * - * @param int $index Index of the URI segment - * @return mixed Returns URI segment or false if none exists - * @access private - */ - private function segment($index) - { - if(isset($this->segments[$index])) return $this->segments[$index]; - return false; - } - - /** - * Handles a POST and inserts into the database - * - * @access private - */ - private function create() - { - $table = $this->segment(0); - - if(!$table || !isset($this->db_structure[$table])){ - $error = array('error' => array( - 'message' => 'Not Found', - 'code' => 404 - )); - die(json_encode($error)); - } - - if($data = $this->_post()){ - $this->db->insert($table, $data) - ->query(); - $success = array('success' => array( - 'message' => 'Success', - 'code' => 200 - )); - die(json_encode($success)); - } else { - $error = array('error' => array( - 'message' => 'No Content', - 'code' => 204 - )); - die(json_encode($error)); - } - } - - /** - * Handles a GET and reads from the database - * - * @access private - */ - private function read() - { - $table = $this->segment(0); - $id = intval($this->segment(1)); - - if(!$table || !isset($this->db_structure[$table])){ - $error = array('error' => array( - 'message' => 'Not Found', - 'code' => 404 - )); - die(json_encode($error)); - } - - if($id && is_int($id)) { - $index = 'id'; - if(isset($this->table_index[$table])) $index = $this->table_index[$table]; - $this->db->select('*') - ->from($table) - ->where($index, $id) - ->query(); - if($result = $this->db->fetch_array()){ - die(json_encode($result)); - } else { - $error = array('error' => array( - 'message' => 'No Content', - 'code' => 204 - )); - die(json_encode($error)); - } - } else { - $this->db->select('*') - ->from($table) - ->order_by($this->_get('order_by'), $this->_get('order')) - ->limit(intval($this->_get('limit')), intval($this->_get('offset'))) - ->query(); - if($result = $this->db->fetch_all()){ - die(json_encode($result)); - } else { - $error = array('error' => array( - 'message' => 'No Content', - 'code' => 204 - )); - die(json_encode($error)); - } - } - } - - /** - * Handles a PUT and updates the database - * - * @access private - */ - private function update() - { - $table = $this->segment(0); - $id = intval($this->segment(1)); - - if(!$table || !isset($this->db_structure[$table]) || !$id){ - $error = array('error' => array( - 'message' => 'Not Found', - 'code' => 404 - )); - die(json_encode($error)); - } - - $index = 'id'; - if(isset($this->table_index[$table])) $index = $this->table_index[$table]; - $this->db->select('*') - ->from($table) - ->where($index, $id) - ->query(); - if($result = $this->db->fetch_array()){ - $this->db->update($table) - ->set($this->_put()) - ->where($index, $id) - ->query(); - $success = array('success' => array( - 'message' => 'Success', - 'code' => 200 - )); - die(json_encode($success)); - } else { - $error = array('error' => array( - 'message' => 'No Content', - 'code' => 204 - )); - die(json_encode($error)); - } - } - - /** - * Handles a DELETE and deletes from the database - * - * @access private - */ - private function delete() - { - $table = $this->segment(0); - $id = intval($this->segment(1)); - - if(!$table || !isset($this->db_structure[$table]) || !$id){ - $error = array('error' => array( - 'message' => 'Not Found', - 'code' => 404 - )); - die(json_encode($error)); - } - - $index = 'id'; - if(isset($this->table_index[$table])) $index = $this->table_index[$table]; - $this->db->select('*') - ->from($table) - ->where($index, $id) - ->query(); - if($result = $this->db->fetch_array()){ - $this->db->delete($table) - ->where($index, $id) - ->query(); - $success = array('success' => array( - 'message' => 'Success', - 'code' => 200 - )); - die(json_encode($success)); - } else { - $error = array('error' => array( - 'message' => 'No Content', - 'code' => 204 - )); - die(json_encode($error)); - } - } - - /** - * Helper function to retrieve $_GET variables - * - * @param string $index Optional $_GET index - * @return mixed Returns the $_GET var at the specified index, - * the whole $_GET array or false - * @access private - */ - private function _get($index = '') - { - if($index){ - if(isset($_GET[$index]) && $_GET[$index]) return strip_tags($_GET[$index]); - } else { - if(isset($_GET) && !empty($_GET)) return $_GET; - } - return false; - } - - /** - * Helper function to retrieve $_POST variables - * - * @param string $index Optional $_POST index - * @return mixed Returns the $_POST var at the specified index, - * the whole $_POST array or false - * @access private - */ - private function _post($index = '') - { - if($index){ - if(isset($_POST[$index]) && $_POST[$index]) return $_POST[$index]; - } else { - if(isset($_POST) && !empty($_POST)) return $_POST; - } - return false; - } - - /** - * Helper function to retrieve PUT variables - * - * @return mixed Returns the contents of PUT as an array - * @access private - */ - private function _put() - { - $output = array(); - parse_str(file_get_contents('php://input'), $output); - return $output; - } - -} - -?> \ No newline at end of file diff --git a/lib/db.php b/lib/db.php deleted file mode 100644 index 34c5389e..00000000 --- a/lib/db.php +++ /dev/null @@ -1,330 +0,0 @@ -_config = $config; - } - - /* - * Initializes the database. Checks the configuration, connects, selects database. - */ - public function init() { - if(!$this->__check_config()) { - return false; - } - - if(!$this->__connect()) { - return false; - } - - if(!$this->__select_db()) { - return false; - } - - return true; - } - - /* - * Checks the configuration for blanks. - */ - private function __check_config() { - $config = $this->_config; - - if(empty($config["server"]) || empty($config["username"]) || empty($config["database"])) { - $this->_error = "Configuration details were blank."; - return false; - } - - $this->_verbose = ($config["verbose"]) ? true : false; - - return true; - } - - /* - * Connects to the database. - */ - private function __connect() { - $connection = @mysql_connect($this->_config["server"], $this->_config["username"], $this->_config["password"]); - - if(!$connection) { - $this->_error = ($this->_verbose) ? mysql_error() : "Could not connect to database."; - return false; - } - - return true; - } - - /* - * Selects the database to be working with. - */ - private function __select_db() { - $database = @mysql_select_db($this->_config["database"]); - - if(!$database) { - $this->_error = ($this->_verbose) ? mysql_error() : "Could not select database."; - return false; - } - - return true; - } - - /* - * SELECT starter. $fields can be either a string or an array of strings to select. - */ - public function select($fields) { - $query = "SELECT"; - - if(!empty($fields) && !is_array($fields)) { - $query .= " {$fields}"; - } else if(is_array($fields)) { - $query .= " `"; - $query .= implode("`,`", $fields); - $query .= "`"; - } else { - $query .= " *"; - } - - $this->_buildQuery = $query; - return $this; - } - - /* - * Adds where the SELECT is going to be coming from (table wise). - * select("*") - * select("username") - * select(array("username", "password")) - */ - public function from($table) { - $this->_buildQuery .= " FROM `{$table}`"; - return $this; - } - - /* - * UPDATE starter. - * update("users") - */ - public function update($table) { - $this->_buildQuery = "UPDATE `{$table}`"; - return $this; - } - - /* - * DELETE starter. - * delete("users") - */ - public function delete($table) { - $this->_buildQuery = "DELETE FROM `{$table}`"; - return $this; - } - - /* - * INSERT starter. $data is an array matched columns to values: - * $data = array("username" => "Caleb", "email" => "caleb@mingle-graphics.com"); - * insert("users", array("username" => "Caleb", "password" => "hash")) - */ - public function insert($table, $data) { - $query = "INSERT INTO `{$table}` ("; - $keys = array_keys($data); - $values = array_values($data); - - $query .= implode(", ", $keys); - $query .= ") VALUES ("; - - $array = array(); - - foreach($values as $value) { - $array[] = "'{$value}'"; - } - - $query .= implode(", ", $array) . ")"; - - $this->_buildQuery = $query; - return $this; - } - - /* - * SET. $data is an array matched key => value. - * set(array("username" => "Caleb")) - */ - public function set($data) { - if(!is_array($data)) return $this; - - $query = "SET "; - $array = array(); - - foreach($data as $key => $value) { - $array[] = "`{$key}`='{$value}'"; - } - - $query .= implode(", ", $array); - - $this->_buildQuery .= " " . $query; - return $this; - } - - /* - * WHERE. $fields and $values can either be strings or arrays based on how many you need. - * $operators can be an array to add in <, >, etc. Must match the index for $fields and $values. - * where("username", "Caleb") - * where(array("username", "password"), array("Caleb", "testing")) - * where(array("username", "level"), array("Caleb", "10"), array("=", "<")) - */ - public function where($fields, $values, $operators = '') { - if(!is_array($fields) && !is_array($values)) { - $operator = (empty($operators)) ? '=' : $operators[0]; - $query = " WHERE `{$fields}` {$operator} '{$values}'"; - } else { - $array = array_combine($fields, $values); - $query = " WHERE "; - - $data = array(); - $counter = 0; - - foreach($array as $key => $value) { - - $operator = (!empty($operators) && !empty($operators[$counter])) ? $operators[$counter] : '='; - - $data[] = "`{$key}` {$operator} '{$value}'"; - - $counter++; - } - - $query .= implode(" AND ", $data); - } - - $this->_buildQuery .= $query; - return $this; - } - - /* - * Order By: - * order_by("username", "asc") - */ - public function order_by($field, $direction = 'asc') { - if($field) $this->_buildQuery .= " ORDER BY `{$field}` " . strtoupper($direction); - return $this; - } - - /* - * Limit: - * limit(1) - * limit(1, 0) - */ - public function limit($max, $min = '0') { - if($max) $this->_buildQuery .= " LIMIT {$min},{$max}"; - return $this; - } - - /* - * Will return the object of data from the query. - */ - public function fetch_object() { - $object = @mysql_fetch_object($this->_query); - - if(!$object && $this->_verbose) { - $this->_error = mysql_error(); - } - - return $object; - } - - /* - * Will return the array of data from the query. - */ - public function fetch_array() { - $array = @mysql_fetch_array($this->_query); - if($array){ - foreach($array as $key=>$val){ - if(is_numeric($key)){ - unset($array[$key]); - } - } - } - - if(!$array && $this->_verbose) { - $this->_error = mysql_error(); - } - - return $array; - } - - public function fetch_all() { - $results = array(); - while($array = @mysql_fetch_array($this->_query)){ - foreach($array as $key=>$val){ - if(is_numeric($key)){ - unset($array[$key]); - } - } - $results[] = $array; - } - - - - if(!$array && $this->_verbose) { - $this->_error = mysql_error(); - } - - return $results; - } - - /* - * Will return the number or rows affected from the query. - */ - public function num_rows() { - $num = @mysql_num_rows($this->_query); - - if(!$num && $this->_verbose) { - $this->_error = mysql_error(); - } - - return $num; - } - - /* - * If $query_text is blank, query will be performed on the built query stored. - */ - public function query($query_text = '') { - $query_text = ($query_text == '') ? $this->_buildQuery : $query_text; - - $query = @mysql_query($query_text); - - if(!$query && $this->_verbose) { - echo "

MySQL Error:

"; - echo "

" . mysql_error() . "

"; - } - - $this->_query = $query; - - return $this; - } - - /* - * Will return the current built query story in $this->_buildQuery; - */ - public function get_query() { - return $this->_buildQuery; - } - - /* - * Will return the current stored error. - */ - public function get_error() { - return $this->_error; - } -} - -?> \ No newline at end of file