diff --git a/library/Respect/Rest/Router_1.php b/library/Respect/Rest/Router_1.php new file mode 100644 index 0000000..b41a2d5 --- /dev/null +++ b/library/Respect/Rest/Router_1.php @@ -0,0 +1,269 @@ +callbackRoute($method, $path, $routeTarget); + elseif ($routeTarget instanceof Routable) //direct instances + return $this->instanceRoute($method, $path, $routeTarget); + elseif (!is_string($routeTarget)) //static returns the argument itself + return $this->staticRoute($method, $path, $routeTarget); + elseif (is_string($routeTarget) && !class_exists($routeTarget)) + return $this->staticRoute($method, $path, $routeTarget); + else + if (!isset($args[2])) //raw classnames + return $this->classRoute($method, $path, $routeTarget); + elseif (is_callable($args[2])) //classnames as factories + return $this->factoryRoute($method, $path, $routeTarget, $args[2]); + else //classnames with constructor arguments + return $this->classRoute($method, $path, $routeTarget, $args[2]); + } + + public function __construct($virtualHost=null) + { + $this->virtualHost = $virtualHost; + } + + public function __destruct() + { + if (!$this->isAutoDispatched || !isset($_SERVER['SERVER_PROTOCOL'])) + return; + + echo $this; + } + + public function __toString() + { + return $this->run(); + } + + /** Applies a routine to every route */ + public function always($routineName, $routineParameter) + { + $routineClass = 'Respect\\Rest\\Routines\\' . $routineName; + $routineInstance = new $routineClass($routineParameter); + $this->globalRoutines[] = $routineInstance; + + foreach ($this->routes as $route) + $route->appendRoutine($routineInstance); + + return $this; + } + + /** Appends a pre-built route to the dispatcher */ + public function appendRoute(AbstractRoute $route) + { + $this->routes[] = $route; + + foreach ($this->globalRoutines as $routine) + $route->appendRoutine($routine); + } + + /** Creates and returns a callback-based route */ + public function callbackRoute($method, $path, $callback) + { + $route = new Routes\Callback($method, $path, $callback); + $this->appendRoute($route); + return $route; + } + + /** Creates and returns a class-based route */ + public function classRoute($method, $path, $class, array $arguments=array()) + { + $route = new Routes\ClassName($method, $path, $class, $arguments); + $this->appendRoute($route); + return $route; + } + + /** Dispatch the current route with a standard Request */ + public function dispatch($method=null, $uri=null) + { + return $this->dispatchRequest(new Request($method, $uri)); + } + + /** Dispatch the current route with a custom Request */ + public function dispatchRequest(Request $request=null) + { + $this->isAutoDispatched = false; + if (!$request) + $request = new Request; + + if ($this->methodOverriding && isset($_REQUEST['_method']) && $request->method == 'POST') + $request->method = strtoupper($_REQUEST['_method']); + + if ($request->method === 'OPTIONS' && $request->uri === '*') { + $allowedMethods = array(); + + foreach ($this->routes as $route) + $allowedMethods[] = $route->method; + + if ($allowedMethods) + header('Allow: '.implode(', ', $allowedMethods)); + + return $request; + } + + usort($this->routes, function($a, $b) { + $a = $a->pattern; + $b = $b->pattern; + + if (0 === stripos($a, $b) || $a == AbstractRoute::CATCHALL_IDENTIFIER) + return 1; + elseif (0 === stripos($b, $a) || $b == AbstractRoute::CATCHALL_IDENTIFIER) + return -1; + elseif (substr_count($a, '/') < substr_count($b, '/')) + return 1; + + return substr_count($a, AbstractRoute::PARAM_IDENTIFIER) + < substr_count($b, AbstractRoute::PARAM_IDENTIFIER) ? -1 : 1; + } + ); + + if ($this->virtualHost) + $request->uri = + preg_replace('#^' . preg_quote($this->virtualHost) . '#', '', $request->uri); + + $matchedByPath = array(); + $allowedMethods = array(); + $paramsByPath = new \SplObjectStorage; + + foreach ($this->routes as $route) + if ($this->matchRoute($request, $route, $params)) { + + $paramsByPath[$route] = $params; + + $matchedByPath[] = $route; + $allowedMethods[] = $route->method; + } + + if ($request->method === 'OPTIONS' && $allowedMethods) { + header('Allow: '.implode(', ', $allowedMethods)); + return $request; + } + + if (!$matchedByPath) + header('HTTP/1.1 404'); + + foreach ($matchedByPath as $route) + if (0 !== stripos($request->method, '__') + && ($route->method === $request->method + || $route->method === 'ANY' + || ($route->method === 'GET' && $request->method === 'HEAD'))) + if ($route->matchRoutines($request, $tempParams = $paramsByPath[$route])) + return $this->configureRequest($request, $route, static::cleanUpParams($tempParams)); + else + $badRequest = true; + + if ($matchedByPath && !isset($badRequest)) + header('HTTP/1.1 405'); + + if ($matchedByPath && $allowedMethods) + header('Allow: '.implode(', ', $allowedMethods)); + + $request->route = null; + return $request; + } + + /** Dispatches and get response with default request parameters */ + public function run(Request $request=null) + { + $route = $this->dispatchRequest($request); + if (!$route || (isset($request->method) && $request->method === 'HEAD')) + return null; + + $response = $route->response(); + if (is_resource($response)) { + fpassthru($response); + return ''; + } + return (string) $response; + + } + + /** Creates and returns an factory-based route */ + public function factoryRoute($method, $path, $className, $factory) + { + $route = new Routes\Factory($method, $path, $className, $factory); + $this->appendRoute($route); + return $route; + } + + /** Creates and returns an instance-based route */ + public function instanceRoute($method, $path, $instance) + { + $route = new Routes\Instance($method, $path, $instance); + $this->appendRoute($route); + return $route; + } + + /** Creates and returns a static route */ + public function staticRoute($method, $path, $instance) + { + $route = new Routes\StaticValue($method, $path, $instance); + $this->appendRoute($route); + return $route; + } + + /** Configures a request for a specific route with specific parameters */ + protected function configureRequest(Request $request, AbstractRoute $route, array $params=array()) + { + $request->route = $route; + $request->params = $params; + return $request; + } + + /** Returns true if the passed route matches the passed request */ + protected function matchRoute(Request $request, AbstractRoute $route, &$params=array()) + { + if ($route->match($request, $params)) { + $request->route = $route; + return true; + } + } + +} diff --git a/library/Respect/Rest/Routines/AbstractAccept.php b/library/Respect/Rest/Routines/AbstractAccept.php index 149ee76..b8d80c2 100644 --- a/library/Respect/Rest/Routines/AbstractAccept.php +++ b/library/Respect/Rest/Routines/AbstractAccept.php @@ -55,12 +55,36 @@ protected function negotiate(Request $request) arsort($acceptList); foreach ($acceptList as $requested => $quality) foreach ($this->callbacksPerMimeType as $provided => $callback) - if ($this->compareItens($requested, $provided)) - return $this->negotiated[$request] = $callback; + if (false !== ($accepted = $this->compareItens($requested, $provided))) { + $this->negotiated[$request] = $callback; + return $this->responseHeaders($accepted); + } return false; } + private function responseHeaders($negotiated) { + $header_type = preg_replace( + array( + '/(^.*)(?=\w*$)/U', // select namespace to strip + '/(?!^)([A-Z]+)/' // select camels to add - + ), + array('','-$1'), get_class($this)); + + $content_header = 'Content-Type'; + + if (false !== strpos($header_type, '-')) + $content_header = str_replace('Accept', 'Content', $header_type); + + header("$content_header: $negotiated"); // RFC 2616 + header("Vary: negotiate,".strtolower($header_type)); // RFC 2616/2295 + header("Content-Location: {$_SERVER['REQUEST_URI']}"); // RFC 2616 + header('Expires: Thu, 01 Jan 1980 00:00:00 GMT'); // RFC 2295 + header('Cache-Control: max-age=86400'); // RFC 2295 + + return true; + } + public function by(Request $request, $params) { $unsyncedParams = $request->params; diff --git a/library/Respect/Rest/Routines/AbstractCallbacksPerType.php b/library/Respect/Rest/Routines/AbstractCallbacksPerType.php new file mode 100644 index 0000000..149ee76 --- /dev/null +++ b/library/Respect/Rest/Routines/AbstractCallbacksPerType.php @@ -0,0 +1,98 @@ +negotiated = new SplObjectStorage; + $this->parseAcceptMap($callbacksPerType); + } + + /** Parses an array of callbacks per accept-type */ + protected function parseAcceptMap(array $callbacksPerType) + { + if (!array_filter($callbacksPerType, 'is_callable')) + throw new UnexpectedValueException('Not a callable argument for Content-Type negotiation.'); + + foreach ($callbacksPerType as $acceptSpec => $callback) + if ('.' === $acceptSpec[0]) + $this->callbacksPerExtension[$acceptSpec] = $callback; + else + $this->callbacksPerMimeType[$acceptSpec] = $callback; + } + + /** Negotiate content with the given Request */ + protected function negotiate(Request $request) + { + foreach ($this->callbacksPerExtension as $provided => $callback) + if (false !== stripos($request->uri, $provided)) + return $this->negotiated[$request] = $callback; + + if (!isset($_SERVER[static::ACCEPT_HEADER])) + return false; + + $acceptHeader = $_SERVER[static::ACCEPT_HEADER]; + $acceptParts = explode(',', $acceptHeader); + $acceptList = array(); + foreach ($acceptParts as $k => &$acceptPart) { + $parts = explode(';q=', trim($acceptPart)); + $provided = array_shift($parts); + $quality = array_shift($parts) ? : (10000 - $k) / 10000; + $acceptList[$provided] = $quality; + } + arsort($acceptList); + foreach ($acceptList as $requested => $quality) + foreach ($this->callbacksPerMimeType as $provided => $callback) + if ($this->compareItens($requested, $provided)) + return $this->negotiated[$request] = $callback; + + return false; + } + + public function by(Request $request, $params) + { + $unsyncedParams = $request->params; + $extensions = array_keys($this->callbacksPerExtension); + + if (empty($extensions) || empty($unsyncedParams)) + return; + + $unsyncedParams[] = str_replace( + $extensions, '', array_pop($unsyncedParams) + ); + $request->params = $unsyncedParams; + } + + public function through(Request $request, $params) + { + if (!isset($this->negotiated[$request]) + || false === $this->negotiated[$request]) + return; + + return $this->negotiated[$request]; + } + + public function when(Request $request, $params) + { + return false !== $this->negotiate($request); + } + + /** Compares two given content-negotiation elements */ + protected function compareItens($requested, $provided) + { + return $requested == $provided; + } + +} diff --git a/library/Respect/Rest/Routines/Accept.php b/library/Respect/Rest/Routines/Accept.php index 23a1b85..e785414 100644 --- a/library/Respect/Rest/Routines/Accept.php +++ b/library/Respect/Rest/Routines/Accept.php @@ -11,19 +11,14 @@ class Accept extends AbstractAccept protected function compareItens($requested, $provided) { - if ($requested === $provided || $requested === '*/*') { - header('Content-Type: '.$provided); - return true; - } - + if ($requested === $provided || $requested === '*/*') + return $provided; list($requestedA, $requestedB) = explode('/', $requested); list($providedA, ) = explode('/', $provided); - if ($providedA === $requestedA && $requestedB === '*') { - header('Content-Type: '.$providedA); - return true; - } + if ($providedA === $requestedA && $requestedB === '*') + return $providedA; return false; } diff --git a/library/Respect/Rest/Routines/AcceptLanguage.php b/library/Respect/Rest/Routines/AcceptLanguage.php index 1048aad..d625248 100644 --- a/library/Respect/Rest/Routines/AcceptLanguage.php +++ b/library/Respect/Rest/Routines/AcceptLanguage.php @@ -15,7 +15,7 @@ protected function compareItens($requested, $provided) $provided = preg_replace('/^x\-/', '', $provided); if ($requested == $provided) - return true; + return $provided; if (stripos($requested, '-') || !stripos($provided, '-')) return false; @@ -23,7 +23,7 @@ protected function compareItens($requested, $provided) list($providedA, ) = explode('-', $provided); if ($requested === $providedA) - return true; + return $providedA; return false; } diff --git a/tests/library/Respect/Rest/RouterTest.php b/tests/library/Respect/Rest/RouterTest.php index 17cbe3f..cf8f01f 100644 --- a/tests/library/Respect/Rest/RouterTest.php +++ b/tests/library/Respect/Rest/RouterTest.php @@ -511,6 +511,45 @@ static function provider_content_type_extension() array('text/xml','.xml') ); } + function test_negotiate_acceptable_complete_headers() + { + global $header; + $_SERVER['REQUEST_URI'] = '/accept'; + $_SERVER['HTTP_ACCEPT'] = 'foo/bar'; + $_SERVER['HTTP_ACCEPT_LANGUAGE'] = '13375p34|<'; + $this->router->get('/accept', function() { return 'ok'; }) + ->accept(array('foo/bar' => function($d) {return $d;})) + ->acceptLanguage(array('13375p34|<' => function($d) {return $d;})); + $this->router->dispatch('get', '/accept'); + \print_r(implode("\n", $header)); + $this->assertContains('Content-Type: foo/bar', $header); + $this->assertContains('Content-Language: 13375p34|<', $header); + $this->assertRegExp('/Vary: negotiate,.*accept(?!-)/', implode("\n", $header)); + $this->assertRegExp('/Vary: negotiate,.*accept-language/', implode("\n", $header)); + $this->assertContains('Content-Location: /accept', $header); + $this->assertContains('Expires: Thu, 01 Jan 1980 00:00:00 GMT', $header); + $this->assertContains('Cache-Control: max-age=86400', $header); + } + function test_accept_content_type_header() + { + global $header; + $_SERVER['HTTP_ACCEPT'] = 'foo/bar'; + $this->router->get('/', function() { return 'ok'; }) + ->accept(array('foo/bar' => function($d) {return $d;})); + $this->router->dispatch('get', '/'); + $this->assertContains('Content-Type: foo/bar', $header); + $this->assertRegExp('/Vary: negotiate,.*accept(?!-)/', implode("\n", $header)); + } + function test_accept_content_language_header() + { + global $header; + $_SERVER['HTTP_ACCEPT_LANGUAGE'] = '13375p34|<'; + $this->router->get('/', function() { return 'ok'; }) + ->acceptLanguage(array('13375p34|<' => function($d) {return $d;})); + $this->router->dispatch('get', '/'); + $this->assertContains('Content-Language: 13375p34|<', $header); + $this->assertRegExp('/Vary: negotiate,.*accept-language/', implode("\n", $header)); + } /** * @dataProvider provider_content_type_extension * @ticket 44