├── views └── rest │ └── html.php ├── config └── rest.php ├── classes ├── rest │ ├── model.php │ ├── controller.php │ ├── auth.php │ ├── content │ │ ├── csv.php │ │ ├── rss.php │ │ ├── xml.php │ │ ├── atom.php │ │ ├── html.php │ │ ├── json.php │ │ └── rdf.php │ ├── cors.php │ ├── method │ │ ├── get.php │ │ ├── put.php │ │ ├── head.php │ │ ├── patch.php │ │ ├── post.php │ │ ├── trace.php │ │ ├── delete.php │ │ ├── options.php │ │ ├── basic.php │ │ └── all.php │ └── core.php ├── rest.php ├── controller │ ├── rest.php │ └── template │ │ └── rest.php └── model │ └── rest │ └── test.php ├── init.php └── README.md /views/rest/html.php: -------------------------------------------------------------------------------- 1 | 2 |
3 | -------------------------------------------------------------------------------- /config/rest.php: -------------------------------------------------------------------------------- 1 | FALSE 6 | ); 7 | -------------------------------------------------------------------------------- /classes/rest/model.php: -------------------------------------------------------------------------------- 1 | ((/)(.))') 14 | ->defaults(array( 15 | 'controller' => 'rest' 16 | )); 17 | -------------------------------------------------------------------------------- /classes/controller/template/rest.php: -------------------------------------------------------------------------------- 1 | _rest = REST::instance($this) 29 | ->method_override(TRUE) 30 | ->content_override(TRUE) 31 | ->execute(); 32 | } 33 | 34 | public function action_html() 35 | { 36 | $values = $this->_rest->result(); 37 | $view = View::factory('rest/html', array('values' => $values)); 38 | $this->response->body($view); 39 | } 40 | 41 | public function action_json() 42 | { 43 | $json = $this->_rest->etag()->result_json(); 44 | $this->response->body($json); 45 | } 46 | 47 | public function action_xml() 48 | { 49 | $xml = $this->_rest->etag()->result_xml(); 50 | $this->response->body($xml->asXML()); 51 | } 52 | 53 | public function action_csv() 54 | { 55 | $csv = $this->_rest->result_csv(); 56 | $this->response->body($csv); 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /classes/model/rest/test.php: -------------------------------------------------------------------------------- 1 | 1, 'title' => 'one', 'author' => 'Jeremy', 'content' => 'Hi'), 18 | array('id' => 2, 'title' => 'two', 'author' => 'Tara', 'content' => 'Hello'), 19 | array('id' => 3, 'title' => 'three', 'author' => 'Isaac', 'content' => 'Hey'), 20 | array('id' => 4, 'title' => 'four', 'author' => 'Zander', 'content' => 'Yo'), 21 | array('id' => 5, 'title' => 'five', 'author' => 'Bryan', 'content' => 'Holla'), 22 | array('id' => 6, 'title' => 'six', 'author' => 'Jon', 'content' => 'Ciao'), 23 | array('id' => 7, 'title' => 'seven', 'author' => 'Leah', 'content' => 'How do you do?'), 24 | array('id' => 8, 'title' => 'eight', 'author' => 'Sean', 'content' => 'Bonjour'), 25 | array('id' => 9, 'title' => 'nine', 'author' => 'Scott', 'content' => 'what\'s up') 26 | ); 27 | 28 | public function rest_auth(Rest $rest) 29 | { 30 | $user = Auth::instance()->get_user(); 31 | return $rest->method() == 'OPTIONS' OR $user !== FALSE; 32 | } 33 | 34 | /** 35 | * Cross-Origin Resource Sharing 36 | * 37 | * @param Rest $rest 38 | */ 39 | public function rest_cors(Rest $rest) 40 | { 41 | $origin = $rest->request()->headers('Origin'); 42 | if (in_array($origin, self::$origin)) 43 | { 44 | $rest->cors(array('origin' => $origin, 'creds' => 'true')); 45 | } 46 | } 47 | 48 | /** 49 | * Cross-Origin Resource Sharing 50 | * 51 | * @param Rest $rest 52 | */ 53 | public function rest_options(Rest $rest) 54 | { 55 | $rest->send_code(200); 56 | } 57 | 58 | /** 59 | * Returns test data 60 | * 61 | * @param Rest $rest 62 | */ 63 | public function rest_get(Rest $rest) 64 | { 65 | $data = Session::instance()->get('rest_test_data', $this->_data); 66 | $id = $rest->param('id'); 67 | if ( ! empty($id)) 68 | { 69 | if ( ! isset($data[$id])) 70 | { 71 | throw new Http_Exception_404('Resource not found, ID: :id', array(':id' => $id)); 72 | } 73 | return $data[$id]; 74 | 75 | } 76 | else 77 | { 78 | return $data; 79 | } 80 | } 81 | 82 | /** 83 | * 84 | * @param Rest $rest 85 | */ 86 | public function rest_put(Rest $rest) 87 | { 88 | $id = $rest->param('id'); 89 | if ( ! empty($id)) 90 | { 91 | $data = Session::instance()->get('rest_test_data', $this->_data); 92 | $data[$id] = $rest->body('json', TRUE); 93 | Session::instance()->set('rest_test_data', $data); 94 | return $data[$id]; 95 | } 96 | else 97 | { 98 | // TODO 99 | $rest->send_code(403); //Forbidden 100 | } 101 | } 102 | 103 | /** 104 | * 105 | * @param Rest $rest 106 | */ 107 | public function rest_post(Rest $rest) 108 | { 109 | $data = Session::instance()->get('rest_test_data', $this->_data); 110 | $post = $rest->post(); 111 | if (empty($post)) 112 | { 113 | $post = $rest->body('json', TRUE); 114 | } 115 | $data[] = $post; 116 | Session::instance()->set('rest_test_data', $data); 117 | $rest->send_created(count($data)+1); 118 | } 119 | 120 | /** 121 | * 122 | * @param Rest $rest 123 | */ 124 | public function rest_delete(Rest $rest) 125 | { 126 | $id = $rest->param('id'); 127 | $data = Session::instance()->get('rest_test_data', $this->_data); 128 | if (isset($data[$id])) 129 | { 130 | unset($data[$id]); 131 | Session::instance()->set('rest_test_data', $data); 132 | } 133 | $rest->send_code(204); 134 | } 135 | 136 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Kohana RESTful Web Service Library 2 | * Author: Jeremy Fowler 3 | * Copyright: (c) 2012 Jeremy Fowler 4 | * License: http://www.opensource.org/licenses/BSD-3-Clause 5 | 6 | ##Requires 7 | * Kohana >= 3.2 8 | * PHP >= 5.3 9 | 10 | ##Features 11 | * X-HTTP-METHOD-OVERRIDE support 12 | * Cross-Origin Resource Sharing 13 | * ETags 14 | 15 | ##Installation 16 | 17 | * `cd modules` 18 | * `git clone git://github.com/jerfowler/REST.git` 19 | * Enable the REST module in bootstrap 20 | * Create REST extended models in classes/model/rest 21 | * Optional: create your own custom controller_rest 22 | 23 | ##Controllers handle content type and output 24 | Each controller must implement one or more of the REST_Content Interfaces 25 | 26 | ```php 27 | class Controller_Template_REST extends Controller 28 | implements REST_Content_HTML, 29 | REST_Content_JSON, 30 | REST_Content_XML, 31 | REST_Content_CSV { 32 | ``` 33 | ###The Rest module gets instantiated in the before method of the controller 34 | * The model is determined by the Controller's action 35 | * REST supports X-HTTP-METHOD-OVERRIDE by using `method_override(TRUE)` 36 | 37 | ```php 38 | /** 39 | * Rest object 40 | * @var Rest 41 | */ 42 | protected $_rest; 43 | 44 | public function before() 45 | { 46 | parent::before(); 47 | 48 | $this->_rest = REST::instance($this) 49 | ->method_override(TRUE) 50 | ->content_override(TRUE) 51 | ->execute(); 52 | } 53 | ``` 54 | 55 | ####Content-Type is auto-detected by the headers (as is Language & Charset) 56 | This can be overridden by using `content_override(TRUE)` and using a special route 57 | 58 | ```php 59 | Route::set('rest', 'rest/((/)(.))') 60 | ->defaults(array( 61 | 'controller' => 'rest' 62 | )); 63 | ``` 64 | 65 | ###Output is handled by the various REST_Content Interface Methods 66 | 67 | * Models pass the values generated by the HTTP method which is then retrieved by `result()` and then various other `result_x()` helper functions. 68 | * ETags read/generated using the `etag()` method 69 | 70 | ```php 71 | public function action_html() 72 | { 73 | $values = $this->_rest->result(); 74 | $view = View::factory('rest/html', array('values' => $values)); 75 | $this->response->body($view); 76 | } 77 | 78 | public function action_json() 79 | { 80 | $json = $this->_rest->etag()->result_json(); 81 | $this->response->body($json); 82 | } 83 | 84 | public function action_xml() 85 | { 86 | $xml = $this->_rest->etag()->result_xml(); 87 | $this->response->body($xml->asXML()); 88 | } 89 | 90 | public function action_csv() 91 | { 92 | $csv = $this->_rest->result_csv(); 93 | $this->response->body($csv); 94 | } 95 | ``` 96 | 97 | ##Models handle the HTTP methods 98 | * Each model must implement one or more of the REST_Method Interfaces 99 | * Model names are pluralized 100 | 101 | ```php 102 | class Model_REST_Users 103 | implements REST_CORS, 104 | REST_Method_Get, 105 | REST_Method_Post { 106 | ``` 107 | ###Each HTTP method is handled by the corresponding Interface method 108 | 109 | ```php 110 | /** 111 | * Cross-Origin Resource Sharing 112 | */ 113 | public function rest_cors(Rest $rest) 114 | { 115 | $origin = $rest->request()->headers('Origin'); 116 | if (in_array($origin, self::$origin)) 117 | { 118 | $rest->cors(array('origin' => $origin, 'creds' => 'true')); 119 | } 120 | } 121 | 122 | public function rest_options(Rest $rest) 123 | { 124 | $rest->send_code(200); 125 | } 126 | 127 | public function rest_get(Rest $rest) 128 | { 129 | $data = Session::instance()->get('rest_test_data', $this->_data); 130 | $id = $rest->param('id'); 131 | if ( ! empty($id)) 132 | { 133 | if ( ! isset($data[$id])) 134 | { 135 | $rest->send_code(404); //Not Found 136 | } 137 | return $data[$id]; 138 | 139 | } 140 | else 141 | { 142 | return $data; 143 | } 144 | } 145 | 146 | public function rest_put(Rest $rest) 147 | { 148 | $id = $rest->param('id'); 149 | if ( ! empty($id)) 150 | { 151 | $data = Session::instance()->get('rest_test_data', $this->_data); 152 | $data[$id] = $rest->body('json', TRUE); 153 | Session::instance()->set('rest_test_data', $data); 154 | return $data[$id]; 155 | } 156 | else 157 | { 158 | $rest->send_code(403); //Forbidden 159 | } 160 | 161 | } 162 | 163 | public function rest_post(Rest $rest) 164 | { 165 | $data = Session::instance()->get('rest_test_data', $this->_data); 166 | $post = $rest->post(); 167 | if (empty($post)) 168 | { 169 | $post = $rest->body('json', TRUE); 170 | } 171 | $data[] = $post; 172 | Session::instance()->set('rest_test_data', $data); 173 | $rest->send_created(count($data)+1); 174 | } 175 | 176 | public function rest_delete(Rest $rest) 177 | { 178 | $id = $rest->param('id'); 179 | $data = Session::instance()->get('rest_test_data', $this->_data); 180 | if (isset($data[$id])) 181 | { 182 | unset($data[$id]); 183 | Session::instance()->set('rest_test_data', $data); 184 | } 185 | $rest->send_code(204); 186 | } 187 | ``` -------------------------------------------------------------------------------- /classes/rest/core.php: -------------------------------------------------------------------------------- 1 | 'rest_', 18 | 'model' => 'Model_REST_', 19 | 'method' => 'REST_Method_', 20 | 'content' => 'REST_Content_' 21 | ); 22 | public static $_methods = array( 23 | 'GET', 24 | 'PUT', 25 | 'POST', 26 | 'DELETE', 27 | 'HEAD', 28 | 'TRACE', 29 | 'PATCH', 30 | 'OPTIONS' 31 | ); 32 | public static $_types = array( 33 | 'text/html' => 'html', 34 | 'application/json' => 'json', 35 | 'application/xml' => 'xml', 36 | 'application/rdf+xml' => 'rdf', 37 | 'application/rss+xml' => 'rss', 38 | 'application/atom+xml' => 'atom', 39 | 'application/vnd.ms-excel' => 'csv' 40 | ); 41 | public static $_cors = array( 42 | 'origin' => '*', 43 | 'methods' => null, 44 | 'headers' => array('Origin', 'Accept', 'Accept-Language', 'Content-Type', 'X-Requested-With', 'X-CSRF-Token'), 45 | 'expose' => null, 46 | 'creds' => null, 47 | 'age' => null 48 | ); 49 | 50 | public static function prefix($name, $value) 51 | { 52 | if (is_null($name)) 53 | { 54 | return self::$_prefix; 55 | } 56 | 57 | if (is_null($value)) 58 | { 59 | return Arr::get(self::$_prefix, $name, NULL); 60 | } 61 | 62 | self::$_prefix[$name] = $value; 63 | } 64 | 65 | /** 66 | * Singleton pattern 67 | * 68 | * @return Rest 69 | */ 70 | public static function instance(REST_Controller $controller, $config = array()) 71 | { 72 | if (!isset(Rest::$_instance)) 73 | { 74 | // Create a new session instance 75 | Rest::$_instance = new Rest($controller, $config); 76 | } 77 | 78 | return Rest::$_instance; 79 | } 80 | 81 | /** 82 | * Adds prefixes to common names to return the full class name 83 | * 84 | * @param string $type The class type 85 | * @param string $name The Common name 86 | * @return string 87 | */ 88 | public static function class_name($type, $name) 89 | { 90 | $prefix = isset(Rest::$_prefix[$type]) ? Rest::$_prefix[$type] : ''; 91 | return strtolower($prefix . $name); 92 | } 93 | 94 | /** 95 | * Removes prefixes of class names to return the common name 96 | * 97 | * @param string $type The class type 98 | * @param object|string $name An instance of a class or the class name 99 | * @return string 100 | */ 101 | public static function common_name($type, $name) 102 | { 103 | $name = is_object($name) ? get_class($name) : $name; 104 | $prefix = isset(Rest::$_prefix[$type]) ? Rest::$_prefix[$type] : ''; 105 | return substr($name, strlen($prefix)); 106 | } 107 | 108 | public static function join($values, $glue = ', ') 109 | { 110 | return is_array($values) ? implode($glue, $values) : $values; 111 | } 112 | 113 | /** 114 | * @var Array configuration options 115 | */ 116 | protected $_config; 117 | 118 | /** 119 | * @var REST_Controller model 120 | */ 121 | protected $_controller; 122 | 123 | /** 124 | * @var REST_Model model 125 | */ 126 | protected $_model; 127 | 128 | /** 129 | * @var Request request instance 130 | */ 131 | protected $_request; 132 | 133 | /** 134 | * @var Kohana_Response response instance 135 | */ 136 | protected $_response; 137 | 138 | /** 139 | * @var String method HTTP method 140 | */ 141 | protected $_method; 142 | 143 | /** 144 | * @var String content HTTP Accept type 145 | */ 146 | protected $_content; 147 | 148 | /** 149 | * @var String charset HTTP Accept charset 150 | */ 151 | protected $_charset; 152 | 153 | /** 154 | * @var String language HTTP Accept language 155 | */ 156 | protected $_language; 157 | 158 | /** 159 | * @var Mixed result from the model's method 160 | */ 161 | protected $_result; 162 | 163 | /** 164 | * Loads Session and configuration options. 165 | * 166 | * @param REST_Controller $controller 167 | * @param mixed $config 168 | */ 169 | public function __construct(REST_Controller $controller, $config = array()) 170 | { 171 | $default = Kohana::$config->load('rest')->as_array(); 172 | $config = Arr::merge($default, $config); 173 | 174 | $this->request($request = Arr::get($config, 'request', Request::initial())); 175 | $this->response(Arr::get($config, 'response', $request->response())); 176 | $this->method(Arr::get($config, 'method', $request->method())); 177 | $this->model(Arr::get($config, 'model', $request->action())); 178 | unset($config['request'], $config['response'], $config['method'], $config['model']); 179 | 180 | $this->controller($controller); 181 | 182 | $this->content(Arr::get($config, 'types', $this->accept())); 183 | $this->charset(Arr::get($config, 'charsets', array(Kohana::$charset))); 184 | $this->language(Arr::get($config, 'languages', array(I18n::$lang))); 185 | 186 | // Save the config in the object 187 | $this->_config = $config; 188 | } 189 | 190 | /** 191 | * Execute the REST model and save the results 192 | * 193 | * @param void 194 | * @return mixed 195 | */ 196 | public function execute() 197 | { 198 | // Delay verifying method until execute in the event of an override 199 | $method = Rest::class_name('method', $this->_method); 200 | if (!$this->_model instanceof $method) 201 | { 202 | // Send the "Method Not Allowed" response 203 | $this->_response->headers('Allow', $this->allowed()); 204 | $this->send_code(405, array('Method :method not allowed.', array(':method' => $this->_method))); 205 | } 206 | 207 | // Check if this is a Cross-Origin Resource Sharing Model 208 | if ($this->_model instanceof REST_CORS) 209 | { 210 | $this->_model->rest_cors($this); 211 | } 212 | 213 | // Check if this is an Authorized Model 214 | if ($this->_model instanceof REST_AUTH) 215 | { 216 | if (FALSE === $this->_model->rest_auth($this)) 217 | { 218 | // Unauthorized 219 | $this->send_code(401); 220 | } 221 | } 222 | 223 | // Execute the model's method, save the result 224 | $exec = Rest::class_name('exec', $this->_method); 225 | $this->_result = $this->_model->$exec($this); 226 | 227 | // Set the action of the controller to the content type 228 | $this->request()->action(Rest::$_types[$this->_content]); 229 | 230 | // Set the Content headers 231 | $type = $this->_content . '; charset=' . $this->_charset; 232 | $this->response()->headers('Content-Type', $type); 233 | $this->response()->headers('Content-Language', $this->_language); 234 | 235 | return $this; 236 | } 237 | 238 | /** 239 | * Returns the result of the model's method 240 | * 241 | * @return mixed 242 | */ 243 | public function result() 244 | { 245 | return $this->_result; 246 | } 247 | 248 | /** 249 | * Returns JSON encoded string of the result of the model's method 250 | * 251 | * @return String 252 | */ 253 | public function result_json() 254 | { 255 | return json_encode($this->_result); 256 | } 257 | 258 | /** 259 | * Generates SimpleXML object from the result of the model's method 260 | * 261 | * @param string $name Optional name of the root node, defaults to model's name 262 | * @return SimpleXMLElement 263 | */ 264 | public function result_xml($name = NULL) 265 | { 266 | $values = $this->result(); 267 | if(is_null($name)) 268 | { 269 | $model = $this->model(); 270 | $name = strtolower(Rest::common_name('model', $model)); 271 | } 272 | 273 | $walk = function(Array $vars, $xml, $node) use (&$walk) 274 | { 275 | foreach ($vars as $name => $value) 276 | { 277 | if (is_array($value)) 278 | { 279 | $name = is_int($name) ? $node : $name; 280 | $sub = $xml->addChild($name); 281 | $walk($value, $sub, Inflector::singular($name)); 282 | } 283 | else 284 | { 285 | $xml->addChild($name, htmlentities($value, ENT_QUOTES)); 286 | } 287 | } 288 | }; 289 | 290 | // Check for associative array 291 | if (array_keys($values) !== range(0, count($values) - 1)) 292 | { 293 | $xml = new SimpleXMLElement('<' . Inflector::singular($name) . '/>'); 294 | } 295 | else 296 | { 297 | $xml = new SimpleXMLElement('<' . $name . '/>'); 298 | } 299 | 300 | $walk($values, $xml, Inflector::singular($name)); 301 | return $xml; 302 | } 303 | 304 | /** 305 | * Generates MS Excel Formated CSV string 306 | * 307 | * @param string $filename Optional filename of the CSV, defaults to model's name 308 | * @return string 309 | */ 310 | public function result_csv($filename = NULL) 311 | { 312 | $model = $this->model(); 313 | if (is_null($filename)) 314 | { 315 | $filename = strtolower(Rest::common_name('model', $model)); 316 | } 317 | $this->response()->headers('Content-disposition', 'filename='.$filename.'.csv'); 318 | 319 | $csv = ''; 320 | $values = $this->result(); 321 | if (empty($values)) return $csv; 322 | 323 | $titles = function(Array $vars, $node) use (&$titles) 324 | { 325 | $result = array(); 326 | foreach ($vars as $name => $value) 327 | { 328 | if (is_array($value)) 329 | { 330 | $name = is_int($name) ? $node.'_'.$name : $name; 331 | $result[] = $titles($value, $name); 332 | } 333 | else 334 | { 335 | $result[] = empty($node) ? $name : $node.'.'.$name; 336 | } 337 | } 338 | return implode('","', $result); 339 | }; 340 | 341 | $walk = function(Array $vars) use (&$walk) 342 | { 343 | $result = array(); 344 | foreach ($vars as $name => $value) 345 | { 346 | if (is_array($value)) 347 | { 348 | $result[] = $walk($value); 349 | } 350 | else 351 | { 352 | $result[] = str_replace('"', '""', $value); 353 | } 354 | } 355 | return implode('","', $result); 356 | }; 357 | 358 | // Check for associative array 359 | if (array_keys($values) !== range(0, count($values) - 1)) 360 | { 361 | $csv = '"'.$titles($values, '')."\"\n"; 362 | $csv .= '"'.$walk($values)."\"\n"; 363 | } 364 | else 365 | { 366 | $csv = '"'.$titles($values[0], '')."\"\n"; 367 | foreach ($values as $row) 368 | { 369 | $csv .= '"'.$walk($row)."\"\n"; 370 | } 371 | } 372 | return $csv; 373 | } 374 | 375 | /** 376 | * Checks ETag, sends 304 on match, generates ETag header 377 | * 378 | * @param string $hash The hash used to generate the ETag, defaults to sha1 379 | * @return REST_Core 380 | */ 381 | public function etag($hash = 'sha1') 382 | { 383 | $match = $this->request()->headers('If-None-Match'); 384 | $etag = $hash($this->result_json()); 385 | if ($match === $etag) 386 | { 387 | $this->send_code(304); 388 | } 389 | else 390 | { 391 | $this->response()->headers('ETag', $etag); 392 | } 393 | 394 | return $this; 395 | } 396 | 397 | /** 398 | * Returns the accepted content types based on Controller's interfaces 399 | * 400 | * @return mixed 401 | */ 402 | public function accept() 403 | { 404 | $accept = array(); 405 | foreach (Rest::$_types as $type => $value) 406 | { 407 | $content = Rest::class_name('content', $value); 408 | if ($this->_controller instanceof $content) 409 | { 410 | $accept[] = $type; 411 | } 412 | } 413 | return $accept; 414 | } 415 | 416 | /** 417 | * Return the allowed methods of the model 418 | * 419 | * @param void 420 | * @return mixed 421 | */ 422 | public function allowed() 423 | { 424 | $allowed = array(); 425 | foreach (Rest::$_methods as $method) 426 | { 427 | $class = Rest::class_name('method', $method); 428 | if ($this->_model instanceof $class) 429 | { 430 | $allowed[] = $method; 431 | } 432 | } 433 | return $allowed; 434 | } 435 | 436 | /** 437 | * Get the short-name content type of the request 438 | * @return string 439 | */ 440 | public function type() 441 | { 442 | $type = $this->request()->headers('Content-Type'); 443 | return Arr::get(Rest::$_types, $type, $type); 444 | } 445 | 446 | /** 447 | * Retrieves a value from the route parameters. 448 | * 449 | * $id = $request->param('id'); 450 | * 451 | * @param string $key Key of the value 452 | * @param mixed $default Default value if the key is not set 453 | * @return mixed 454 | */ 455 | public function param($key = NULL, $default = NULL) 456 | { 457 | return $this->request()->param($key, $default); 458 | } 459 | 460 | /** 461 | * Gets HTTP POST parameters to the request. 462 | * 463 | * @param mixed $key Key or key value pairs to set 464 | * @return mixed 465 | */ 466 | public function post($key = NULL) 467 | { 468 | return $this->request()->post($key); 469 | } 470 | 471 | /** 472 | * Gets HTTP query string. 473 | * 474 | * @param mixed $key Key or key value pairs to set 475 | * @return mixed 476 | */ 477 | public function query($key = NULL, $array = TRUE) 478 | { 479 | if (is_null($key)) 480 | { 481 | $query = $this->request()->query($key); 482 | foreach ($query as $name => $value) 483 | { 484 | if ($value == '') 485 | { 486 | return json_decode($name, $array); 487 | } 488 | } 489 | return $query; 490 | } 491 | return $this->request()->query($key); 492 | } 493 | 494 | /** 495 | * Gets HTTP body to the request or response. The body is 496 | * included after the header, separated by a single empty new line. 497 | * 498 | * @param string $content Content to set to the object 499 | * @param boolean $array Return an associative array, json only 500 | * @return mixed 501 | */ 502 | public function body($type = NULL, $array = FALSE) 503 | { 504 | $body = $this->request()->body(); 505 | $type = ($type) ? $type : $this->type(); 506 | switch ($type) 507 | { 508 | case 'json': 509 | return json_decode($body, $array); 510 | break; 511 | case 'xml': 512 | return new SimpleXMLElement($body); 513 | break; 514 | default: 515 | return $body; 516 | break; 517 | } 518 | } 519 | 520 | /** 521 | * Set or get the request 522 | * 523 | * @param Request $request Request 524 | * @return Request 525 | * @return void 526 | */ 527 | public function request(Request $request = NULL) 528 | { 529 | if ($request === NULL) 530 | { 531 | // Act as a getter 532 | return $this->_request; 533 | } 534 | 535 | // Act as a setter 536 | $this->_request = $request; 537 | 538 | return $this; 539 | } 540 | 541 | /** 542 | * Set or get the response 543 | * 544 | * @param Response $response Response 545 | * @return Response 546 | * @return void 547 | */ 548 | public function response(Response $response = NULL) 549 | { 550 | if ($response === NULL) 551 | { 552 | // Act as a getter 553 | return $this->_response; 554 | } 555 | 556 | // Act as a setter 557 | $this->_response = $response; 558 | 559 | return $this; 560 | } 561 | 562 | /** 563 | * Set or get the method 564 | * 565 | * @param String $method Method 566 | * @return String 567 | * @return void 568 | */ 569 | public function method($method = NULL) 570 | { 571 | if ($method === NULL) 572 | { 573 | // Act as a getter 574 | return $this->_method; 575 | } 576 | 577 | // Act as a setter 578 | $this->_method = $method; 579 | 580 | return $this; 581 | } 582 | 583 | /** 584 | * Set or get the model 585 | * 586 | * @param mixed $model Model 587 | * @return REST_Model 588 | * @return void 589 | */ 590 | public function model($model = NULL) 591 | { 592 | if ($model === NULL) 593 | { 594 | // Act as a getter 595 | return $this->_model; 596 | } 597 | 598 | // Act as a setter 599 | if (is_object($model)) 600 | { 601 | $this->_model = $model; 602 | } 603 | else 604 | { 605 | $class = Rest::class_name('model', $model); 606 | if (FALSE === class_exists($class)) 607 | { 608 | // Send the "Model Not Found" response 609 | $this->send_code(404, array('Resource ":model" not found.', array(':model' => $model))); 610 | } 611 | $this->_model = new $class; 612 | } 613 | 614 | if (!$this->_model instanceof REST_Model) 615 | { 616 | // Send the Internal Server Error response 617 | $this->send_code(500, array('Class :class does not implement REST_Model.', array(':class' => get_class($this->_model)))); 618 | } 619 | 620 | return $this; 621 | } 622 | 623 | /** 624 | * Set or get the controller 625 | * 626 | * @param REST_Controller $controller Controller 627 | * @return REST_Controller 628 | * @return void 629 | */ 630 | public function controller(REST_Controller $controller = NULL) 631 | { 632 | if ($controller === NULL) 633 | { 634 | // Act as a getter 635 | return $this->_controller; 636 | } 637 | 638 | // Act as a setter 639 | $this->_controller = $controller; 640 | 641 | return $this; 642 | } 643 | 644 | /** 645 | * Set or get the content type 646 | * 647 | * @param Array $content Content 648 | * @return String 649 | * @return void 650 | */ 651 | public function content(Array $types = NULL) 652 | { 653 | if ($types === NULL) 654 | { 655 | // Act as a getter 656 | return $this->_content; 657 | } 658 | 659 | $request = $this->request(); 660 | 661 | // Act as a setter 662 | $this->_content = $request->headers()->preferred_accept($types); 663 | 664 | if (FALSE === $this->_content) 665 | { 666 | $this->send_code(406, array('Supplied Accept types: :accept not supported. Supported types: :types', 667 | array( 668 | ':accept' => $request->headers('Accept'), 669 | ':types' => implode(', ', $types) 670 | ))); 671 | } 672 | 673 | return $this; 674 | } 675 | 676 | /** 677 | * Set or get the charset 678 | * 679 | * @param array $charsets 680 | * @return REST_Core 681 | */ 682 | public function charset(Array $charsets = NULL) 683 | { 684 | if ($charsets === NULL) 685 | { 686 | // Act as a getter 687 | return $this->_charset; 688 | } 689 | 690 | $request = $this->request(); 691 | 692 | // Act as a setter 693 | $this->_charset = $request->headers()->preferred_charset($charsets); 694 | 695 | if (FALSE === $this->_charset) 696 | { 697 | $this->send_code(406, array('Supplied Accept-Charset: :accept not supported. Supported types: :types', 698 | array( 699 | ':accept' => $request->headers('Accept-Charset'), 700 | ':types' => implode(', ', $charsets) 701 | ))); 702 | } 703 | 704 | return $this; 705 | } 706 | 707 | /** 708 | * Set or get the language 709 | * 710 | * @param array $charsets 711 | * @return REST_Core 712 | */ 713 | public function language(Array $languages = NULL) 714 | { 715 | if ($languages === NULL) 716 | { 717 | // Act as a getter 718 | return $this->_language; 719 | } 720 | 721 | $request = $this->request(); 722 | 723 | // Act as a setter 724 | $this->_language = $request->headers()->preferred_language($languages); 725 | 726 | if (FALSE === $this->_language) 727 | { 728 | $this->send_code(406, array('Supplied Accept-Language: :accept not supported. Supported languages: :types', 729 | array( 730 | ':accept' => $request->headers('Accept-Language'), 731 | ':types' => implode(', ', $languages) 732 | ))); 733 | } 734 | 735 | return $this; 736 | } 737 | 738 | /** 739 | * Allows setting the method from the X-HTTP-METHOD-OVERRIDE header 740 | * 741 | * @param boolean $override 742 | * @return Rest 743 | */ 744 | public function method_override($override = FALSE) 745 | { 746 | $request = $this->request(); 747 | $method = $request->headers('X-HTTP-METHOD-OVERRIDE'); 748 | $method = (isset($method) AND $override) ? $method : $request->method(); 749 | $this->method($method); 750 | return $this; 751 | } 752 | 753 | /** 754 | * Allows setting the content type from the Request param content_type 755 | * 756 | * @param boolean $override 757 | * @return Rest 758 | */ 759 | public function content_override($override = FALSE) 760 | { 761 | $types = $this->accept(); 762 | 763 | if (FALSE === $override) 764 | { 765 | $this->content(Arr::get($this->_config, 'types', $types)); 766 | return $this; 767 | } 768 | 769 | $content = $this->request()->param('content_type', FALSE); 770 | if (FALSE === $content) 771 | { 772 | // No content_type param used... 773 | return $this; 774 | } 775 | 776 | $key = array_search($content, Rest::$_types); 777 | if (FALSE === $key) 778 | { 779 | $this->send_code(406, array('Supplied Override Type: :accept not supported. Supported types: :types', 780 | array( 781 | ':accept' => $content, 782 | ':types' => implode(', ', $types) 783 | ))); 784 | } 785 | 786 | if (!in_array($key, $types)) 787 | { 788 | $this->send_code(406, array('Supplied Content Type: :accept not supported. Supported types: :types', 789 | array( 790 | ':accept' => $key, 791 | ':types' => implode(', ', $types) 792 | ))); 793 | } 794 | 795 | $this->_content = $key; 796 | return $this; 797 | } 798 | 799 | /** 800 | * Cross-Origin Resource Sharing Helper 801 | * 802 | * @param array $values 803 | * @return Rest 804 | */ 805 | public function cors(Array $values = array()) 806 | { 807 | $cors = self::$_cors; 808 | $cors['methods'] = $this->allowed(); 809 | $cors = Arr::merge($cors, $values); 810 | 811 | $response = $this->response(); 812 | 813 | if (isset($cors['origin'])) 814 | { 815 | $response->headers('Access-Control-Allow-Origin', self::join($cors['origin'])); 816 | } 817 | 818 | if (isset($cors['methods'])) 819 | { 820 | $response->headers('Access-Control-Allow-Methods', self::join($cors['methods'])); 821 | } 822 | 823 | if (isset($cors['headers'])) 824 | { 825 | $response->headers('Access-Control-Allow-Headers', self::join($cors['headers'])); 826 | } 827 | 828 | if (isset($cors['expose'])) 829 | { 830 | $response->headers('Access-Control-Expose-Headers', self::join($cors['expose'])); 831 | } 832 | 833 | if (isset($cors['creds'])) 834 | { 835 | $response->headers('Access-Control-Allow-Credentials', $cors['creds']); 836 | } 837 | 838 | if (isset($cors['age'])) 839 | { 840 | $response->headers('Access-Control-Max-Age', $cors['age']); 841 | } 842 | 843 | return $this; 844 | } 845 | 846 | /** 847 | * Sends the created response (POST) 848 | * 849 | * @param type $id 850 | * @param type $code 851 | */ 852 | public function send_created($id, $code = 201) 853 | { 854 | $request = $this->request(); 855 | $url = array( 856 | $request->directory(), 857 | $request->controller(), 858 | strtolower(Rest::common_name('model', $this->_model)), 859 | $id 860 | ); 861 | $url = URL::site(implode('/', $url), TRUE, Kohana::$index_file); 862 | $this->request()->redirect($url, $code); 863 | } 864 | 865 | /** 866 | * Sends the response code and exits the application 867 | * 868 | * @param type $code 869 | * @param mixed $body 870 | */ 871 | public function send_code($code = 204, $body = NULL) 872 | { 873 | // Echo response and exit if we aren't using exceptions 874 | if (FALSE === Arr::get($this->_config, 'exceptions', FALSE)) 875 | { 876 | if (is_array($body)) 877 | { 878 | list($str, $pairs) = $body; 879 | $body = strtr($str, $pairs); 880 | } 881 | echo $this->response() 882 | ->status($code) 883 | ->send_headers() 884 | ->body($body); 885 | 886 | // Stop execution 887 | exit; 888 | } 889 | else 890 | { 891 | // See if special exception class exists 892 | $class = 'Http_Exception_'.$code; 893 | if (class_exists($class)) 894 | { 895 | if (is_array($body)) 896 | { 897 | list($str, $pairs) = $body; 898 | throw new $class($str, $pairs); 899 | } 900 | else 901 | { 902 | throw new $class($body); 903 | } 904 | } 905 | else 906 | { 907 | if (is_array($body)) 908 | { 909 | list($str, $pairs) = $body; 910 | throw new HTTP_Exception($str, $pairs, $code); 911 | } 912 | else 913 | { 914 | throw new HTTP_Exception($body, NULL, $code); 915 | } 916 | } 917 | } 918 | } 919 | } 920 | // End Rest --------------------------------------------------------------------------------