├── .gitattributes ├── .gitignore ├── README.md ├── bin ├── import └── install ├── bootstrap.php ├── composer.json ├── lib └── API │ ├── Application.php │ ├── Exception.php │ ├── Exception │ └── ValidationException.php │ └── Middleware │ ├── Cache.php │ ├── JSON.php │ ├── RateLimit.php │ └── TokenOverBasicAuth.php ├── public ├── .htaccess └── index.php └── share ├── config └── default.php ├── db └── .sqlite └── sql ├── data ├── contacts.sql └── users.sql └── tables ├── contacts.sql ├── notes.sql └── users.sql /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # ========================= 18 | # Operating System Files 19 | # ========================= 20 | 21 | # OSX 22 | # ========================= 23 | 24 | .DS_Store 25 | .AppleDouble 26 | .LSOverride 27 | 28 | # Icon must ends with two \r. 29 | Icon 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear on external disk 35 | .Spotlight-V100 36 | .Trashes 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | My Contacts API 2 | =============== 3 | 4 | _Author: Vito Tardia ()_ 5 | 6 | This application implements a simple contact list service that manages contacts with linked notes. It has **two object types**, contacts and notes. Each contact has basic attributes such as first name, last name, and email address. Also, each contact can have a number of markdown-formatted notes linked to it. 7 | 8 | **This is sample code** for the article "Constructing a full REST API with respect to best practices - Part 1 and 2" written for Sitepoint. 9 | 10 | ## Resources and Actions 11 | 12 | URL HTTP Method Operation 13 | /api/contacts GET Returns an array of contacts 14 | /api/contacts/:id GET Returns the contact with id of :id 15 | /api/contacts POST Adds a new contact and return it with an id attribute added 16 | /api/contacts/:id PUT Updates the contact with id of :id 17 | /api/contacts/:id PATCH Partially updates the contact with id of :id 18 | /api/contacts/:id DELETE Deletes the contact with id of :id 19 | 20 | /api/contacts/:id/star PUT Adds to favorites the contact with id of :id 21 | /api/contacts/:id/star DELETE Removes from favorites the contact with id of :id 22 | 23 | /api/contacts/:id/notes GET Returns the notes for the contact with id of :id 24 | /api/contacts/:id/notes/:nid GET Returns the note with id of :nid for the contact with id of :id 25 | /api/contacts/:id/notes POST Adds a new note for the contact with id of :id 26 | /api/contacts/:id/notes/:nid PUT Updates the note with id if :nid for the contact with id of :id 27 | /api/contacts/:id/notes/:nid PATCH Partially updates the note with id of :nid for the contact with id of :id 28 | /api/contacts/:id/notes/:nid DELETE Deletes the note with id of :nid for the contact with id of :id 29 | -------------------------------------------------------------------------------- /bin/import: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | $argc) { 6 | echo "Usage: import \n"; 7 | exit(1); 8 | } 9 | 10 | $config = array(); 11 | 12 | $_ENV['SLIM_MODE'] = $argv[1]; 13 | 14 | $configFile = dirname(__FILE__) . '/share/config/' 15 | . $_ENV['SLIM_MODE'] . '.php'; 16 | 17 | if (is_readable($configFile)) { 18 | require_once $configFile; 19 | } else { 20 | require_once dirname(__FILE__) . '/../share/config/default.php'; 21 | } 22 | 23 | 24 | // Init database 25 | if (!empty($config['db'])) { 26 | \ORM::configure($config['db']['dsn']); 27 | if (!empty($config['db']['username']) 28 | && !empty($config['db']['password'])) { 29 | \ORM::configure('username', $config['db']['username']); 30 | \ORM::configure('password', $config['db']['password']); 31 | } 32 | } 33 | 34 | $db = \ORM::get_db(); 35 | 36 | $users = file_get_contents( 37 | dirname(__FILE__) . '/../share/sql/data/users.sql' 38 | ); 39 | $contacts = file_get_contents( 40 | dirname(__FILE__) . '/../share/sql/data/contacts.sql' 41 | ); 42 | 43 | $db->exec($users); 44 | $db->exec($contacts); 45 | -------------------------------------------------------------------------------- /bin/install: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | $argc) { 6 | echo "Usage: install \n"; 7 | exit(1); 8 | } 9 | 10 | $config = array(); 11 | 12 | $_ENV['SLIM_MODE'] = $argv[1]; 13 | 14 | $configFile = dirname(__FILE__) . '/share/config/' 15 | . $_ENV['SLIM_MODE'] . '.php'; 16 | 17 | if (is_readable($configFile)) { 18 | require_once $configFile; 19 | } else { 20 | require_once dirname(__FILE__) . '/../share/config/default.php'; 21 | } 22 | 23 | 24 | // Init database 25 | if (!empty($config['db'])) { 26 | \ORM::configure($config['db']['dsn']); 27 | if (!empty($config['db']['username']) 28 | && !empty($config['db']['password'])) { 29 | \ORM::configure('username', $config['db']['username']); 30 | \ORM::configure('password', $config['db']['password']); 31 | } 32 | } 33 | 34 | $db = \ORM::get_db(); 35 | 36 | $tableUsers = file_get_contents( 37 | dirname(__FILE__) . '/../share/sql/tables/users.sql' 38 | ); 39 | $tableContacts = file_get_contents( 40 | dirname(__FILE__) . '/../share/sql/tables/contacts.sql' 41 | ); 42 | $tableNotes = file_get_contents( 43 | dirname(__FILE__) . '/../share/sql/tables/notes.sql' 44 | ); 45 | 46 | $db->exec($tableUsers); 47 | $db->exec($tableContacts); 48 | $db->exec($tableNotes); 49 | -------------------------------------------------------------------------------- /bootstrap.php: -------------------------------------------------------------------------------- 1 | configureMode('production', function () use ($app) { 34 | $app->config(array( 35 | 'log.enable' => true, 36 | 'log.level' => \Slim\Log::WARN, 37 | 'debug' => false 38 | )); 39 | }); 40 | 41 | // Only invoked if mode is "development" 42 | $app->configureMode('development', function () use ($app) { 43 | $app->config(array( 44 | 'log.enable' => true, 45 | 'log.level' => \Slim\Log::DEBUG, 46 | 'debug' => false 47 | )); 48 | }); 49 | 50 | // Get log writer 51 | $log = $app->getLog(); 52 | 53 | // Init database 54 | try { 55 | 56 | if (!empty($config['db'])) { 57 | \ORM::configure($config['db']['dsn']); 58 | if (!empty($config['db']['username']) 59 | && !empty($config['db']['password'])) { 60 | \ORM::configure('username', $config['db']['username']); 61 | \ORM::configure('password', $config['db']['password']); 62 | } 63 | } 64 | 65 | } catch (\PDOException $e) { 66 | $log->error($e->getMessage()); 67 | } 68 | 69 | 70 | // Cache Middleware (inner) 71 | $app->add(new API\Middleware\Cache('/api/v1')); 72 | 73 | // Parses JSON body 74 | $app->add(new \Slim\Middleware\ContentTypes()); 75 | 76 | // Manage Rate Limit 77 | $app->add(new API\Middleware\RateLimit('/api/v1')); 78 | 79 | // JSON Middleware 80 | $app->add(new API\Middleware\JSON('/api/v1')); 81 | 82 | // Auth Middleware (outer) 83 | $app->add(new API\Middleware\TokenOverBasicAuth(array('root' => '/api/v1'))); 84 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vtardia/my-contacts", 3 | "description": "Simple RESTful API for contacts management", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Vito Tardia", 8 | "email": "vito@tardia.me" 9 | } 10 | ], 11 | "require": { 12 | "php": ">=5.4", 13 | "slim/slim": "*", 14 | "slim/extras": "*", 15 | "slim/middleware": "*", 16 | "monolog/monolog": "*", 17 | "j4mie/paris": "*", 18 | "flynsarmy/slim-monolog": "*" 19 | }, 20 | "archive": { 21 | "exclude": ["vendor", ".DS_Store", "*.log"] 22 | }, 23 | "autoload": { 24 | "psr-0": { 25 | "API": "lib/" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/API/Application.php: -------------------------------------------------------------------------------- 1 | FILTER_SANITIZE_NUMBER_INT, 21 | 'firstname' => FILTER_SANITIZE_STRING, 22 | 'lastname' => FILTER_SANITIZE_STRING, 23 | 'email' => FILTER_SANITIZE_EMAIL, 24 | 'phone' => FILTER_SANITIZE_STRING, 25 | ), 26 | false 27 | ); 28 | 29 | switch ($action) { 30 | 31 | case 'update': 32 | if (empty($contact['id'])) { 33 | $errors['contact'][] = array( 34 | 'field' => 'id', 35 | 'message' => 'ID cannot be empty on update' 36 | ); 37 | break; 38 | } 39 | if (isset($contact['firstname']) 40 | && empty($contact['firstname'])) { 41 | $errors['contact'][] = array( 42 | 'field' => 'firstname', 43 | 'message' => 'First name cannot be empty' 44 | ); 45 | } 46 | if (isset($contact['email'])) { 47 | if (empty($contact['email'])) { 48 | $errors['contact'][] = array( 49 | 'field' => 'email', 50 | 'message' => 'Email address cannot be empty' 51 | ); 52 | break; 53 | } 54 | 55 | if (false === filter_var( 56 | $contact['email'], 57 | FILTER_VALIDATE_EMAIL 58 | )) { 59 | $errors['contact'][] = array( 60 | 'field' => 'email', 61 | 'message' => 'Email address is invalid' 62 | ); 63 | break; 64 | } 65 | 66 | // Test for unique email 67 | $results = \ORM::forTable('contacts') 68 | ->where('email', $contact['email'])->findOne(); 69 | if (false !== $results 70 | && $results->id !== $contact['id']) { 71 | $errors['contact'][] = array( 72 | 'field' => 'email', 73 | 'message' => 'Email address already exists' 74 | ); 75 | } 76 | } 77 | break; 78 | 79 | case 'create': 80 | default: 81 | if (empty($contact['firstname'])) { 82 | $errors['contact'][] = array( 83 | 'field' => 'firstname', 84 | 'message' => 'First name cannot be empty' 85 | ); 86 | } 87 | if (empty($contact['email'])) { 88 | $errors['contact'][] = array( 89 | 'field' => 'email', 90 | 'message' => 'Email address cannot be empty' 91 | ); 92 | } elseif (false === filter_var( 93 | $contact['email'], 94 | FILTER_VALIDATE_EMAIL 95 | )) { 96 | $errors['contact'][] = array( 97 | 'field' => 'email', 98 | 'message' => 'Email address is invalid' 99 | ); 100 | } else { 101 | 102 | // Test for unique email 103 | $results = \ORM::forTable('contacts') 104 | ->where('email', $contact['email'])->count(); 105 | if ($results > 0) { 106 | $errors['contact'][] = array( 107 | 'field' => 'email', 108 | 'message' => 'Email address already exists' 109 | ); 110 | } 111 | } 112 | 113 | break; 114 | } 115 | 116 | 117 | if (!empty($notes) && is_array($notes)) { 118 | $noteCount = count($notes); 119 | for ($i = 0; $i < $noteCount; $i++) { 120 | 121 | $noteErrors = $this->validateNote($notes[$i], $action); 122 | if (!empty($noteErrors)) { 123 | $errors['notes'][] = $noteErrors; 124 | unset($noteErrors); 125 | } 126 | 127 | } 128 | } 129 | 130 | return $errors; 131 | } 132 | 133 | public function validateNote($note = array(), $action = 'create') 134 | { 135 | $errors = array(); 136 | 137 | $note = filter_var_array( 138 | $note, 139 | array( 140 | 'id' => FILTER_SANITIZE_NUMBER_INT, 141 | 'body' => FILTER_SANITIZE_STRING, 142 | 'contact_id' => FILTER_SANITIZE_NUMBER_INT, 143 | ), 144 | false 145 | ); 146 | 147 | if (isset($note['body']) && empty($note['body'])) { 148 | $errors[] = array( 149 | 'field' => 'body', 150 | 'message' => 'Note body cannot be empty' 151 | ); 152 | } 153 | 154 | 155 | return $errors; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /lib/API/Exception.php: -------------------------------------------------------------------------------- 1 | data = $data; 14 | } 15 | 16 | public function getData() 17 | { 18 | return $this->data; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/API/Middleware/Cache.php: -------------------------------------------------------------------------------- 1 | root = $root; 9 | $this->ttl = 300; // 5 minutes 10 | 11 | } 12 | 13 | public function call() 14 | { 15 | $key = $this->app->request->getResourceUri(); 16 | $response = $this->app->response; 17 | 18 | if ($ttl = $this->app->config('cache.ttl')) { 19 | $this->ttl = $ttl; 20 | } 21 | 22 | if (preg_match('|^' . $this->root . '.*|', $key)) { 23 | $method = strtolower($this->app->request->getMethod()); 24 | 25 | if ('get' === $method) { 26 | $queryString = http_build_query($this->app->request->get()); 27 | if (!empty($queryString)) { 28 | $key .= '?' . $queryString; 29 | } 30 | 31 | $data = $this->fetch($key); 32 | if ($data) { 33 | 34 | // Cache hit... return the cached content 35 | $response->headers->set( 36 | 'Content-Type', 37 | 'application/json' 38 | ); 39 | $response->headers->set( 40 | 'X-Cache', 41 | 'HIT' 42 | ); 43 | try { 44 | 45 | $this->app->etag($data['checksum']); 46 | $this->app->expires($data['expires']); 47 | $response->body($data['content']); 48 | } catch (\Slim\Exception\Stop $e) { 49 | } 50 | return; 51 | } 52 | 53 | // Cache miss... continue on to generate the page 54 | $this->next->call(); 55 | 56 | if ($response->status() == 200) { 57 | 58 | // Cache result for future look up 59 | $checksum = md5($response->body()); 60 | $expires = time() + $this->ttl; 61 | 62 | $this->save( 63 | $key, 64 | array( 65 | 'checksum' => $checksum, 66 | 'expires' => $expires, 67 | 'content' => $response->body(), 68 | ) 69 | ); 70 | 71 | $response->headers->set( 72 | 'X-Cache', 73 | 'MISS' 74 | ); 75 | try { 76 | $this->app->etag($checksum); 77 | $this->app->expires($expires); 78 | 79 | } catch (\Slim\Exception\Stop $e) { 80 | } 81 | return; 82 | } 83 | 84 | } else { 85 | if ($response->status() == 200) { 86 | $response->headers->set( 87 | 'X-Cache', 88 | 'NONE' 89 | ); 90 | $this->clean($key); 91 | } 92 | } 93 | 94 | } 95 | $this->next->call(); 96 | } 97 | 98 | protected function fetch($key) 99 | { 100 | return apc_fetch($key); 101 | } 102 | 103 | protected function save($key, $value) 104 | { 105 | apc_store($key, $value, $this->ttl); 106 | } 107 | 108 | protected function clean($key = '') 109 | { 110 | // Delete all keys beginning with $key 111 | if (!empty($key)) { 112 | $toDelete = new \APCIterator('user', '|^'.$key.'.*|', APC_ITER_KEY); 113 | return apc_delete($toDelete); 114 | } 115 | 116 | // Clean all user cache 117 | return apc_clear_cache('user'); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /lib/API/Middleware/JSON.php: -------------------------------------------------------------------------------- 1 | root = $root; 9 | } 10 | 11 | public function call() 12 | { 13 | if (preg_match( 14 | '|^' . $this->root . '.*|', 15 | $this->app->request->getResourceUri() 16 | )) { 17 | 18 | // Force response headers to JSON 19 | $this->app->response->headers->set( 20 | 'Content-Type', 21 | 'application/json' 22 | ); 23 | 24 | $method = strtolower($this->app->request->getMethod()); 25 | $mediaType = $this->app->request->getMediaType(); 26 | 27 | if (in_array( 28 | $method, 29 | array('post', 'put', 'patch') 30 | ) && '' !== $this->app->request()->getBody()) { 31 | 32 | if (empty($mediaType) 33 | || $mediaType !== 'application/json') { 34 | $this->app->halt(415); 35 | } 36 | } 37 | } 38 | $this->next->call(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/API/Middleware/RateLimit.php: -------------------------------------------------------------------------------- 1 | root = $root; 10 | $this->max = 100; // Requests per hour 11 | 12 | } 13 | 14 | public function call() 15 | { 16 | $response = $this->app->response; 17 | $request = $this->app->request; 18 | 19 | if ($max = $this->app->config('rate.limit')) { 20 | $this->max = $max; 21 | } 22 | 23 | // Activate on given root URL only 24 | if (preg_match( 25 | '|^' . $this->root . '.*|', 26 | $this->app->request->getResourceUri() 27 | )) { 28 | 29 | 30 | // Use API key from the current user as ID 31 | if ($key = $this->app->user['apikey']) { 32 | 33 | $data = $this->fetch($key); 34 | if (false === $data) { 35 | 36 | // First time or previous perion expired, 37 | // initialize and save a new entry 38 | 39 | $remaining = ($this->max -1); 40 | $reset = 3600; 41 | 42 | $this->save( 43 | $key, 44 | array( 45 | 'remaining' => $remaining, 46 | 'created' => time() 47 | ), 48 | $reset 49 | ); 50 | } else { 51 | 52 | // Take the current entry and update it 53 | 54 | $remaining = (--$data['remaining'] >= 0) 55 | ? $data['remaining'] : -1; 56 | 57 | $reset = (($data['created'] + 3600) - time()); 58 | 59 | $this->save( 60 | $key, 61 | array( 62 | 'remaining' => $remaining, 63 | 'created' => $data['created'] 64 | ), 65 | $reset 66 | ); 67 | } 68 | 69 | // Set rating headers 70 | 71 | $response->headers->set( 72 | 'X-Rate-Limit-Limit', 73 | $this->max 74 | ); 75 | 76 | $response->headers->set( 77 | 'X-Rate-Limit-Reset', 78 | $reset 79 | ); 80 | 81 | $response->headers->set( 82 | 'X-Rate-Limit-Remaining', 83 | $remaining 84 | ); 85 | 86 | // Check if the current key is allowed to pass 87 | if (0 > $remaining) { 88 | 89 | // Rewrite remaining headers 90 | $response->headers->set( 91 | 'X-Rate-Limit-Remaining', 92 | 0 93 | ); 94 | 95 | // Exits with status "429 Too Many Requests" (see doc below) 96 | $this->fail(); 97 | } 98 | 99 | 100 | } else { 101 | // Exits with status "429 Too Many Requests" (see doc below) 102 | $this->fail(); 103 | } 104 | 105 | 106 | } 107 | 108 | $this->next->call(); 109 | 110 | } 111 | 112 | protected function fetch($key) 113 | { 114 | return apc_fetch($key); 115 | } 116 | 117 | protected function save($key, $value, $expire = 0) 118 | { 119 | apc_store($key, $value, $expire); 120 | } 121 | 122 | /** 123 | * Exits with status "429 Too Many Requests" 124 | * 125 | * Work around on Apache's issue: it does not support 126 | * status code 429 until version 2.4 127 | * 128 | * @link http://stackoverflow.com/questions/17735514/php-apache-silently-converting-http-429-and-others-to-500 129 | */ 130 | protected function fail() 131 | { 132 | header('HTTP/1.1 429 Too Many Requests', false, 429); 133 | 134 | // Write the remaining headers 135 | foreach ($this->app->response->headers as $key => $value) { 136 | header($key . ': ' . $value); 137 | } 138 | exit; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /lib/API/Middleware/TokenOverBasicAuth.php: -------------------------------------------------------------------------------- 1 | 14 | * @version 1.0 15 | * @copyright 2014 Vito Tardia 16 | * 17 | * USAGE 18 | * 19 | * $app = new \Slim\Slim(); 20 | * $app->add(new API\Middleware\TokenOverBasicAuth()); 21 | * 22 | * MIT LICENSE 23 | * 24 | * Permission is hereby granted, free of charge, to any person obtaining 25 | * a copy of this software and associated documentation files (the 26 | * "Software"), to deal in the Software without restriction, including 27 | * without limitation the rights to use, copy, modify, merge, publish, 28 | * distribute, sublicense, and/or sell copies of the Software, and to 29 | * permit persons to whom the Software is furnished to do so, subject to 30 | * the following conditions: 31 | * 32 | * The above copyright notice and this permission notice shall be 33 | * included in all copies or substantial portions of the Software. 34 | * 35 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 36 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 37 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 38 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 39 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 40 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 41 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 42 | */ 43 | 44 | namespace API\Middleware; 45 | 46 | class TokenOverBasicAuth extends \Slim\Middleware 47 | { 48 | /** 49 | * @var array 50 | */ 51 | protected $settings = array( 52 | 'realm' => 'Protected Area', 53 | 'root' => '/' 54 | ); 55 | 56 | /** 57 | * Constructor 58 | * 59 | * @param array $config Configuration and Login Details 60 | * @return void 61 | */ 62 | public function __construct(array $config = array()) 63 | { 64 | if (!isset($this->app)) { 65 | $this->app = \Slim\Slim::getInstance(); 66 | } 67 | $this->config = array_merge($this->settings, $config); 68 | } 69 | 70 | /** 71 | * Call 72 | * 73 | * This method will check the HTTP request headers for 74 | * previous authentication. If the request has already authenticated, 75 | * the next middleware is called. Otherwise, 76 | * a 401 Authentication Required response is returned to the client. 77 | * 78 | * @return void 79 | */ 80 | public function call() 81 | { 82 | $req = $this->app->request(); 83 | $res = $this->app->response(); 84 | 85 | if (preg_match( 86 | '|^' . $this->config['root'] . '.*|', 87 | $req->getResourceUri() 88 | )) { 89 | 90 | // We just need the user 91 | $authToken = $req->headers('PHP_AUTH_USER'); 92 | 93 | if (!($authToken && $this->verify($authToken))) { 94 | $res->status(401); 95 | $res->header( 96 | 'WWW-Authenticate', 97 | sprintf('Basic realm="%s"', $this->config['realm']) 98 | ); 99 | } 100 | 101 | } 102 | 103 | $this->next->call(); 104 | } 105 | 106 | /** 107 | * Check passed auth token 108 | * 109 | * @param string $authToken 110 | * @return boolean 111 | */ 112 | protected function verify($authToken) 113 | { 114 | $user = \ORM::forTable('users')->where('apikey', $authToken) 115 | ->findOne(); 116 | 117 | if (false !== $user) { 118 | $this->app->user = $user->asArray(); 119 | return true; 120 | } 121 | 122 | return false; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | 3 | ## Front controller redirect 4 | RewriteCond %{REQUEST_FILENAME} !-f 5 | RewriteRule ^ index.php [QSA,L] 6 | 7 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | group( 10 | '/api', 11 | function () use ($app, $log) { 12 | 13 | // Common to all sub routes 14 | 15 | // Get contacts 16 | $app->get('/', function () { 17 | echo "

