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