[6019] | 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 | namespace Tonic; |
---|
| 12 | use \ReflectionClass as ReflectionClass; |
---|
| 13 | use \ReflectionMethod as ReflectionMethod; |
---|
| 14 | use \Exception as Exception; |
---|
| 15 | //*/ |
---|
| 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 | //echo $resource['class']; |
---|
| 531 | if (!class_exists($resource['class'])) { |
---|
| 532 | throw new Exception('Unable to load resource'); |
---|
| 533 | } |
---|
| 534 | $resourceDetails = $this->getResourceClassDetails($resource['class']); |
---|
| 535 | $resource = $this->resources[$uri] = array( |
---|
| 536 | 'namespace' => $resourceDetails['namespaceName'], |
---|
| 537 | 'class' => $resourceDetails['className'], |
---|
| 538 | 'filename' => $resourceDetails['filename'], |
---|
| 539 | 'line' => $resourceDetails['line'], |
---|
| 540 | 'priority' => 0, |
---|
| 541 | 'loaded' => TRUE |
---|
| 542 | ); |
---|
| 543 | } |
---|
| 544 | |
---|
| 545 | $this->allowedMethods = array_intersect(array_map('strtoupper', get_class_methods($resource['class'])), $this->HTTPMethods); |
---|
| 546 | |
---|
| 547 | return new $resource['class']($parameters); |
---|
| 548 | } |
---|
| 549 | |
---|
| 550 | // no resource found, throw response exception |
---|
| 551 | throw new ResponseException('A resource matching URI "'.$this->uri.'" was not found', Response::NOTFOUND); |
---|
| 552 | |
---|
| 553 | } |
---|
| 554 | |
---|
| 555 | /** |
---|
| 556 | * Check if an etag matches the requests if-match header |
---|
| 557 | * @param str etag Etag to match |
---|
| 558 | * @return bool |
---|
| 559 | */ |
---|
| 560 | function ifMatch($etag) { |
---|
| 561 | if (isset($this->ifMatch[0]) && $this->ifMatch[0] == '*') { |
---|
| 562 | return TRUE; |
---|
| 563 | } |
---|
| 564 | return in_array($etag, $this->ifMatch); |
---|
| 565 | } |
---|
| 566 | |
---|
| 567 | /** |
---|
| 568 | * Check if an etag matches the requests if-none-match header |
---|
| 569 | * @param str etag Etag to match |
---|
| 570 | * @return bool |
---|
| 571 | */ |
---|
| 572 | function ifNoneMatch($etag) { |
---|
| 573 | if (isset($this->ifMatch[0]) && $this->ifMatch[0] == '*') { |
---|
| 574 | return FALSE; |
---|
| 575 | } |
---|
| 576 | return in_array($etag, $this->ifNoneMatch); |
---|
| 577 | } |
---|
| 578 | |
---|
| 579 | /** |
---|
| 580 | * Return the most acceptable of the given formats based on the accept array |
---|
| 581 | * @param str[] formats |
---|
| 582 | * @param str default The default format if the requested format does not match $formats |
---|
| 583 | * @return str |
---|
| 584 | */ |
---|
| 585 | function mostAcceptable($formats, $default = NULL) { |
---|
| 586 | foreach (call_user_func_array('array_merge', $this->accept) as $format) { |
---|
| 587 | if (in_array($format, $formats)) { |
---|
| 588 | return $format; |
---|
| 589 | } |
---|
| 590 | } |
---|
| 591 | return $default; |
---|
| 592 | } |
---|
| 593 | |
---|
| 594 | } |
---|
| 595 | |
---|
| 596 | /** |
---|
| 597 | * Base resource class |
---|
| 598 | * @namespace Tonic\Lib |
---|
| 599 | */ |
---|
| 600 | class Resource { |
---|
| 601 | |
---|
| 602 | private $parameters; |
---|
| 603 | |
---|
| 604 | /** |
---|
| 605 | * Resource constructor |
---|
| 606 | * @param str[] parameters Parameters passed in from the URL as matched from the URI regex |
---|
| 607 | */ |
---|
| 608 | function __construct($parameters) { |
---|
| 609 | $this->parameters = $parameters; |
---|
| 610 | } |
---|
| 611 | |
---|
| 612 | /** |
---|
| 613 | * Convert the object into a string suitable for printing |
---|
| 614 | * @return str |
---|
| 615 | * @codeCoverageIgnore |
---|
| 616 | */ |
---|
| 617 | function __toString() { |
---|
| 618 | $str = get_class($this); |
---|
| 619 | foreach ($this->parameters as $name => $value) { |
---|
| 620 | $str .= "\n".$name.': '.$value; |
---|
| 621 | } |
---|
| 622 | return $str; |
---|
| 623 | } |
---|
| 624 | |
---|
| 625 | /** |
---|
| 626 | * Execute a request on this resource. |
---|
| 627 | * @param Request request The request to execute the resource in the context of |
---|
| 628 | * @return Response |
---|
| 629 | * @throws ResponseException If the HTTP method is not allowed on the resource, a 405 exception is thrown |
---|
| 630 | */ |
---|
| 631 | function exec($request) { |
---|
| 632 | |
---|
| 633 | if ( |
---|
| 634 | in_array(strtoupper($request->method), $request->HTTPMethods) && |
---|
| 635 | method_exists($this, $request->method) |
---|
| 636 | ) { |
---|
| 637 | |
---|
| 638 | $method = new ReflectionMethod($this, $request->method); |
---|
| 639 | $parameters = array(); |
---|
| 640 | foreach ($method->getParameters() as $param) { |
---|
| 641 | if ($param->name == 'request') { |
---|
| 642 | $parameters[] = $request; |
---|
| 643 | } elseif (isset($this->parameters[$param->name])) { |
---|
| 644 | $parameters[] = $this->parameters[$param->name]; |
---|
| 645 | unset($this->parameters[$param->name]); |
---|
| 646 | } else { |
---|
| 647 | $parameters[] = reset($this->parameters); |
---|
| 648 | array_shift($this->parameters); |
---|
| 649 | } |
---|
| 650 | } |
---|
| 651 | |
---|
| 652 | $response = call_user_func_array( |
---|
| 653 | array($this, $request->method), |
---|
| 654 | $parameters |
---|
| 655 | ); |
---|
| 656 | |
---|
| 657 | $responseClassName = class_exists('Tonic\\Response') ? 'Tonic\\Response' : 'Response'; |
---|
| 658 | if (!$response || !($response instanceof $responseClassName)) { |
---|
| 659 | throw new Exception('Method '.$request->method.' of '.get_class($this).' did not return a Response object'); |
---|
| 660 | } |
---|
| 661 | |
---|
| 662 | } else { |
---|
| 663 | |
---|
| 664 | // send 405 method not allowed |
---|
| 665 | throw new ResponseException( |
---|
| 666 | 'The HTTP method "'.$request->method.'" is not allowed for the resource "'.$request->uri.'".', |
---|
| 667 | Response::METHODNOTALLOWED |
---|
| 668 | ); |
---|
| 669 | |
---|
| 670 | } |
---|
| 671 | |
---|
| 672 | # good for debugging, remove this at some point |
---|
| 673 | $response->addHeader('X-Resource', get_class($this)); |
---|
| 674 | |
---|
| 675 | return $response; |
---|
| 676 | |
---|
| 677 | } |
---|
| 678 | |
---|
| 679 | } |
---|
| 680 | |
---|
| 681 | /** |
---|
| 682 | * Model the data of the outgoing HTTP response |
---|
| 683 | * @namespace Tonic\Lib |
---|
| 684 | */ |
---|
| 685 | class Response { |
---|
| 686 | |
---|
| 687 | /** |
---|
| 688 | * HTTP response code constant |
---|
| 689 | */ |
---|
| 690 | const OK = 200, |
---|
| 691 | CREATED = 201, |
---|
| 692 | NOCONTENT = 204, |
---|
| 693 | MOVEDPERMANENTLY = 301, |
---|
| 694 | FOUND = 302, |
---|
| 695 | SEEOTHER = 303, |
---|
| 696 | NOTMODIFIED = 304, |
---|
| 697 | TEMPORARYREDIRECT = 307, |
---|
| 698 | BADREQUEST = 400, |
---|
| 699 | UNAUTHORIZED = 401, |
---|
| 700 | FORBIDDEN = 403, |
---|
| 701 | NOTFOUND = 404, |
---|
| 702 | METHODNOTALLOWED = 405, |
---|
| 703 | NOTACCEPTABLE = 406, |
---|
| 704 | GONE = 410, |
---|
| 705 | LENGTHREQUIRED = 411, |
---|
| 706 | PRECONDITIONFAILED = 412, |
---|
| 707 | UNSUPPORTEDMEDIATYPE = 415, |
---|
| 708 | INTERNALSERVERERROR = 500; |
---|
| 709 | |
---|
| 710 | /** |
---|
| 711 | * The request object generating this response |
---|
| 712 | * @var Request |
---|
| 713 | */ |
---|
| 714 | public $request; |
---|
| 715 | |
---|
| 716 | /** |
---|
| 717 | * The HTTP response code to send |
---|
| 718 | * @var int |
---|
| 719 | */ |
---|
| 720 | public $code = Response::OK; |
---|
| 721 | |
---|
| 722 | /** |
---|
| 723 | * The HTTP headers to send |
---|
| 724 | * @var str[] |
---|
| 725 | */ |
---|
| 726 | public $headers = array(); |
---|
| 727 | |
---|
| 728 | /** |
---|
| 729 | * The HTTP response body to send |
---|
| 730 | * @var str |
---|
| 731 | */ |
---|
| 732 | public $body; |
---|
| 733 | |
---|
| 734 | /** |
---|
| 735 | * Create a response object. |
---|
| 736 | * @param Request request The request object generating this response |
---|
| 737 | * @param str uri The URL of the actual resource being used to build the response |
---|
| 738 | */ |
---|
| 739 | function __construct($request, $uri = NULL) { |
---|
| 740 | |
---|
| 741 | $this->request = $request; |
---|
| 742 | |
---|
| 743 | if ($uri && $uri != $request->uri) { // add content location header |
---|
| 744 | $this->addHeader('Content-Location', $uri); |
---|
| 745 | $this->addVary('Accept'); |
---|
| 746 | $this->addVary('Accept-Language'); |
---|
| 747 | } |
---|
| 748 | $this->addHeader('Allow', implode(', ', $request->allowedMethods)); |
---|
| 749 | |
---|
| 750 | } |
---|
| 751 | |
---|
| 752 | /** |
---|
| 753 | * Convert the object into a string suitable for printing |
---|
| 754 | * @return str |
---|
| 755 | * @codeCoverageIgnore |
---|
| 756 | */ |
---|
| 757 | function __toString() { |
---|
| 758 | $str = 'HTTP/1.1 '.$this->code; |
---|
| 759 | foreach ($this->headers as $name => $value) { |
---|
| 760 | $str .= "\n".$name.': '.$value; |
---|
| 761 | } |
---|
| 762 | return $str; |
---|
| 763 | } |
---|
| 764 | |
---|
| 765 | /** |
---|
| 766 | * Add a header to the response |
---|
| 767 | * @param str header |
---|
| 768 | * @param str value |
---|
| 769 | */ |
---|
| 770 | function addHeader($header, $value) { |
---|
| 771 | $this->headers[$header] = $value; |
---|
| 772 | } |
---|
| 773 | |
---|
| 774 | /** |
---|
| 775 | * Send a cache control header with the response |
---|
| 776 | * @param int time Cache length in seconds |
---|
| 777 | */ |
---|
| 778 | function addCacheHeader($time = 86400) { |
---|
| 779 | if ($time) { |
---|
| 780 | $this->addHeader('Cache-Control', 'max-age='.$time.', must-revalidate'); |
---|
| 781 | } else { |
---|
| 782 | $this->addHeader('Cache-Control', 'no-cache'); |
---|
| 783 | } |
---|
| 784 | } |
---|
| 785 | |
---|
| 786 | /** |
---|
| 787 | * Send an etag with the response |
---|
| 788 | * @param str etag Etag value |
---|
| 789 | */ |
---|
| 790 | function addEtag($etag) { |
---|
| 791 | $this->addHeader('Etag', '"'.$etag.'"'); |
---|
| 792 | } |
---|
| 793 | |
---|
| 794 | function addVary($header) { |
---|
| 795 | if (isset($this->headers['Vary'])) { |
---|
| 796 | $this->headers['Vary'] .= ', '.$header; |
---|
| 797 | } else { |
---|
| 798 | $this->addHeader('Vary', $header); |
---|
| 799 | } |
---|
| 800 | } |
---|
| 801 | |
---|
| 802 | /** |
---|
| 803 | * Output the response |
---|
| 804 | * @codeCoverageIgnore |
---|
| 805 | */ |
---|
| 806 | function output() { |
---|
| 807 | |
---|
| 808 | if (php_sapi_name() != 'cli' && !headers_sent()) { |
---|
| 809 | |
---|
| 810 | header('HTTP/1.1 '.$this->code); |
---|
| 811 | foreach ($this->headers as $header => $value) { |
---|
| 812 | header($header.': '.$value); |
---|
| 813 | } |
---|
| 814 | } |
---|
| 815 | |
---|
| 816 | if (strtoupper($this->request->method) !== 'HEAD') { |
---|
| 817 | echo $this->body; |
---|
| 818 | } |
---|
| 819 | |
---|
| 820 | } |
---|
| 821 | |
---|
| 822 | } |
---|
| 823 | |
---|
| 824 | /** |
---|
| 825 | * Exception class for HTTP response errors |
---|
| 826 | * @namespace Tonic\Lib |
---|
| 827 | */ |
---|
| 828 | class ResponseException extends Exception { |
---|
| 829 | |
---|
| 830 | /** |
---|
| 831 | * Generate a default response for this exception |
---|
| 832 | * @param Request request |
---|
| 833 | * @return Response |
---|
| 834 | */ |
---|
| 835 | function response($request) { |
---|
| 836 | $response = new Response($request); |
---|
| 837 | $response->code = $this->code; |
---|
| 838 | $response->body = $this->message; |
---|
| 839 | return $response; |
---|
| 840 | } |
---|
| 841 | |
---|
| 842 | } |
---|
| 843 | |
---|