This can be the documentation entry point

"; 18 | echo "

This URL could also contain discovery" 19 | ." information in side the headers

"; 20 | }); 21 | 22 | // Group for API Version 1 23 | $app->group( 24 | '/v1', 25 | // API Methods 26 | function () use ($app, $log) { 27 | 28 | // Get contacts 29 | $app->get( 30 | '/contacts', 31 | function () use ($app, $log) { 32 | 33 | $contacts = array(); 34 | $filters = array(); 35 | $total = 0; 36 | $pages = 1; 37 | 38 | // Default resultset 39 | $results = \ORM::forTable('contacts'); 40 | 41 | // Get and sanitize filters from the URL 42 | if ($rawfilters = $app->request->get()) { 43 | unset( 44 | $rawfilters['sort'], 45 | $rawfilters['fields'], 46 | $rawfilters['page'], 47 | $rawfilters['per_page'] 48 | ); 49 | foreach ($rawfilters as $key => $value) { 50 | $filters[$key] = filter_var( 51 | $value, 52 | FILTER_SANITIZE_STRING 53 | ); 54 | } 55 | 56 | } 57 | 58 | // Add filters to the query 59 | if (!empty($filters)) { 60 | foreach ($filters as $key => $value) { 61 | if ('q' == $key) { 62 | $results->whereRaw( 63 | '(`firstname` LIKE ? OR `email` LIKE ?)', 64 | array('%'.$value.'%', '%'.$value.'%') 65 | ); 66 | } else { 67 | $results->where($key, $value); 68 | } 69 | } 70 | 71 | } 72 | 73 | // Get and sanitize field list from the URL 74 | if ($fields = $app->request->get('fields')) { 75 | $fields = explode(',', $fields); 76 | $fields = array_map( 77 | function ($field) { 78 | $field = filter_var( 79 | $field, 80 | FILTER_SANITIZE_STRING 81 | ); 82 | return trim($field); 83 | }, 84 | $fields 85 | ); 86 | } 87 | 88 | // Add field list to the query 89 | if (is_array($fields) && !empty($fields)) { 90 | $results->selectMany($fields); 91 | } 92 | 93 | 94 | // Manage sort options 95 | // sort=firstname => ORDER BY firstname ASC 96 | // sort=-firstname => ORDER BY firstname DESC 97 | // sort=-firstname,email => 98 | // ORDER BY firstname DESC, email ASC 99 | if ($sort = $app->request->get('sort')) { 100 | $sort = explode(',', $sort); 101 | $sort = array_map( 102 | function ($s) { 103 | $s = filter_var($s, FILTER_SANITIZE_STRING); 104 | return trim($s); 105 | }, 106 | $sort 107 | ); 108 | foreach ($sort as $expr) { 109 | if ('-' == substr($expr, 0, 1)) { 110 | $results->orderByDesc(substr($expr, 1)); 111 | } else { 112 | $results->orderByAsc($expr); 113 | } 114 | } 115 | } 116 | 117 | 118 | // Manage pagination 119 | $page = filter_var( 120 | $app->request->get('page'), 121 | FILTER_SANITIZE_NUMBER_INT 122 | ); 123 | if (!empty($page)) { 124 | 125 | $perPage = filter_var( 126 | $app->request->get('per_page'), 127 | FILTER_SANITIZE_NUMBER_INT 128 | ); 129 | if (empty($perPage)) { 130 | $perPage = 10; 131 | } 132 | 133 | // Total after filters and 134 | // before pagination limit 135 | $total = $results->count(); 136 | 137 | // Compute the pagination Link header 138 | $pages = ceil($total / $perPage); 139 | 140 | // Base for all links 141 | $linkBaseURL = $app->request->getUrl() 142 | . $app->request->getRootUri() 143 | . $app->request->getResourceUri(); 144 | 145 | // Init empty vars 146 | $queryString = array(); 147 | $links = array(); 148 | $next = ''; 149 | $last = ''; 150 | $prev = ''; 151 | $first = ''; 152 | 153 | // Adding fields 154 | if (!empty($fields)) { 155 | $queryString[] = 'fields=' 156 | . join( 157 | ',', 158 | array_map( 159 | function ($f) { 160 | return urlencode($f); 161 | }, 162 | $fields 163 | ) 164 | ); 165 | } 166 | 167 | // Adding filters 168 | if (!empty($filters)) { 169 | $queryString[] = http_build_query($filters); 170 | } 171 | 172 | // Adding sort options 173 | if (!empty($sort)) { 174 | $queryString[] = 'sort=' 175 | . join( 176 | ',', 177 | array_map( 178 | function ($s) { 179 | return urlencode($s); 180 | }, 181 | $sort 182 | ) 183 | ); 184 | } 185 | 186 | // Next and Last link 187 | if ($page < $pages) { 188 | $next = $linkBaseURL . '?' . join( 189 | '&', 190 | array_merge( 191 | $queryString, 192 | array( 193 | 'page=' . (string) ($page + 1), 194 | 'per_page=' . $perPage 195 | ) 196 | ) 197 | ); 198 | $last = $linkBaseURL . '?' . join( 199 | '&', 200 | array_merge( 201 | $queryString, 202 | array( 203 | 'page=' . (string) $pages, 204 | 'per_page=' . $perPage 205 | ) 206 | ) 207 | ); 208 | 209 | $links[] = sprintf('<%s>; rel="next"', $next); 210 | $links[] = sprintf('<%s>; rel="last"', $last); 211 | } 212 | 213 | // Previous and First link 214 | if ($page > 1 && $page <= $pages) { 215 | $prev = $linkBaseURL . '?' . join( 216 | '&', 217 | array_merge( 218 | $queryString, 219 | array( 220 | 'page=' . (string) ($page - 1), 221 | 'per_page=' . $perPage 222 | ) 223 | ) 224 | ); 225 | $first = $linkBaseURL . '?' . join( 226 | '&', 227 | array_merge( 228 | $queryString, 229 | array( 230 | 'page=1', 'per_page=' . $perPage 231 | ) 232 | ) 233 | ); 234 | $links[] = sprintf('<%s>; rel="prev"', $prev); 235 | $links[] = sprintf('<%s>; rel="first"', $first); 236 | } 237 | 238 | // Set header if required 239 | if (!empty($links)) { 240 | $app->response->headers->set( 241 | 'Link', 242 | join(',', $links) 243 | ); 244 | } 245 | 246 | $results->limit($perPage) 247 | ->offset($page * $perPage - $perPage); 248 | } 249 | 250 | 251 | $contacts = $results->findArray(); 252 | 253 | if (empty($total)) { 254 | $total = count($contacts); 255 | } 256 | $app->response->headers->set('X-Total-Count', $total); 257 | 258 | echo json_encode($contacts, JSON_PRETTY_PRINT); 259 | } 260 | ); 261 | 262 | // Get contact with ID 263 | $app->get( 264 | '/contacts/:id', 265 | function ($id) use ($app, $log) { 266 | 267 | $id = filter_var( 268 | filter_var($id, FILTER_SANITIZE_NUMBER_INT), 269 | FILTER_VALIDATE_INT 270 | ); 271 | 272 | if (false === $id) { 273 | throw new ValidationException("Invalid contact ID"); 274 | } 275 | 276 | $contact = \ORM::forTable('contacts')->findOne($id); 277 | if ($contact) { 278 | 279 | $output = $contact->asArray(); 280 | 281 | if ('notes' === $app->request->get('embed')) { 282 | $notes = \ORM::forTable('notes') 283 | ->where('contact_id', $id) 284 | ->orderByDesc('id') 285 | ->findArray(); 286 | 287 | if (!empty($notes)) { 288 | $output['notes'] = $notes; 289 | } 290 | } 291 | 292 | echo json_encode($output, JSON_PRETTY_PRINT); 293 | return; 294 | } 295 | $app->notFound(); 296 | } 297 | ); 298 | 299 | // Adds new contact 300 | $app->post( 301 | '/contacts', 302 | function () use ($app, $log) { 303 | 304 | $body = $app->request()->getBody(); 305 | 306 | $errors = $app->validateContact($body); 307 | 308 | if (empty($errors)) { 309 | $contact = \ORM::for_table('contacts')->create(); 310 | 311 | if (isset($body['notes'])) { 312 | $notes = $body['notes']; 313 | unset($body['notes']); 314 | } 315 | 316 | $contact->set($body); 317 | 318 | if (true === $contact->save()) { 319 | 320 | // Insert notes 321 | if (!empty($notes)) { 322 | $contactNotes = array(); 323 | foreach ($notes as $item) { 324 | $item['contact_id'] = $contact->id; 325 | $note = \ORM::for_table('notes') 326 | ->create(); 327 | $note->set($item); 328 | if (true === $note->save()) { 329 | $contactNotes[] = $note->asArray(); 330 | } 331 | } 332 | } 333 | 334 | $output = $contact->asArray(); 335 | if (!empty($contactNotes)) { 336 | $output['notes'] = $contactNotes; 337 | } 338 | echo json_encode($output, JSON_PRETTY_PRINT); 339 | } else { 340 | throw new Exception("Unable to save contact"); 341 | } 342 | 343 | } else { 344 | throw new ValidationException( 345 | "Invalid data", 346 | 0, 347 | $errors 348 | ); 349 | } 350 | } 351 | ); 352 | 353 | // Update contact with ID 354 | $app->map( 355 | '/contacts/:id', 356 | function ($id) use ($app, $log) { 357 | 358 | $contact = \ORM::forTable('contacts')->findOne($id); 359 | 360 | if ($contact) { 361 | 362 | $body = $app->request()->getBody(); 363 | 364 | $errors = $app->validateContact($body, 'update'); 365 | 366 | if (empty($errors)) { 367 | 368 | if (isset($body['notes'])) { 369 | $notes = $body['notes']; 370 | unset($body['notes']); 371 | } 372 | 373 | $contact->set($body); 374 | 375 | if (true === $contact->save()) { 376 | 377 | // Process notes 378 | if (!empty($notes)) { 379 | $contactNotes = array(); 380 | foreach ($notes as $item) { 381 | 382 | $item['contact_id'] = $contact->id; 383 | 384 | if (empty($item['id'])) { 385 | 386 | // New note 387 | $note = \ORM::for_table('notes') 388 | ->create(); 389 | } else { 390 | 391 | // Existing note 392 | $note = \ORM::forTable('notes') 393 | ->findOne($item['id']); 394 | } 395 | 396 | if ($note) { 397 | $note->set($item); 398 | if (true === $note->save()) { 399 | $contactNotes[] = $note->asArray(); 400 | } 401 | } 402 | } 403 | } 404 | 405 | $output = $contact->asArray(); 406 | if (!empty($contactNotes)) { 407 | $output['notes'] = $contactNotes; 408 | } 409 | echo json_encode( 410 | $output, 411 | JSON_PRETTY_PRINT 412 | ); 413 | return; 414 | 415 | } else { 416 | throw new Exception( 417 | "Unable to save contact" 418 | ); 419 | } 420 | 421 | } else { 422 | throw new ValidationException( 423 | "Invalid data", 424 | 0, 425 | $errors 426 | ); 427 | } 428 | 429 | } 430 | 431 | $app->notFound(); 432 | } 433 | )->via('PUT', 'PATCH'); 434 | 435 | 436 | // Delete contact with ID 437 | $app->delete( 438 | '/contacts/:id', 439 | function ($id) use ($app, $log) { 440 | 441 | $contact = \ORM::forTable('contacts')->findOne($id); 442 | 443 | if ($contact) { 444 | 445 | $contact->delete(); 446 | 447 | $app->halt(204); 448 | } 449 | 450 | $app->notFound(); 451 | } 452 | ); 453 | 454 | 455 | // Add contact to favorites 456 | $app->put( 457 | '/contacts/:id/star', 458 | function ($id) use ($app, $log) { 459 | 460 | $contact = \ORM::forTable('contacts')->findOne($id); 461 | 462 | if ($contact) { 463 | $contact->set('favorite', 1); 464 | if (true === $contact->save()) { 465 | $output = $contact->asArray(); 466 | echo json_encode( 467 | $output, 468 | JSON_PRETTY_PRINT 469 | ); 470 | return; 471 | } else { 472 | throw new Exception( 473 | "Unable to save contact" 474 | ); 475 | } 476 | } 477 | 478 | $app->notFound(); 479 | } 480 | ); 481 | 482 | // Remove contact from favorites 483 | $app->delete( 484 | '/contacts/:id/star', 485 | function ($id) use ($app, $log) { 486 | $contact = \ORM::forTable('contacts')->findOne($id); 487 | 488 | if ($contact) { 489 | $contact->set('favorite', 0); 490 | if (true === $contact->save()) { 491 | $output = $contact->asArray(); 492 | echo json_encode( 493 | $output, 494 | JSON_PRETTY_PRINT 495 | ); 496 | return; 497 | } else { 498 | throw new Exception( 499 | "Unable to save contact" 500 | ); 501 | } 502 | } 503 | $app->notFound(); 504 | } 505 | ); 506 | 507 | // Get notes for contact 508 | $app->get( 509 | '/contacts/:id/notes', 510 | function ($id) use ($app, $log) { 511 | 512 | $contact = \ORM::forTable('contacts') 513 | ->select('id')->findOne($id); 514 | 515 | if ($contact) { 516 | $notes = \ORM::forTable('notes') 517 | ->where('contact_id', $id)->findArray(); 518 | echo json_encode($notes, JSON_PRETTY_PRINT); 519 | return; 520 | } 521 | 522 | $app->notFound(); 523 | } 524 | ); 525 | 526 | // Add a new note for contact with id :id 527 | $app->post( 528 | '/contacts/:id/notes', 529 | function ($id) use ($app, $log) { 530 | 531 | $contact = \ORM::forTable('contacts') 532 | ->select('id')->findOne($id); 533 | 534 | if ($contact) { 535 | 536 | $body = $app->request()->getBody(); 537 | 538 | $errors = $app->validateNote($body); 539 | 540 | if (empty($errors)) { 541 | 542 | $note = \ORM::for_table('notes') 543 | ->create(); 544 | 545 | $note->set($body); 546 | $note->contact_id = $id; 547 | 548 | if (true === $note->save()) { 549 | 550 | echo json_encode( 551 | $note->asArray(), 552 | JSON_PRETTY_PRINT 553 | ); 554 | return; 555 | } else { 556 | throw new Exception( 557 | "Unable to save note" 558 | ); 559 | } 560 | 561 | } else { 562 | throw new ValidationException( 563 | "Invalid data", 564 | 0, 565 | $errors 566 | ); 567 | } 568 | 569 | } 570 | $app->notFound(); 571 | } 572 | ); 573 | 574 | // Get single note 575 | $app->get( 576 | '/contacts/:id/notes/:note_id', 577 | function ($id, $note_id) use ($app, $log) { 578 | 579 | $id = filter_var( 580 | filter_var($id, FILTER_SANITIZE_NUMBER_INT), 581 | FILTER_VALIDATE_INT 582 | ); 583 | 584 | if (false === $id) { 585 | throw new ValidationException("Invalid contact ID"); 586 | } 587 | 588 | $note_id = filter_var( 589 | filter_var($note_id, FILTER_SANITIZE_NUMBER_INT), 590 | FILTER_VALIDATE_INT 591 | ); 592 | 593 | if (false === $note_id) { 594 | throw new ValidationException("Invalid note ID"); 595 | } 596 | 597 | $contact = \ORM::forTable('contacts') 598 | ->select('id')->findOne($id); 599 | 600 | if ($contact) { 601 | 602 | $note = \ORM::forTable('notes')->findOne($note_id); 603 | 604 | if ($note) { 605 | 606 | echo json_encode( 607 | $note->asArray(), 608 | JSON_PRETTY_PRINT 609 | ); 610 | return; 611 | } 612 | 613 | } 614 | $app->notFound(); 615 | } 616 | ); 617 | 618 | // Update a single note 619 | $app->map( 620 | '/contacts/:id/notes/:note_id', 621 | function ($id, $note_id) use ($app, $log) { 622 | 623 | $id = filter_var( 624 | filter_var($id, FILTER_SANITIZE_NUMBER_INT), 625 | FILTER_VALIDATE_INT 626 | ); 627 | 628 | if (false === $id) { 629 | throw new ValidationException("Invalid contact ID"); 630 | } 631 | 632 | $note_id = filter_var( 633 | filter_var($note_id, FILTER_SANITIZE_NUMBER_INT), 634 | FILTER_VALIDATE_INT 635 | ); 636 | 637 | if (false === $note_id) { 638 | throw new ValidationException("Invalid note ID"); 639 | } 640 | 641 | $contact = \ORM::forTable('contacts') 642 | ->select('id')->findOne($id); 643 | 644 | if ($contact) { 645 | 646 | $note = \ORM::forTable('notes')->findOne($note_id); 647 | 648 | if ($note) { 649 | 650 | $body = $app->request()->getBody(); 651 | 652 | $errors = $app->validateNote($body, 'update'); 653 | 654 | if (empty($errors)) { 655 | $note->set('body', $body['body']); 656 | if (true === $note->save()) { 657 | 658 | echo json_encode( 659 | $note->asArray(), 660 | JSON_PRETTY_PRINT 661 | ); 662 | return; 663 | 664 | } else { 665 | 666 | throw new Exception( 667 | "Unable to save note" 668 | ); 669 | } 670 | } else { 671 | throw new ValidationException( 672 | "Invalid data", 673 | 0, 674 | $errors 675 | ); 676 | 677 | } 678 | 679 | } 680 | } 681 | $app->notFound(); 682 | } 683 | )->via('PUT', 'PATCH'); 684 | 685 | // Delete single note 686 | $app->delete( 687 | '/contacts/:id/notes/:note_id', 688 | function ($id, $note_id) use ($app, $log) { 689 | 690 | $contact = \ORM::forTable('contacts') 691 | ->select('id')->findOne($id); 692 | 693 | if ($contact) { 694 | 695 | $note = \ORM::forTable('notes')->findOne($note_id); 696 | 697 | if ($note) { 698 | $note->delete(); 699 | $app->halt(204); 700 | } 701 | 702 | } 703 | 704 | $app->notFound(); 705 | } 706 | ); 707 | 708 | } 709 | ); 710 | } 711 | ); 712 | 713 | // Public human readable home page 714 | $app->get( 715 | '/', 716 | function () use ($app, $log) { 717 | echo "

