From f48a28f1684e66164cfc6f249e27d4c0cc34d817 Mon Sep 17 00:00:00 2001 From: Matthew Schultz Date: Fri, 28 Aug 2015 12:52:41 -0500 Subject: [PATCH 1/4] Changed the content type default to use uppercase so when the after closure in Resource::provides sets the content type, it overrides the response content type instead of creating another one with a different case. --- src/Tonic/Response.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tonic/Response.php b/src/Tonic/Response.php index 1506b50..28e2073 100644 --- a/src/Tonic/Response.php +++ b/src/Tonic/Response.php @@ -57,7 +57,7 @@ class Response public $code = self::NOCONTENT, $body = null, - $headers = array('content-type' => 'text/html'); + $headers = array('Content-Type' => 'text/html'); public function __construct($code = null, $body = null, $headers = array()) { From 18bac3257dda3b1458e4bb2debef0761d4501c5d Mon Sep 17 00:00:00 2001 From: Matthew Schultz Date: Fri, 28 Aug 2015 16:35:44 -0500 Subject: [PATCH 2/4] Fixed Resource::allowedMethods and Resource:options so it reports the allowed http methods again. --- src/Tonic/Resource.php | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/Tonic/Resource.php b/src/Tonic/Resource.php index 7c4366a..a6890b9 100644 --- a/src/Tonic/Resource.php +++ b/src/Tonic/Resource.php @@ -167,17 +167,12 @@ public function exec() */ public function options() { - $options = array(); - - $resourceMetadata = $this->app->getResourceMetadata($this); - - foreach ($resourceMetadata->getMethods() as $method => $methodMetadata) { - $options[] = strtoupper($method); - } - - return new Response(200, $options, array( - 'Allow' => implode(',', $options) + $options = implode(',', $this->allowedMethods()); + $response = new Response(200, $options, array( + 'Allow' => $options )); + $response->contentType = 'text/plain'; + return $response; } /** @@ -297,9 +292,18 @@ public function allowedMethods() { $metadata = $this->app->getResourceMetadata($this); $allowedMethods = array(); - foreach ($metadata['methods'] as $method => $properties) { - foreach ($properties['method'] as $method) { - $allowedMethods[] = strtoupper($method[0]); + foreach ($metadata->getMethods() as $method => $methodMetadata) { + foreach ($methodMetadata->getConditions() as $key => $values) { + if ($key !== 'method') { + continue; + } + foreach ($values as $value) { + if (in_array($value, $allowedMethods)) { + continue; + } + $allowedMethods[] = $value; + + } } } return array_values(array_unique($allowedMethods)); From c2f49964f9d895c56d5a89fdd19963e594eb1161 Mon Sep 17 00:00:00 2001 From: Matthew Schultz Date: Fri, 28 Aug 2015 16:48:38 -0500 Subject: [PATCH 3/4] Removed unnecessary array_values call in Resource::allowedMethods. --- src/Tonic/Resource.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Tonic/Resource.php b/src/Tonic/Resource.php index a6890b9..22f2a7b 100644 --- a/src/Tonic/Resource.php +++ b/src/Tonic/Resource.php @@ -302,11 +302,10 @@ public function allowedMethods() continue; } $allowedMethods[] = $value; - } } } - return array_values(array_unique($allowedMethods)); + return array_unique($allowedMethods); } public function __toString() From 7d34026a9ae2fc3e4ba0f1b5389e1fc8f611369c Mon Sep 17 00:00:00 2001 From: Matthew Schultz Date: Wed, 2 Sep 2015 22:01:09 -0500 Subject: [PATCH 4/4] Added support for rfc2616 sec 14.1 accept-params. --- src/Tonic/Request.php | 67 ++++++++++++++++++++++++++++++------------ src/Tonic/Resource.php | 40 +++++++++++++++++++++---- 2 files changed, 84 insertions(+), 23 deletions(-) diff --git a/src/Tonic/Request.php b/src/Tonic/Request.php index 7cbc771..4e2dc87 100644 --- a/src/Tonic/Request.php +++ b/src/Tonic/Request.php @@ -13,6 +13,7 @@ class Request protected $contentType; protected $data; protected $accept = array(); + protected $acceptParams = array(); protected $acceptLanguage = array(); protected $ifMatch = array(); protected $ifNoneMatch = array(); @@ -62,7 +63,9 @@ public function __construct($options = array()) $this->contentType = $this->getContentTypeFromEnvironment($options); $this->data = $this->getDataFromEnvironment($options); - $this->accept = array_unique(array_merge($this->accept, $this->getAcceptArrayFromEnvironment($this->getOption($options, 'accept')))); + $accept = $this->getAcceptArrayFromEnvironment($this->getOption($options, 'accept'), $acceptParams); + $this->acceptParams = $acceptParams; + $this->accept = array_merge($this->accept, $accept); $this->acceptLanguage = array_unique(array_merge($this->acceptLanguage, $this->getAcceptArrayFromEnvironment($this->getOption($options, 'acceptLanguage')))); $this->ifMatch = $this->getMatchArrayFromEnvironment($this->getOption($options, 'ifMatch')); @@ -175,6 +178,11 @@ public function setAccept($value) } } + public function getAcceptParams() + { + return $this->acceptParams; + } + public function getAcceptLanguage() { return $this->acceptLanguage; @@ -243,7 +251,7 @@ private function getMethodFromEnvironment($options) $override = strtoupper($this->getHeader('xHttpMethodOverride')); if ($override && $method == 'POST') { $method = $override; - + } else { // get override value from URL and use if applicable if ( @@ -254,7 +262,7 @@ private function getMethodFromEnvironment($options) if (preg_match('/![A-Z]+$/', $this->uri, $match, PREG_OFFSET_CAPTURE)) { $method = strtoupper(substr($this->uri, $match[0][1] + 1)); $this->uri = substr($this->uri, 0, $match[0][1]); - + // get override value from _method querystring } elseif (isset($_GET['_method'])) { $method = strtoupper($_GET['_method']); @@ -326,28 +334,51 @@ private function getURIFromEnvironment($options) } /** - * Get accepted content mimetypes from request header - * @param str $acceptString + * Get accepted content mimetypes and accept parameters from request header + * @param str $acceptString accept string + * @param array $params accept parameters * @return str[] */ - private function getAcceptArrayFromEnvironment($acceptString) + private function getAcceptArrayFromEnvironment($acceptString, &$acceptParamArray = array()) { - $accept = $acceptArray = array(); - foreach (explode(',', strtolower($acceptString)) as $part) { - $parts = preg_split('/\s*;\s*q=/', $part); - if (isset($parts) && isset($parts[1]) && $parts[1]) { - $num = $parts[1] * 10; - } else { - $num = 10; + $accept = $acceptArray = $acceptParam = $acceptParamArray = array(); + $parts = preg_split('/\s*,\s*/', strtolower($acceptString)); + foreach ($parts as $part) { + if (empty($part)) { + continue; + } + $partParams = preg_split('/\s*;\s*/', $part); + $type = array_shift($partParams); + $num = 10; + $acceptParams = array(); + foreach ($partParams as $param) { + $keyValue = preg_split('/\s*=\s*/', $param); + if ($keyValue[0] === 'q') { + if (array_key_exists(1, $keyValue) && is_numeric($keyValue[1]) && (int)$keyValue[1] >= 0 + && (int)$keyValue[1] <= 1) { + $num = $keyValue[1] * 10; + } + } else { + $key = $keyValue[0]; + $value = array_key_exists(1, $keyValue) ? $keyValue[1] : null; + $acceptParams[] = $key . '=' . $value; + } + } + // quality of 0 is not acceptable to the client - rfc2616 sec 3.9 + if ($num === 0) { + $type = null; } - if ($parts[0]) { - $accept[$num][] = $parts[0]; + if ($type) { + $accept[$num][] = $type; + $acceptParam[$num][] = $acceptParams; } } krsort($accept); - foreach ($accept as $parts) { - foreach ($parts as $part) { - $acceptArray[] = trim($part); + krsort($acceptParam); + foreach ($accept as $i => $parts) { + foreach ($parts as $j => $part) { + $acceptArray[] = $part; + $acceptParamArray[] = $acceptParam[$i][$j]; } } diff --git a/src/Tonic/Resource.php b/src/Tonic/Resource.php index 22f2a7b..6f7ab72 100644 --- a/src/Tonic/Resource.php +++ b/src/Tonic/Resource.php @@ -236,18 +236,48 @@ protected function accepts($mimetype) /** * Provides condition mimetype must be in request accept array, returns a number * based on the priority of the match. - * @param str $mimetype + * @param str $mimetype + * @param str $parameter variable number of key=value parameters to check for in the accept header * @return int */ - protected function provides($mimetype) + protected function provides() { - if (count($this->request->getAccept()) == 0) return 0; - $pos = array_search($mimetype, $this->request->getAccept()); + $params = func_get_args(); + $mimetype = array_shift($params); + + if (count($this->request->getAccept()) == 0) { + return 0; + } + $acceptParams = $this->request->getAcceptParams(); + $pos = false; + foreach ($this->request->getAccept() as $i => $acceptMimetype) { + if ($mimetype === $acceptMimetype) { + foreach ($acceptParams[$i] as $acceptParam) { + if (array_search($acceptParam, $params) === false) { + continue 2; + } + } + $pos = $i; + break; + } + } if ($pos === FALSE) { if (in_array('*/*', $this->request->getAccept())) { return 0; } else { - throw new NotAcceptableException('No matching method for response type "'.join(', ', $this->request->getAccept()).'"'); + $responseTypes = array(); + + foreach ($this->request->getAccept() as $i => $acceptMimetype) { + $responseType = $acceptMimetype; + foreach ($acceptParams[$i] as $acceptParam) { + if (! empty($acceptParam)) { + $responseType .= ';' . $acceptParam; + } + } + $responseTypes[] = $responseType; + } + + throw new NotAcceptableException('No matching method for response type "'.join(', ', $responseTypes).'"'); } } else { $this->after(function ($response) use ($mimetype) {