1 | <?php |
---|
2 | /* |
---|
3 | * This file is part of the Tonic. |
---|
4 | * (c) Paul James <paul@peej.co.uk> |
---|
5 | * |
---|
6 | * For the full copyright and license information, please view the LICENSE |
---|
7 | * file that was distributed with this source code. |
---|
8 | */ |
---|
9 | |
---|
10 | /* Turn on namespace in PHP5.3 if you have namespace collisions from the Tonic classnames |
---|
11 | //*/ |
---|
12 | namespace Tonic; |
---|
13 | use \ReflectionClass as ReflectionClass; |
---|
14 | use \ReflectionMethod as ReflectionMethod; |
---|
15 | use \Exception as Exception; |
---|
16 | |
---|
17 | /** |
---|
18 | * Model the data of the incoming HTTP request |
---|
19 | * @namespace Tonic\Lib |
---|
20 | */ |
---|
21 | class Request { |
---|
22 | |
---|
23 | /** |
---|
24 | * The requested URI |
---|
25 | * @var str |
---|
26 | */ |
---|
27 | public $uri; |
---|
28 | |
---|
29 | /** |
---|
30 | * The URI where the front controller is positioned in the server URI-space |
---|
31 | * @var str |
---|
32 | */ |
---|
33 | public $baseUri = ''; |
---|
34 | |
---|
35 | /** |
---|
36 | * Array of possible URIs based upon accept and accept-language request headers in order of preference |
---|
37 | * @var str[] |
---|
38 | */ |
---|
39 | public $negotiatedUris = array(); |
---|
40 | |
---|
41 | /** |
---|
42 | * Array of possible URIs based upon accept request headers in order of preference |
---|
43 | * @var str[] |
---|
44 | */ |
---|
45 | public $formatNegotiatedUris = array(); |
---|
46 | |
---|
47 | /** |
---|
48 | * Array of possible URIs based upon accept-language request headers in order of preference |
---|
49 | * @var str[] |
---|
50 | */ |
---|
51 | public $languageNegotiatedUris = array(); |
---|
52 | |
---|
53 | /** |
---|
54 | * Array of accept headers in order of preference |
---|
55 | * @var str[][] |
---|
56 | */ |
---|
57 | public $accept = array(); |
---|
58 | |
---|
59 | /** |
---|
60 | * Array of accept-language headers in order of preference |
---|
61 | * @var str[][] |
---|
62 | */ |
---|
63 | public $acceptLang = array(); |
---|
64 | |
---|
65 | /** |
---|
66 | * Array of accept-encoding headers in order of preference |
---|
67 | * @var str[] |
---|
68 | */ |
---|
69 | public $acceptEncoding = array(); |
---|
70 | |
---|
71 | /** |
---|
72 | * Map of file/URI extensions to mimetypes |
---|
73 | * @var str[] |
---|
74 | */ |
---|
75 | public $mimetypes = array( |
---|
76 | 'html' => 'text/html', |
---|
77 | 'txt' => 'text/plain', |
---|
78 | 'php' => 'application/php', |
---|
79 | 'css' => 'text/css', |
---|
80 | 'js' => 'application/javascript', |
---|
81 | 'json' => 'application/json', |
---|
82 | 'xml' => 'application/xml', |
---|
83 | 'rss' => 'application/rss+xml', |
---|
84 | 'atom' => 'application/atom+xml', |
---|
85 | 'gz' => 'application/x-gzip', |
---|
86 | 'tar' => 'application/x-tar', |
---|
87 | 'zip' => 'application/zip', |
---|
88 | 'gif' => 'image/gif', |
---|
89 | 'png' => 'image/png', |
---|
90 | 'jpg' => 'image/jpeg', |
---|
91 | 'ico' => 'image/x-icon', |
---|
92 | 'swf' => 'application/x-shockwave-flash', |
---|
93 | 'flv' => 'video/x-flv', |
---|
94 | 'avi' => 'video/mpeg', |
---|
95 | 'mpeg' => 'video/mpeg', |
---|
96 | 'mpg' => 'video/mpeg', |
---|
97 | 'mov' => 'video/quicktime', |
---|
98 | 'mp3' => 'audio/mpeg' |
---|
99 | ); |
---|
100 | |
---|
101 | /** |
---|
102 | * Supported HTTP methods |
---|
103 | * @var str[] |
---|
104 | */ |
---|
105 | public $HTTPMethods = array( |
---|
106 | 'GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS' |
---|
107 | ); |
---|
108 | |
---|
109 | /** |
---|
110 | * Allowed resource methods |
---|
111 | * @var str[] |
---|
112 | */ |
---|
113 | public $allowedMethods = array(); |
---|
114 | |
---|
115 | /** |
---|
116 | * HTTP request method of incoming request |
---|
117 | * @var str |
---|
118 | */ |
---|
119 | public $method = 'GET'; |
---|
120 | |
---|
121 | /** |
---|
122 | * Body data of incoming request |
---|
123 | * @var str |
---|
124 | */ |
---|
125 | public $data; |
---|
126 | |
---|
127 | /** |
---|
128 | * Array of if-match etags |
---|
129 | * @var str[] |
---|
130 | */ |
---|
131 | public $ifMatch = array(); |
---|
132 | |
---|
133 | /** |
---|
134 | * Array of if-none-match etags |
---|
135 | * @var str[] |
---|
136 | */ |
---|
137 | public $ifNoneMatch = array(); |
---|
138 | |
---|
139 | /** |
---|
140 | * The resource classes loaded and how they are wired to URIs |
---|
141 | * @var str[] |
---|
142 | */ |
---|
143 | public $resources = array(); |
---|
144 | |
---|
145 | /** |
---|
146 | * A list of URL to namespace/package mappings for routing requests to a |
---|
147 | * group of resources that are wired into a different URL-space |
---|
148 | * @var str[] |
---|
149 | */ |
---|
150 | public $mounts = array(); |
---|
151 | |
---|
152 | /** |
---|
153 | * Set a default configuration option |
---|
154 | */ |
---|
155 | protected function getConfig($config, $configVar, $serverVar = NULL, $default = NULL) { |
---|
156 | if (isset($config[$configVar])) { |
---|
157 | return $config[$configVar]; |
---|
158 | } elseif (isset($_SERVER[$serverVar]) && $_SERVER[$serverVar] != '') { |
---|
159 | return $_SERVER[$serverVar]; |
---|
160 | } else { |
---|
161 | return $default; |
---|
162 | } |
---|
163 | } |
---|
164 | |
---|
165 | /** |
---|
166 | * Create a request object using the given configuration options. |
---|
167 | * |
---|
168 | * The configuration options array can contain the following: |
---|
169 | * |
---|
170 | * <dl> |
---|
171 | * <dt>uri</dt> <dd>The URI of the request</dd> |
---|
172 | * <dt>method</dt> <dd>The HTTP method of the request</dd> |
---|
173 | * <dt>data</dt> <dd>The body data of the request</dd> |
---|
174 | * <dt>accept</dt> <dd>An accept header</dd> |
---|
175 | * <dt>acceptLang</dt> <dd>An accept-language header</dd> |
---|
176 | * <dt>acceptEncoding</dt> <dd>An accept-encoding header</dd> |
---|
177 | * <dt>ifMatch</dt> <dd>An if-match header</dd> |
---|
178 | * <dt>ifNoneMatch</dt> <dd>An if-none-match header</dd> |
---|
179 | * <dt>mimetypes</dt> <dd>A map of file/URI extenstions to mimetypes, these |
---|
180 | * will be added to the default map of mimetypes</dd> |
---|
181 | * <dt>baseUri</dt> <dd>The base relative URI to use when dispatcher isn't |
---|
182 | * at the root of the domain. Do not put a trailing slash</dd> |
---|
183 | * <dt>mounts</dt> <dd>an array of namespace to baseUri prefix mappings</dd> |
---|
184 | * <dt>HTTPMethods</dt> <dd>an array of HTTP methods to support</dd> |
---|
185 | * </dl> |
---|
186 | * |
---|
187 | * @param mixed[] config Configuration options |
---|
188 | */ |
---|
189 | function __construct($config = array()) { |
---|
190 | |
---|
191 | // set defaults |
---|
192 | $config['uri'] = parse_url($this->getConfig($config, 'uri', 'REQUEST_URI'), PHP_URL_PATH); |
---|
193 | $config['baseUri'] = $this->getConfig($config, 'baseUri', ''); |
---|
194 | $config['accept'] = $this->getConfig($config, 'accept', 'HTTP_ACCEPT'); |
---|
195 | $config['acceptLang'] = $this->getConfig($config, 'acceptLang', 'HTTP_ACCEPT_LANGUAGE'); |
---|
196 | $config['acceptEncoding'] = $this->getConfig($config, 'acceptEncoding', 'HTTP_ACCEPT_ENCODING'); |
---|
197 | $config['ifMatch'] = $this->getConfig($config, 'ifMatch', 'HTTP_IF_MATCH'); |
---|
198 | $config['ifNoneMatch'] = $this->getConfig($config, 'ifNoneMatch', 'HTTP_IF_NONE_MATCH'); |
---|
199 | |
---|
200 | if (isset($config['mimetypes']) && is_array($config['mimetypes'])) { |
---|
201 | foreach ($config['mimetypes'] as $ext => $mimetype) { |
---|
202 | $this->mimetypes[$ext] = $mimetype; |
---|
203 | } |
---|
204 | } |
---|
205 | |
---|
206 | // set baseUri |
---|
207 | $this->baseUri = $config['baseUri']; |
---|
208 | |
---|
209 | // get request URI |
---|
210 | $parts = explode('/', $config['uri']); |
---|
211 | $lastPart = array_pop($parts); |
---|
212 | $this->uri = join('/', $parts); |
---|
213 | |
---|
214 | $parts = explode('.', $lastPart); |
---|
215 | $this->uri .= '/'.$parts[0]; |
---|
216 | |
---|
217 | if ($this->uri != '/' && substr($this->uri, -1, 1) == '/') { // remove trailing slash problem |
---|
218 | $this->uri = substr($this->uri, 0, -1); |
---|
219 | } |
---|
220 | |
---|
221 | array_shift($parts); |
---|
222 | foreach ($parts as $part) { |
---|
223 | $this->accept[10][] = $part; |
---|
224 | $this->acceptLang[10][] = $part; |
---|
225 | } |
---|
226 | |
---|
227 | // sort accept headers |
---|
228 | $accept = explode(',', strtolower($config['accept'])); |
---|
229 | foreach ($accept as $mimetype) { |
---|
230 | $parts = explode(';q=', $mimetype); |
---|
231 | if (isset($parts) && isset($parts[1]) && $parts[1]) { |
---|
232 | $num = $parts[1] * 10; |
---|
233 | } else { |
---|
234 | $num = 10; |
---|
235 | } |
---|
236 | $key = array_search($parts[0], $this->mimetypes); |
---|
237 | if ($key) { |
---|
238 | $this->accept[$num][] = $key; |
---|
239 | } |
---|
240 | } |
---|
241 | krsort($this->accept); |
---|
242 | |
---|
243 | // sort lang accept headers |
---|
244 | $accept = explode(',', strtolower($config['acceptLang'])); |
---|
245 | foreach ($accept as $mimetype) { |
---|
246 | $parts = explode(';q=', $mimetype); |
---|
247 | if (isset($parts) && isset($parts[1]) && $parts[1]) { |
---|
248 | $num = $parts[1] * 10; |
---|
249 | } else { |
---|
250 | $num = 10; |
---|
251 | } |
---|
252 | $this->acceptLang[$num][] = $parts[0]; |
---|
253 | } |
---|
254 | krsort($this->acceptLang); |
---|
255 | |
---|
256 | // get encoding accept headers |
---|
257 | if ($config['acceptEncoding']) { |
---|
258 | foreach (explode(',', $config['acceptEncoding']) as $key => $accept) { |
---|
259 | $this->acceptEncoding[$key] = trim($accept); |
---|
260 | } |
---|
261 | } |
---|
262 | |
---|
263 | // create negotiated URI lists from accept headers and request URI |
---|
264 | foreach ($this->accept as $typeOrder) { |
---|
265 | foreach ($typeOrder as $type) { |
---|
266 | if ($type) { |
---|
267 | foreach ($this->acceptLang as $langOrder) { |
---|
268 | foreach ($langOrder as $lang) { |
---|
269 | if ($lang && $lang != $type) { |
---|
270 | $this->negotiatedUris[] = $this->uri.'.'.$type.'.'.$lang; |
---|
271 | } |
---|
272 | } |
---|
273 | } |
---|
274 | $this->negotiatedUris[] = $this->uri.'.'.$type; |
---|
275 | $this->formatNegotiatedUris[] = $this->uri.'.'.$type; |
---|
276 | } |
---|
277 | } |
---|
278 | } |
---|
279 | foreach ($this->acceptLang as $langOrder) { |
---|
280 | foreach ($langOrder as $lang) { |
---|
281 | if ($lang) { |
---|
282 | $this->negotiatedUris[] = $this->uri.'.'.$lang; |
---|
283 | $this->languageNegotiatedUris[] = $this->uri.'.'.$lang; |
---|
284 | } |
---|
285 | } |
---|
286 | } |
---|
287 | $this->negotiatedUris[] = $this->uri; |
---|
288 | $this->formatNegotiatedUris[] = $this->uri; |
---|
289 | $this->languageNegotiatedUris[] = $this->uri; |
---|
290 | |
---|
291 | $this->negotiatedUris = array_values(array_unique($this->negotiatedUris)); |
---|
292 | $this->formatNegotiatedUris = array_values(array_unique($this->formatNegotiatedUris)); |
---|
293 | $this->languageNegotiatedUris = array_values(array_unique($this->languageNegotiatedUris)); |
---|
294 | |
---|
295 | $this->HTTPMethods = $this->getConfig($config, 'HTTPMethods', NULL, $this->HTTPMethods); |
---|
296 | |
---|
297 | // get HTTP method |
---|
298 | $this->method = strtoupper($this->getConfig($config, 'method', 'REQUEST_METHOD', $this->method)); |
---|
299 | |
---|
300 | // get HTTP request data |
---|
301 | $this->data = $this->getConfig($config, 'data', NULL, file_get_contents("php://input")); |
---|
302 | |
---|
303 | // conditional requests |
---|
304 | if ($config['ifMatch']) { |
---|
305 | $ifMatch = explode(',', $config['ifMatch']); |
---|
306 | foreach ($ifMatch as $etag) { |
---|
307 | $this->ifMatch[] = trim($etag, '" '); |
---|
308 | } |
---|
309 | } |
---|
310 | if ($config['ifNoneMatch']) { |
---|
311 | $ifNoneMatch = explode(',', $config['ifNoneMatch']); |
---|
312 | foreach ($ifNoneMatch as $etag) { |
---|
313 | $this->ifNoneMatch[] = trim($etag, '" '); |
---|
314 | } |
---|
315 | } |
---|
316 | |
---|
317 | // mounts |
---|
318 | if (isset($config['mount']) && is_array($config['mount'])) { |
---|
319 | $this->mounts = $config['mount']; |
---|
320 | } |
---|
321 | |
---|
322 | // prime named resources for autoloading |
---|
323 | if (isset($config['autoload']) && is_array($config['autoload'])) { |
---|
324 | foreach ($config['autoload'] as $uri => $className) { |
---|
325 | $this->resources[$uri] = array( |
---|
326 | 'class' => $className, |
---|
327 | 'loaded' => FALSE |
---|
328 | ); |
---|
329 | } |
---|
330 | } |
---|
331 | |
---|
332 | // load definitions of already loaded resource classes |
---|
333 | $resourceClassName = class_exists('Tonic\\Resource') ? 'Tonic\\Resource' : 'Resource'; |
---|
334 | foreach (get_declared_classes() as $className) { |
---|
335 | if (is_subclass_of($className, $resourceClassName)) { |
---|
336 | |
---|
337 | $resourceDetails = $this->getResourceClassDetails($className); |
---|
338 | |
---|
339 | preg_match_all('/@uri\s+([^\s]+)(?:\s([0-9]+))?/', $resourceDetails['comment'], $annotations); |
---|
340 | if (isset($annotations[1]) && $annotations[1]) { |
---|
341 | $uris = $annotations[1]; |
---|
342 | } else { |
---|
343 | $uris = array(); |
---|
344 | } |
---|
345 | |
---|
346 | foreach ($uris as $index => $uri) { |
---|
347 | if ($uri != '/' && substr($uri, -1, 1) == '/') { // remove trailing slash problem |
---|
348 | $uri = substr($uri, 0, -1); |
---|
349 | } |
---|
350 | if (isset($annotations[2][$index]) && is_numeric($annotations[2][$index])) { |
---|
351 | $priority = intval($annotations[2][$index]); |
---|
352 | } else { |
---|
353 | $priority = 0; |
---|
354 | } |
---|
355 | if ( |
---|
356 | !isset($this->resources[$resourceDetails['mountPoint'].$uri]) || |
---|
357 | $this->resources[$resourceDetails['mountPoint'].$uri]['priority'] < $priority |
---|
358 | ) { |
---|
359 | $this->resources[$resourceDetails['mountPoint'].$uri] = array( |
---|
360 | 'namespace' => $resourceDetails['namespaceName'], |
---|
361 | 'class' => $resourceDetails['className'], |
---|
362 | 'filename' => $resourceDetails['filename'], |
---|
363 | 'line' => $resourceDetails['line'], |
---|
364 | 'priority' => $priority, |
---|
365 | 'loaded' => TRUE |
---|
366 | ); |
---|
367 | } |
---|
368 | } |
---|
369 | } |
---|
370 | } |
---|
371 | |
---|
372 | } |
---|
373 | |
---|
374 | /** |
---|
375 | * Get the details of a Resource class by reflection |
---|
376 | * @param str className |
---|
377 | * @return str[] |
---|
378 | */ |
---|
379 | protected function getResourceClassDetails($className) { |
---|
380 | |
---|
381 | $resourceReflector = new ReflectionClass($className); |
---|
382 | $comment = $resourceReflector->getDocComment(); |
---|
383 | |
---|
384 | $className = $resourceReflector->getName(); |
---|
385 | if (method_exists($resourceReflector, 'getNamespaceName')) { |
---|
386 | $namespaceName = $resourceReflector->getNamespaceName(); |
---|
387 | } else { |
---|
388 | // @codeCoverageIgnoreStart |
---|
389 | $namespaceName = FALSE; |
---|
390 | // @codeCoverageIgnoreEnd |
---|
391 | } |
---|
392 | |
---|
393 | if (!$namespaceName) { |
---|
394 | preg_match('/@(?:package|namespace)\s+([^\s]+)/', $comment, $package); |
---|
395 | if (isset($package[1])) { |
---|
396 | $namespaceName = $package[1]; |
---|
397 | } |
---|
398 | } |
---|
399 | |
---|
400 | // adjust URI for mountpoint |
---|
401 | if (isset($this->mounts[$namespaceName])) { |
---|
402 | $mountPoint = $this->mounts[$namespaceName]; |
---|
403 | } else { |
---|
404 | $mountPoint = ''; |
---|
405 | } |
---|
406 | |
---|
407 | return array( |
---|
408 | 'comment' => $comment, |
---|
409 | 'className' => $className, |
---|
410 | 'namespaceName' => $namespaceName, |
---|
411 | 'filename' => $resourceReflector->getFileName(), |
---|
412 | 'line' => $resourceReflector->getStartLine(), |
---|
413 | 'mountPoint' => $mountPoint |
---|
414 | ); |
---|
415 | |
---|
416 | } |
---|
417 | |
---|
418 | /** |
---|
419 | * Convert the object into a string suitable for printing |
---|
420 | * @return str |
---|
421 | * @codeCoverageIgnore |
---|
422 | */ |
---|
423 | function __toString() { |
---|
424 | $str = 'URI: '.$this->uri."\n"; |
---|
425 | $str .= 'Method: '.$this->method."\n"; |
---|
426 | if ($this->data) { |
---|
427 | $str .= 'Data: '.$this->data."\n"; |
---|
428 | } |
---|
429 | $str .= 'Acceptable Formats:'; |
---|
430 | foreach ($this->accept as $accept) { |
---|
431 | foreach ($accept as $a) { |
---|
432 | $str .= ' .'.$a; |
---|
433 | if (isset($this->mimetypes[$a])) $str .= ' ('.$this->mimetypes[$a].')'; |
---|
434 | } |
---|
435 | } |
---|
436 | $str .= "\n"; |
---|
437 | $str .= 'Acceptable Languages:'; |
---|
438 | foreach ($this->acceptLang as $accept) { |
---|
439 | foreach ($accept as $a) { |
---|
440 | $str .= ' '.$a; |
---|
441 | } |
---|
442 | } |
---|
443 | $str .= "\n"; |
---|
444 | $str .= 'Negotated URIs:'."\n"; |
---|
445 | foreach ($this->negotiatedUris as $uri) { |
---|
446 | $str .= "\t".$uri."\n"; |
---|
447 | } |
---|
448 | $str .= 'Format Negotated URIs:'."\n"; |
---|
449 | foreach ($this->formatNegotiatedUris as $uri) { |
---|
450 | $str .= "\t".$uri."\n"; |
---|
451 | } |
---|
452 | $str .= 'Language Negotated URIs:'."\n"; |
---|
453 | foreach ($this->languageNegotiatedUris as $uri) { |
---|
454 | $str .= "\t".$uri."\n"; |
---|
455 | } |
---|
456 | if ($this->ifMatch) { |
---|
457 | $str .= 'If Match:'; |
---|
458 | foreach ($this->ifMatch as $etag) { |
---|
459 | $str .= ' '.$etag; |
---|
460 | } |
---|
461 | $str .= "\n"; |
---|
462 | } |
---|
463 | if ($this->ifNoneMatch) { |
---|
464 | $str .= 'If None Match:'; |
---|
465 | foreach ($this->ifNoneMatch as $etag) { |
---|
466 | $str .= ' '.$etag; |
---|
467 | } |
---|
468 | $str .= "\n"; |
---|
469 | } |
---|
470 | $str .= 'Loaded Resources:'."\n"; |
---|
471 | foreach ($this->resources as $uri => $resource) { |
---|
472 | $str .= "\t".$uri."\n"; |
---|
473 | if (isset($resource['namespace']) && $resource['namespace']) $str .= "\t\tNamespace: ".$resource['namespace']."\n"; |
---|
474 | $str .= "\t\tClass: ".$resource['class']."\n"; |
---|
475 | $str .= "\t\tFile: ".$resource['filename']; |
---|
476 | if (isset($resource['line']) && $resource['line']) $str .= '#'.$resource['line']; |
---|
477 | $str .= "\n"; |
---|
478 | } |
---|
479 | return $str; |
---|
480 | } |
---|
481 | |
---|
482 | /** |
---|
483 | * Instantiate the resource class that matches the request URI the best |
---|
484 | * @return Resource |
---|
485 | * @throws ResponseException If the resource does not exist, a 404 exception is thrown |
---|
486 | */ |
---|
487 | function loadResource() { |
---|
488 | |
---|
489 | $uriMatches = array(); |
---|
490 | foreach ($this->resources as $uri => $resource) { |
---|
491 | |
---|
492 | preg_match_all('#((?<!\?):[^/]+|{[^0-9][^}]*}|\(.+?\))#', $uri, $params, PREG_PATTERN_ORDER); |
---|
493 | |
---|
494 | $uri = $this->baseUri.$uri; |
---|
495 | if ($uri != '/' && substr($uri, -1, 1) == '/') { // remove trailing slash problem |
---|
496 | $uri = substr($uri, 0, -1); |
---|
497 | } |
---|
498 | $uriRegex = preg_replace('#((?<!\?):[^(/]+|{[^0-9][^}]*})#', '(.+)', $uri); |
---|
499 | |
---|
500 | if (preg_match('#^'.$uriRegex.'$#', $this->uri, $matches)) { |
---|
501 | array_shift($matches); |
---|
502 | |
---|
503 | if (isset($params[1])) { |
---|
504 | foreach ($params[1] as $index => $param) { |
---|
505 | if (isset($matches[$index])) { |
---|
506 | if (substr($param, 0, 1) == ':') { |
---|
507 | $matches[substr($param, 1)] = $matches[$index]; |
---|
508 | unset($matches[$index]); |
---|
509 | } elseif (substr($param, 0, 1) == '{' && substr($param, -1, 1) == '}') { |
---|
510 | $matches[substr($param, 1, -1)] = $matches[$index]; |
---|
511 | unset($matches[$index]); |
---|
512 | } |
---|
513 | } |
---|
514 | } |
---|
515 | } |
---|
516 | |
---|
517 | $uriMatches[isset($resource['priority']) ? $resource['priority'] : 0] = array( |
---|
518 | $uri, |
---|
519 | $resource, |
---|
520 | $matches |
---|
521 | ); |
---|
522 | |
---|
523 | } |
---|
524 | } |
---|
525 | krsort($uriMatches); |
---|
526 | |
---|
527 | if ($uriMatches) { |
---|
528 | list($uri, $resource, $parameters) = array_shift($uriMatches); |
---|
529 | if (!$resource['loaded']) { // autoload |
---|
530 | if (!class_exists($resource['class'])) { |
---|
531 | throw new Exception('Unable to load resource'); |
---|
532 | } |
---|
533 | $resourceDetails = $this->getResourceClassDetails($resource['class']); |
---|
534 | $resource = $this->resources[$uri] = array( |
---|
535 | 'namespace' => $resourceDetails['namespaceName'], |
---|
536 | 'class' => $resourceDetails['className'], |
---|
537 | 'filename' => $resourceDetails['filename'], |
---|
538 | 'line' => $resourceDetails['line'], |
---|
539 | 'priority' => 0, |
---|
540 | 'loaded' => TRUE |
---|
541 | ); |
---|
542 | } |
---|
543 | |
---|
544 | $this->allowedMethods = array_intersect(array_map('strtoupper', get_class_methods($resource['class'])), $this->HTTPMethods); |
---|
545 | |
---|
546 | return new $resource['class']($parameters); |
---|
547 | } |
---|
548 | |
---|
549 | // no resource found, throw response exception |
---|
550 | throw new ResponseException('A resource matching URI "'.$this->uri.'" was not found', Response::NOTFOUND); |
---|
551 | |
---|
552 | } |
---|
553 | |
---|
554 | /** |
---|
555 | * Check if an etag matches the requests if-match header |
---|
556 | * @param str etag Etag to match |
---|
557 | * @return bool |
---|
558 | */ |
---|
559 | function ifMatch($etag) { |
---|
560 | if (isset($this->ifMatch[0]) && $this->ifMatch[0] == '*') { |
---|
561 | return TRUE; |
---|
562 | } |
---|
563 | return in_array($etag, $this->ifMatch); |
---|
564 | } |
---|
565 | |
---|
566 | /** |
---|
567 | * Check if an etag matches the requests if-none-match header |
---|
568 | * @param str etag Etag to match |
---|
569 | * @return bool |
---|
570 | */ |
---|
571 | function ifNoneMatch($etag) { |
---|
572 | if (isset($this->ifMatch[0]) && $this->ifMatch[0] == '*') { |
---|
573 | return FALSE; |
---|
574 | } |
---|
575 | return in_array($etag, $this->ifNoneMatch); |
---|
576 | } |
---|
577 | |
---|
578 | /** |
---|
579 | * Return the most acceptable of the given formats based on the accept array |
---|
580 | * @param str[] formats |
---|
581 | * @param str default The default format if the requested format does not match $formats |
---|
582 | * @return str |
---|
583 | */ |
---|
584 | function mostAcceptable($formats, $default = NULL) { |
---|
585 | foreach (call_user_func_array('array_merge', $this->accept) as $format) { |
---|
586 | if (in_array($format, $formats)) { |
---|
587 | return $format; |
---|
588 | } |
---|
589 | } |
---|
590 | return $default; |
---|
591 | } |
---|
592 | |
---|
593 | } |
---|
594 | |
---|
595 | /** |
---|
596 | * Base resource class |
---|
597 | * @namespace Tonic\Lib |
---|
598 | */ |
---|
599 | class Resource { |
---|
600 | |
---|
601 | private $parameters; |
---|
602 | |
---|
603 | /** |
---|
604 | * Resource constructor |
---|
605 | * @param str[] parameters Parameters passed in from the URL as matched from the URI regex |
---|
606 | */ |
---|
607 | function __construct($parameters) { |
---|
608 | $this->parameters = $parameters; |
---|
609 | } |
---|
610 | |
---|
611 | /** |
---|
612 | * Convert the object into a string suitable for printing |
---|
613 | * @return str |
---|
614 | * @codeCoverageIgnore |
---|
615 | */ |
---|
616 | function __toString() { |
---|
617 | $str = get_class($this); |
---|
618 | foreach ($this->parameters as $name => $value) { |
---|
619 | $str .= "\n".$name.': '.$value; |
---|
620 | } |
---|
621 | return $str; |
---|
622 | } |
---|
623 | |
---|
624 | /** |
---|
625 | * Execute a request on this resource. |
---|
626 | * @param Request request The request to execute the resource in the context of |
---|
627 | * @return Response |
---|
628 | * @throws ResponseException If the HTTP method is not allowed on the resource, a 405 exception is thrown |
---|
629 | */ |
---|
630 | function exec($request) { |
---|
631 | |
---|
632 | if ( |
---|
633 | in_array(strtoupper($request->method), $request->HTTPMethods) && |
---|
634 | method_exists($this, $request->method) |
---|
635 | ) { |
---|
636 | |
---|
637 | $method = new ReflectionMethod($this, $request->method); |
---|
638 | $parameters = array(); |
---|
639 | foreach ($method->getParameters() as $param) { |
---|
640 | if ($param->name == 'request') { |
---|
641 | $parameters[] = $request; |
---|
642 | } elseif (isset($this->parameters[$param->name])) { |
---|
643 | $parameters[] = $this->parameters[$param->name]; |
---|
644 | unset($this->parameters[$param->name]); |
---|
645 | } else { |
---|
646 | $parameters[] = reset($this->parameters); |
---|
647 | array_shift($this->parameters); |
---|
648 | } |
---|
649 | } |
---|
650 | |
---|
651 | $response = call_user_func_array( |
---|
652 | array($this, $request->method), |
---|
653 | $parameters |
---|
654 | ); |
---|
655 | |
---|
656 | $responseClassName = class_exists('Tonic\\Response') ? 'Tonic\\Response' : 'Response'; |
---|
657 | if (!$response || !($response instanceof $responseClassName)) { |
---|
658 | throw new Exception('Method '.$request->method.' of '.get_class($this).' did not return a Response object'); |
---|
659 | } |
---|
660 | |
---|
661 | } else { |
---|
662 | |
---|
663 | // send 405 method not allowed |
---|
664 | throw new ResponseException( |
---|
665 | 'The HTTP method "'.$request->method.'" is not allowed for the resource "'.$request->uri.'".', |
---|
666 | Response::METHODNOTALLOWED |
---|
667 | ); |
---|
668 | |
---|
669 | } |
---|
670 | |
---|
671 | # good for debugging, remove this at some point |
---|
672 | $response->addHeader('X-Resource', get_class($this)); |
---|
673 | |
---|
674 | return $response; |
---|
675 | |
---|
676 | } |
---|
677 | |
---|
678 | } |
---|
679 | |
---|
680 | /** |
---|
681 | * Model the data of the outgoing HTTP response |
---|
682 | * @namespace Tonic\Lib |
---|
683 | */ |
---|
684 | class Response { |
---|
685 | |
---|
686 | /** |
---|
687 | * HTTP response code constant |
---|
688 | */ |
---|
689 | const OK = 200, |
---|
690 | CREATED = 201, |
---|
691 | NOCONTENT = 204, |
---|
692 | MOVEDPERMANENTLY = 301, |
---|
693 | FOUND = 302, |
---|
694 | SEEOTHER = 303, |
---|
695 | NOTMODIFIED = 304, |
---|
696 | TEMPORARYREDIRECT = 307, |
---|
697 | BADREQUEST = 400, |
---|
698 | UNAUTHORIZED = 401, |
---|
699 | FORBIDDEN = 403, |
---|
700 | NOTFOUND = 404, |
---|
701 | METHODNOTALLOWED = 405, |
---|
702 | NOTACCEPTABLE = 406, |
---|
703 | GONE = 410, |
---|
704 | LENGTHREQUIRED = 411, |
---|
705 | PRECONDITIONFAILED = 412, |
---|
706 | UNSUPPORTEDMEDIATYPE = 415, |
---|
707 | INTERNALSERVERERROR = 500; |
---|
708 | |
---|
709 | /** |
---|
710 | * The request object generating this response |
---|
711 | * @var Request |
---|
712 | */ |
---|
713 | public $request; |
---|
714 | |
---|
715 | /** |
---|
716 | * The HTTP response code to send |
---|
717 | * @var int |
---|
718 | */ |
---|
719 | public $code = Response::OK; |
---|
720 | |
---|
721 | /** |
---|
722 | * The HTTP headers to send |
---|
723 | * @var str[] |
---|
724 | */ |
---|
725 | public $headers = array(); |
---|
726 | |
---|
727 | /** |
---|
728 | * The HTTP response body to send |
---|
729 | * @var str |
---|
730 | */ |
---|
731 | public $body; |
---|
732 | |
---|
733 | /** |
---|
734 | * Create a response object. |
---|
735 | * @param Request request The request object generating this response |
---|
736 | * @param str uri The URL of the actual resource being used to build the response |
---|
737 | */ |
---|
738 | function __construct($request, $uri = NULL) { |
---|
739 | |
---|
740 | $this->request = $request; |
---|
741 | |
---|
742 | if ($uri && $uri != $request->uri) { // add content location header |
---|
743 | $this->addHeader('Content-Location', $uri); |
---|
744 | $this->addVary('Accept'); |
---|
745 | $this->addVary('Accept-Language'); |
---|
746 | } |
---|
747 | $this->addHeader('Allow', implode(', ', $request->allowedMethods)); |
---|
748 | |
---|
749 | } |
---|
750 | |
---|
751 | /** |
---|
752 | * Convert the object into a string suitable for printing |
---|
753 | * @return str |
---|
754 | * @codeCoverageIgnore |
---|
755 | */ |
---|
756 | function __toString() { |
---|
757 | $str = 'HTTP/1.1 '.$this->code; |
---|
758 | foreach ($this->headers as $name => $value) { |
---|
759 | $str .= "\n".$name.': '.$value; |
---|
760 | } |
---|
761 | return $str; |
---|
762 | } |
---|
763 | |
---|
764 | /** |
---|
765 | * Add a header to the response |
---|
766 | * @param str header |
---|
767 | * @param str value |
---|
768 | */ |
---|
769 | function addHeader($header, $value) { |
---|
770 | $this->headers[$header] = $value; |
---|
771 | } |
---|
772 | |
---|
773 | /** |
---|
774 | * Send a cache control header with the response |
---|
775 | * @param int time Cache length in seconds |
---|
776 | */ |
---|
777 | function addCacheHeader($time = 86400) { |
---|
778 | if ($time) { |
---|
779 | $this->addHeader('Cache-Control', 'max-age='.$time.', must-revalidate'); |
---|
780 | } else { |
---|
781 | $this->addHeader('Cache-Control', 'no-cache'); |
---|
782 | } |
---|
783 | } |
---|
784 | |
---|
785 | /** |
---|
786 | * Send an etag with the response |
---|
787 | * @param str etag Etag value |
---|
788 | */ |
---|
789 | function addEtag($etag) { |
---|
790 | $this->addHeader('Etag', '"'.$etag.'"'); |
---|
791 | } |
---|
792 | |
---|
793 | function addVary($header) { |
---|
794 | if (isset($this->headers['Vary'])) { |
---|
795 | $this->headers['Vary'] .= ', '.$header; |
---|
796 | } else { |
---|
797 | $this->addHeader('Vary', $header); |
---|
798 | } |
---|
799 | } |
---|
800 | |
---|
801 | /** |
---|
802 | * Output the response |
---|
803 | * @codeCoverageIgnore |
---|
804 | */ |
---|
805 | function output() { |
---|
806 | |
---|
807 | if (php_sapi_name() != 'cli' && !headers_sent()) { |
---|
808 | |
---|
809 | header('HTTP/1.1 '.$this->code); |
---|
810 | foreach ($this->headers as $header => $value) { |
---|
811 | header($header.': '.$value); |
---|
812 | } |
---|
813 | } |
---|
814 | |
---|
815 | if (strtoupper($this->request->method) !== 'HEAD') { |
---|
816 | echo $this->body; |
---|
817 | } |
---|
818 | |
---|
819 | } |
---|
820 | |
---|
821 | } |
---|
822 | |
---|
823 | /** |
---|
824 | * Exception class for HTTP response errors |
---|
825 | * @namespace Tonic\Lib |
---|
826 | */ |
---|
827 | class ResponseException extends Exception { |
---|
828 | |
---|
829 | /** |
---|
830 | * Generate a default response for this exception |
---|
831 | * @param Request request |
---|
832 | * @return Response |
---|
833 | */ |
---|
834 | function response($request) { |
---|
835 | $response = new Response($request); |
---|
836 | $response->code = $this->code; |
---|
837 | $response->body = $this->message; |
---|
838 | return $response; |
---|
839 | } |
---|
840 | |
---|
841 | } |
---|
842 | |
---|