Hello, this can be the public App Interface

"; 718 | } 719 | ); 720 | 721 | // JSON friendly errors 722 | // NOTE: debug must be false 723 | // or default error template will be printed 724 | $app->error(function (\Exception $e) use ($app, $log) { 725 | 726 | $mediaType = $app->request->getMediaType(); 727 | 728 | $isAPI = (bool) preg_match('|^/api/v.*$|', $app->request->getPath()); 729 | 730 | // Standard exception data 731 | $error = array( 732 | 'code' => $e->getCode(), 733 | 'message' => $e->getMessage(), 734 | 'file' => $e->getFile(), 735 | 'line' => $e->getLine(), 736 | ); 737 | 738 | // Graceful error data for production mode 739 | if (!in_array( 740 | get_class($e), 741 | array('API\\Exception', 'API\\Exception\ValidationException') 742 | ) 743 | && 'production' === $app->config('mode')) { 744 | $error['message'] = 'There was an internal error'; 745 | unset($error['file'], $error['line']); 746 | } 747 | 748 | // Custom error data (e.g. Validations) 749 | if (method_exists($e, 'getData')) { 750 | $errors = $e->getData(); 751 | } 752 | 753 | if (!empty($errors)) { 754 | $error['errors'] = $errors; 755 | } 756 | 757 | $log->error($e->getMessage()); 758 | if ('application/json' === $mediaType || true === $isAPI) { 759 | $app->response->headers->set( 760 | 'Content-Type', 761 | 'application/json' 762 | ); 763 | echo json_encode($error, JSON_PRETTY_PRINT); 764 | } else { 765 | echo ' 766 | Error 767 |

