├── LICENSE ├── api ├── .htaccess ├── Api.php ├── Apis │ └── Records.php └── index.php ├── index.php ├── test_cookie.php └── test_header.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Arminas Žukauskas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /api/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine on 2 | RewriteBase /api/ 3 | RewriteCond %{REQUEST_FILENAME} !-f 4 | RewriteCond %{REQUEST_FILENAME} !-d 5 | RewriteRule .* index.php/$0 [PT] 6 | -------------------------------------------------------------------------------- /api/Api.php: -------------------------------------------------------------------------------- 1 | array( 26 | 'regex' => 'system', 27 | ), 28 | 'records' => array( 29 | 'regex' => 'records(?:/?([0-9]+)?)', 30 | ), 31 | ); 32 | 33 | public static $input = null; 34 | public static $input_data = array(); 35 | 36 | public static function serve() 37 | { 38 | $path_info = '/'; 39 | 40 | // Parse needed information from PATH_INFO or REQUEST_URI 41 | if (!empty($_SERVER['PATH_INFO'])) { 42 | $path_info = $_SERVER['PATH_INFO']; 43 | } else { 44 | if (!empty($_SERVER['REQUEST_URI'])) { 45 | if (strpos($_SERVER['REQUEST_URI'], '?') > 0) { 46 | $path_info = strstr($_SERVER['REQUEST_URI'], '?', true); 47 | } else { 48 | $path_info = $_SERVER['REQUEST_URI']; 49 | } 50 | } 51 | } 52 | 53 | // Support for api/{version}/whatever{.format} 54 | preg_match('#^/?([^/]+?)/.+?\.(.+?)$#', $path_info, $request_info); 55 | 56 | // Check if we have version and format in url 57 | if (!$request_info || !isset($request_info[2])) { 58 | // Should throw 404 here 59 | return false; 60 | } 61 | 62 | self::$request_version = $request_info[1]; 63 | self::$request_format = $request_info[2]; 64 | 65 | // Check version 66 | if (!in_array(self::$request_version, self::$supported_versions)) { 67 | // Should throw 406 Unsupported version here 68 | return false; 69 | } 70 | 71 | // Check format 72 | if (!in_array(self::$request_format, self::$supported_formats)) { 73 | // Should throw 406 Unsupported format here 74 | return false; 75 | } 76 | 77 | self::$input = file_get_contents('php://input'); 78 | 79 | // For PUT/DELETE there is input data instead of request variables 80 | if (!empty(self::$input)) { 81 | preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches); 82 | if (isset($matches[1]) && strpos(self::$input, $matches[1]) !== false) { 83 | $this->parse_raw_request(self::$input, self::$input_data); 84 | } else { 85 | parse_str(self::$input, self::$input_data); 86 | } 87 | } 88 | 89 | $request_method = strtolower($_SERVER['REQUEST_METHOD']); 90 | 91 | // If this is OPTIONS request return it right now 92 | if ($request_method == 'options') { 93 | Api::outputHeaders(); 94 | } else { 95 | $handler = null; 96 | 97 | // How url should start, example: /api/v1.0/ 98 | $url_start = '/(?:'.implode('|', self::$supported_versions).')/'; 99 | 100 | // How url should end, example: .json 101 | $url_end = '\.(?:'.implode('|', self::$supported_formats).')'; 102 | 103 | foreach (self::$public_routes as $handler_name => $route_config) { 104 | $regex = $url_start.$route_config['regex'].$url_end; 105 | 106 | if (preg_match('#^'.$regex.'$#', $path_info, $params_matches)) { 107 | $handler = $handler_name; 108 | break; 109 | } 110 | } 111 | 112 | if (!$handler) { 113 | // Some 404 action 114 | } 115 | 116 | $classname = 'Api_'.ucfirst($handler); 117 | $api_object = new $classname(); 118 | 119 | if (!method_exists($api_object, $request_method)) { 120 | // Some 404 action 121 | } 122 | 123 | // Finally call to our inner class 124 | call_user_func_array(array($api_object, $request_method), $params_matches); 125 | } 126 | } 127 | 128 | /** 129 | * Helper method to parse raw requests 130 | */ 131 | private function parse_raw_request($input, &$a_data) 132 | { 133 | // grab multipart boundary from content type header 134 | preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches); 135 | $boundary = $matches[1]; 136 | 137 | // split content by boundary and get rid of last -- element 138 | $a_blocks = preg_split("/-+$boundary/", $input); 139 | array_pop($a_blocks); 140 | 141 | // loop data blocks 142 | foreach ($a_blocks as $id => $block) { 143 | if (empty($block)) { 144 | continue; 145 | } 146 | 147 | // parse uploaded files 148 | if (strpos($block, 'application/octet-stream') !== false) { 149 | // match "name", then everything after "stream" (optional) except for prepending newlines 150 | preg_match("/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s", $block, $matches); 151 | // parse all other fields 152 | } else { 153 | // match "name" and optional value in between newline sequences 154 | preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $block, $matches); 155 | } 156 | 157 | $a_data[$matches[1]] = $matches[2]; 158 | } 159 | } 160 | 161 | // This method will handle both cross origin and same domain requests 162 | public static function outputHeaders($cookies = array()) 163 | { 164 | $referer = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : null; 165 | 166 | if (!$referer) { 167 | $referer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null; 168 | } 169 | 170 | $origin = '*'; 171 | 172 | // If we have referer information try to parse it 173 | if ($referer) { 174 | $info = parse_url($referer); 175 | 176 | if ($info && isset($info['scheme']) && ($info['scheme'] == 'http' || $info['scheme'] == 'https')) { 177 | $origin = $info['host']; 178 | 179 | if ($origin == $_SERVER['HTTP_HOST']) { 180 | $origin = $info['scheme'].'://'.$origin; 181 | } else { 182 | $origin = '*'; 183 | } 184 | } 185 | } 186 | 187 | // Do not send any cookies that might be issued 188 | header_remove('Set-Cookie'); 189 | 190 | // If this is packaged app or request from 3rd party, append auth token to the headers 191 | if ($origin == '*' || (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']) && !empty($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']))) { 192 | header('Access-Control-Allow-Origin: *'); 193 | header('Access-Control-Expose-Headers: x-authorization'); 194 | header('Access-Control-Allow-Headers: origin, content-type, accept, x-authorization'); 195 | header('X-Authorization: '.YOUR_TOKEN_HERE); 196 | // Or if this is simple crossdomain call from our domain 197 | } else { 198 | header('Access-Control-Allow-Origin: '.$origin); 199 | header('Access-Control-Expose-Headers: set-cookie, cookie'); 200 | header('Access-Control-Allow-Headers: origin, content-type, accept, set-cookie, cookie'); 201 | 202 | // Allow cookie credentials because we're on the same domain 203 | header('Access-Control-Allow-Credentials: true'); 204 | 205 | // Let's set all the cookies we want except for options method. It does not support them. 206 | if (strtolower($_SERVER['REQUEST_METHOD']) != 'options') { 207 | setcookie(TOKEN_COOKIE_NAME, YOUR_TOKEN_HERE, time()+86400*30, '/', '.'.$_SERVER['HTTP_HOST']); 208 | 209 | // Any other cookies 210 | if (sizeof($cookies)) { 211 | foreach ($cookies as $cookie) { 212 | call_user_func_array('setcookie', $cookie); 213 | } 214 | } 215 | } 216 | } 217 | 218 | header('Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE'); 219 | header('Access-Control-Max-Age: 86400'); 220 | } 221 | 222 | public static function responseOk($result = array(), $metadata = array(), $cookies = array()) 223 | { 224 | // For now we will support only this 225 | if (self::$request_format == 'json') { 226 | http_response_code(200); 227 | header('Content-type: application/json; charset=utf-8'); 228 | self::outputHeaders($cookies); 229 | 230 | echo json_encode(array( 231 | 'metadata' => $metadata, 232 | 'status' => self::STATUS_OK, 233 | 'result' => $result, 234 | )); 235 | } 236 | } 237 | 238 | public static function responseError($code = 404, $info = null) 239 | { 240 | http_response_code($code); 241 | if (self::$request_format == 'json') { 242 | header('Content-type: application/json; charset=utf-8'); 243 | self::outputHeaders(); 244 | 245 | echo json_encode(array( 246 | 'status' => self::STATUS_ERR, 247 | 'info' => $info, 248 | )); 249 | } 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /api/Apis/Records.php: -------------------------------------------------------------------------------- 1 | getRecord(intval($id)); 17 | } else { 18 | return $this->getRecords(); 19 | } 20 | } 21 | 22 | private function getRecord($record_id) 23 | { 24 | // In real world there would be call to model with validation and probably token checking 25 | $record = array('title'=>'Foo', 'content'=>'Bar', 'tags'=>array('abc', 'xyz')); 26 | return Api::responseOk($this->format($record)); 27 | } 28 | 29 | private function getRecords() 30 | { 31 | // In real world there would be call to model with validation and probably token checking 32 | $records = array( 33 | array('title'=>'Foo1', 'content'=>'Bar1', 'tags'=>array('abc', 'xyz')), 34 | array('title'=>'Foo2', 'content'=>'Bar2', 'tags'=>array('abc')), 35 | array('title'=>'Foo3', 'content'=>'Bar3', 'tags'=>array('xyz')), 36 | ); 37 | 38 | $current_page = 1; 39 | $items_on_page = 10; 40 | $total_pages = 4; 41 | $items_count = 37; 42 | 43 | $records_return = array(); 44 | foreach ($records as $record) { 45 | $records_return[]= $this->format($record); 46 | } 47 | 48 | $_metadata = array( 49 | 'total_pages' => $total_pages, 50 | 'per_page' => $items_on_page, 51 | 'current_page' => $current_page, 52 | 'total_count' => $items_count, 53 | ); 54 | 55 | return Api::responseOk($records_return, $_metadata); 56 | } 57 | 58 | /** 59 | * Method to format item output, return only whitelisted fields 60 | */ 61 | private function format($record) 62 | { 63 | $return = array( 64 | 'title' => null, 65 | 'content' => null, 66 | ); 67 | 68 | $return['title'] = $record['title']; 69 | $return['content'] = $record['content']; 70 | 71 | return $return; 72 | } 73 | 74 | /** 75 | * Update record 76 | */ 77 | public function put($record_id = null) 78 | { 79 | // In real world there would be call to model with validation and probably token checking 80 | 81 | // Use Api::$input_data to update 82 | return Api::responseOk('OK', array()); 83 | } 84 | 85 | /** 86 | * Create record 87 | */ 88 | public function post() 89 | { 90 | // In real world there would be call to model with validation and probably token checking 91 | 92 | $info = array('record'=>array('id'=>7)); 93 | 94 | return Api::responseOk($info); 95 | } 96 | 97 | /** 98 | * Delete record 99 | */ 100 | public function delete( $id = null ) 101 | { 102 | // In real world there would be call to model with validation and probably token checking 103 | 104 | return Api::responseOk('OK', array()); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /api/index.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | Cookie test 4 | 5 | 30 | 31 | 32 | Launch this from same domain as api and see the console. 33 | 34 | 35 | -------------------------------------------------------------------------------- /test_header.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | Custom header test 4 | 5 | 33 | 34 | 35 | Launch this from different domain as api and see the console. 36 | 37 | 38 | --------------------------------------------------------------------------------