Error: ' . $error['code'] . '

' 768 | . $error['message'] 769 | .'

'; 770 | } 771 | 772 | }); 773 | 774 | /// Custom 404 error 775 | $app->notFound(function () use ($app) { 776 | 777 | $mediaType = $app->request->getMediaType(); 778 | 779 | $isAPI = (bool) preg_match('|^/api/v.*$|', $app->request->getPath()); 780 | 781 | 782 | if ('application/json' === $mediaType || true === $isAPI) { 783 | 784 | $app->response->headers->set( 785 | 'Content-Type', 786 | 'application/json' 787 | ); 788 | 789 | echo json_encode( 790 | array( 791 | 'code' => 404, 792 | 'message' => 'Not found' 793 | ), 794 | JSON_PRETTY_PRINT 795 | ); 796 | 797 | } else { 798 | echo ' 799 | 404 Page Not Found 800 |

404 Page Not Found

The page you are 801 | looking for could not be found.

'; 802 | } 803 | }); 804 | 805 | $app->run(); 806 | -------------------------------------------------------------------------------- /share/config/default.php: -------------------------------------------------------------------------------- 1 | 'sqlite', 8 | 'dbname' => $_ENV['SLIM_MODE'] . '.sqlite', 9 | 'dbpath' => realpath(__DIR__ . '/../db') 10 | ); 11 | 12 | 13 | $config['db']['dsn'] = sprintf( 14 | '%s:%s/%s', 15 | $config['db']['driver'], 16 | $config['db']['dbpath'], 17 | $config['db']['dbname'] 18 | ); 19 | 20 | $config['app']['mode'] = $_ENV['SLIM_MODE']; 21 | 22 | // Cache TTL in seconds 23 | $config['app']['cache.ttl'] = 60; 24 | 25 | // Max requests per hour 26 | $config['app']['rate.limit'] = 1000; 27 | 28 | $config['app']['log.writer'] = new \Flynsarmy\SlimMonolog\Log\MonologWriter(array( 29 | 'handlers' => array( 30 | new \Monolog\Handler\StreamHandler( 31 | realpath(__DIR__ . '/../logs') 32 | .'/'.$_ENV['SLIM_MODE'] . '_' .date('Y-m-d').'.log' 33 | ), 34 | ), 35 | )); 36 | -------------------------------------------------------------------------------- /share/db/.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swader/REST/5d42e639f1dbc47dfb187364550673d4a81a583f/share/db/.sqlite -------------------------------------------------------------------------------- /share/sql/data/contacts.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Camilla","Donati","ridiculus.mus.Proin@Quisquepurus.co.uk","(815) 195-5424"); 2 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Erica","Ferraro","dui.quis@sollicitudinorcisem.com","(574) 384-5826"); 3 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Dario","Marchetti","pede.nec@lacusvestibulumlorem.edu","(950) 737-5317"); 4 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Camilla","Coppola","diam.eu@mauris.edu","(671) 299-6391"); 5 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Elisa","Gatti","arcu@Nulla.net","(441) 370-5344"); 6 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Francesco","Ferrari","ornare.libero.at@fermentumrisusat.edu","(750) 440-7845"); 7 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Giovanni","Mancini","vitae.mauris.sit@tellusnonmagna.com","(579) 716-9757"); 8 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Sara","Ferrante","adipiscing.lacus@ligula.net","(530) 782-1007"); 9 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Emanuele","Giordano","Fusce@erat.ca","(533) 689-1586"); 10 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Silvia","Marra","Aliquam.fringilla.cursus@arcuSed.co.uk","(614) 609-1589"); 11 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Erica","Villani","Etiam.ligula.tortor@montesnascetur.org","(231) 520-9921"); 12 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Simona","Piras","mauris@ipsumCurabiturconsequat.com","(327) 108-4448"); 13 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Alessio","Catalano","lectus.Nullam@bibendum.net","(877) 777-0026"); 14 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Ginevra","Montanari","quis@consequatpurus.edu","(928) 549-9206"); 15 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Nicola","Bellini","dolor@magnatellusfaucibus.edu","(200) 882-6342"); 16 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Noemi","Fontana","in.consequat@utnulla.com","(489) 865-3158"); 17 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Fabio","Palmieri","ut.nisi@scelerisque.co.uk","(554) 778-9877"); 18 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Monica","Castelli","auctor.quis.tristique@nisiCumsociis.edu","(415) 659-4432"); 19 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Lorenzo","Parisi","purus.in.molestie@ipsum.com","(510) 233-4655"); 20 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Gianpiero","Ruggeri","Suspendisse@nullaInteger.edu","(428) 347-2676"); 21 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Giada","Pellegrino","nisi.Cum.sociis@cursus.org","(966) 923-6133"); 22 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Domenico","Albanese","pede.Praesent.eu@sapiengravida.net","(242) 608-7335"); 23 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Cristiano","Ferri","dolor.Nulla@variusNam.com","(532) 187-3507"); 24 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Anna","Donati","pede@diam.co.uk","(665) 699-6419"); 25 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Arianna","Sala","molestie@egestasSedpharetra.edu","(157) 654-3242"); 26 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Silvia","Montanari","id.blandit@tempor.org","(244) 414-7169"); 27 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Cristina","Giordano","bibendum.sed@tincidunt.org","(312) 242-3682"); 28 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Paolo","Fumagalli","Ut@blanditviverra.ca","(599) 732-5058"); 29 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Alessio","Ceccarelli","arcu@In.co.uk","(933) 478-3010"); 30 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Matilde","Costantini","enim.Curabitur.massa@odioAliquam.ca","(650) 173-3443"); 31 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Luca","Marino","amet@arcu.co.uk","(815) 924-6730"); 32 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Antonio","Romeo","amet.ultricies@felisDonec.co.uk","(823) 403-5040"); 33 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Angela","Santoro","scelerisque@seddictumeleifend.com","(560) 323-3311"); 34 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Valerio","Valente","quis@urnaNuncquis.net","(481) 200-1813"); 35 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Federica","Rizzo","ac.mattis@duiaugueeu.co.uk","(779) 319-6428"); 36 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Alessio","Farina","ipsum.dolor.sit@Sed.co.uk","(217) 426-6110"); 37 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Daniele","Piras","fringilla@loremipsum.com","(272) 462-7501"); 38 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Luigi","Rizzi","penatibus.et.magnis@ascelerisquesed.org","(736) 281-6354"); 39 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Alberto","Catalano","natoque@nonluctussit.ca","(924) 436-9002"); 40 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Luigi","Mariani","parturient.montes.nascetur@Donec.edu","(356) 747-2807"); 41 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Sara","Ceccarelli","tempor.augue@dolordapibus.co.uk","(238) 247-5724"); 42 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Aurora","Farina","gravida.nunc@ante.com","(863) 900-3146"); 43 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Elisa","Monti","felis.Nulla.tempor@morbitristique.com","(991) 862-1199"); 44 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Samuele","Sala","parturient.montes@Morbi.co.uk","(717) 381-6151"); 45 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Emanuele","Fiore","nisl.Quisque.fringilla@dolorsit.org","(272) 128-8329"); 46 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Giovanni","Rinaldi","dolor.nonummy.ac@Nullafacilisi.net","(731) 808-7684"); 47 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Gianluca","De Simone","adipiscing.elit.Curabitur@CuraeDonec.net","(443) 599-7477"); 48 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Fabio","Mancini","purus.sapien@Maurisvelturpis.org","(295) 764-9970"); 49 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Alex","Gentile","dictum.cursus.Nunc@elitpretium.org","(280) 312-2829"); 50 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Viola","Conte","Sed.dictum@miac.org","(971) 491-2667"); 51 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Elena","Barone","bibendum.sed@Integerinmagna.com","(279) 570-6383"); 52 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Eleonora","Gallo","aptent.taciti@tempus.com","(604) 928-8550"); 53 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Giulia","Cattaneo","mattis.Cras@semut.net","(358) 211-7476"); 54 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Paolo","Mele","neque@sodaleselit.com","(313) 668-4379"); 55 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Federica","Monaco","odio@elementumsem.org","(476) 761-7517"); 56 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Diego","Conti","interdum.Nunc.sollicitudin@etnetuset.net","(120) 357-1250"); 57 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Giacomo","Parisi","ante.lectus@Duis.ca","(736) 550-7890"); 58 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Rebecca","Moretti","risus.a.ultricies@molestieSed.org","(729) 911-4851"); 59 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Ginevra","Mariani","dolor@fames.co.uk","(415) 950-4945"); 60 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Salvatore","Ferrero","suscipit@Cras.co.uk","(845) 230-6291"); 61 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Michele","Morelli","eget.odio@tempor.net","(453) 866-0053"); 62 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Diego","Marchi","et.rutrum@ligula.org","(187) 827-7319"); 63 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Francesco","Proietti","convallis.est.vitae@ategestas.net","(240) 821-4238"); 64 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Ginevra","Monaco","Donec.tempor.est@nibhAliquamornare.co.uk","(986) 172-6480"); 65 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Nicole","Amato","accumsan@cursusvestibulum.com","(491) 537-2574"); 66 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Giovanni","Moro","aliquet@dapibusligula.net","(672) 620-1008"); 67 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Stefano","Amato","eu@lacus.co.uk","(765) 327-2495"); 68 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Enrico","Rossetti","eget.tincidunt@tristiquepellentesque.edu","(403) 825-8200"); 69 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Mirko","Guerra","euismod.urna@mauriseu.edu","(747) 545-8440"); 70 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Francesco","Ruggeri","metus.facilisis.lorem@egestas.ca","(706) 305-0476"); 71 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Luca","Giordano","lacus.Aliquam@vestibulum.ca","(299) 843-7847"); 72 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Michele","Martino","tristique@nisi.edu","(782) 216-8705"); 73 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Giovanni","Farina","dictum@Phasellusdolorelit.org","(303) 415-8239"); 74 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Viola","Coppola","Nullam.vitae.diam@Donec.co.uk","(858) 668-0156"); 75 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Margherita","Catalano","sapien.imperdiet.ornare@justo.ca","(373) 816-0729"); 76 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Emanuele","Basile","aliquet.lobortis@Donecest.ca","(106) 704-2750"); 77 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Ilaria","Cirillo","metus.urna@Integereu.co.uk","(110) 597-1869"); 78 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Marta","Poli","aliquet@ullamcorperviverra.com","(218) 408-0586"); 79 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Paola","Valente","non@Aenean.edu","(722) 290-1047"); 80 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Sara","Esposito","faucibus.leo@venenatisamagna.org","(145) 550-1337"); 81 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Erica","Ferrara","rutrum@Nunc.ca","(406) 547-3782"); 82 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Angelo","Gentile","lectus.pede@Proin.edu","(613) 164-8595"); 83 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Vanessa","Milani","ut.ipsum.ac@bibendum.co.uk","(250) 704-9886"); 84 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Margherita","Olivieri","eget.odio.Aliquam@Etiamimperdiet.org","(368) 877-9735"); 85 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Edoardo","Aiello","vitae.sodales@et.edu","(283) 462-4473"); 86 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Gabriele","Pace","tellus@Sed.org","(974) 193-3208"); 87 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Francesca","Marra","elit.Curabitur@sociisnatoque.co.uk","(443) 169-7667"); 88 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Giuseppe","Longo","lorem.eget.mollis@ac.co.uk","(406) 862-8199"); 89 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Alessandra","Ferro","urna.et.arcu@vitaealiquetnec.ca","(537) 126-0818"); 90 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Gianpiero","Lorusso","placerat.augue@purusNullam.edu","(572) 986-2888"); 91 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Cristiano","Ferri","eget.massa@nulla.org","(444) 819-5120"); 92 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Giulia","Pepe","lacus@tristiqueaceleifend.co.uk","(348) 647-6149"); 93 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Diego","Esposito","tellus.Suspendisse@erat.net","(962) 277-4175"); 94 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Roberto","Bellini","Etiam@adipiscing.edu","(108) 259-8043"); 95 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Manuel","Battaglia","felis.Nulla.tempor@consequat.co.uk","(196) 797-3108"); 96 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Cristian","Montanari","vel.arcu@accumsanconvallisante.co.uk","(152) 957-3869"); 97 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Sara","De Santis","dignissim.Maecenas.ornare@urna.co.uk","(151) 126-2599"); 98 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Paolo","Longo","orci@quamelementum.org","(834) 444-4260"); 99 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Emanuele","Palumbo","sit.amet@lacusUtnec.net","(313) 884-1326"); 100 | INSERT INTO `contacts` (`firstname`,`lastname`,`email`,`phone`) VALUES ("Valerio","Santoro","ac@Aliquam.org","(224) 412-4274"); 101 | -------------------------------------------------------------------------------- /share/sql/data/users.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `users` (`email`,`apikey`) VALUES ("me@example.com", "MyVeryLongAPIKey"); 2 | -------------------------------------------------------------------------------- /share/sql/tables/contacts.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS `contacts`; 2 | 3 | CREATE TABLE IF NOT EXISTS `contacts` ( 4 | `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 5 | `firstname` varchar(255) default NULL, 6 | `lastname` varchar(255) default NULL, 7 | `email` varchar(255) UNIQUE default NULL, 8 | `phone` varchar(100) default NULL, 9 | `favorite` INTEGER NOT NULL DEFAULT 0 10 | ); 11 | -------------------------------------------------------------------------------- /share/sql/tables/notes.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS `notes`; 2 | 3 | CREATE TABLE IF NOT EXISTS `notes` ( 4 | `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 5 | `body` TEXT NOT NULL, 6 | `contact_id` INTEGER NOT NULL 7 | ); 8 | -------------------------------------------------------------------------------- /share/sql/tables/users.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS `users`; 2 | 3 | CREATE TABLE IF NOT EXISTS `users` ( 4 | `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 5 | `email` TEXT NOT NULL, 6 | `apikey` TEXT NOT NULL 7 | ); 8 | --------------------------------------------------------------------------------