From 3a92c940a99fbeb95de22cce73efee4a9bce8a17 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Fri, 8 Apr 2016 23:29:44 +0200 Subject: [PATCH] implementing media type negotiation (based on language negotiation logic) in cases both JSON and (X)HTML are being requested, resolving #68 --- lib/i18n.php | 6 ++- lib/request.php | 107 ++++++++++++++++++++++++++++++++++++++++++++---- lib/zerobin.php | 2 +- tst/request.php | 48 ++++++++++++++++++++++ 4 files changed, 151 insertions(+), 12 deletions(-) diff --git a/lib/i18n.php b/lib/i18n.php index 0900ef30..30d7d116 100644 --- a/lib/i18n.php +++ b/lib/i18n.php @@ -198,7 +198,8 @@ class i18n if (array_key_exists('HTTP_ACCEPT_LANGUAGE', $_SERVER)) { $languageRanges = explode(',', trim($_SERVER['HTTP_ACCEPT_LANGUAGE'])); - foreach ($languageRanges as $languageRange) { + foreach ($languageRanges as $languageRange) + { if (preg_match( '/(\*|[a-zA-Z0-9]{1,8}(?:-[a-zA-Z0-9]{1,8})*)(?:\s*;\s*q\s*=\s*(0(?:\.\d{0,3})|1(?:\.0{0,3})))?/', trim($languageRange), $match @@ -325,7 +326,8 @@ class i18n protected static function _getMatchingLanguage($acceptedLanguages, $availableLanguages) { $matches = array(); $any = false; - foreach ($acceptedLanguages as $acceptedQuality => $acceptedValues) { + foreach ($acceptedLanguages as $acceptedQuality => $acceptedValues) + { $acceptedQuality = floatval($acceptedQuality); if ($acceptedQuality === 0.0) continue; foreach ($availableLanguages as $availableValue) diff --git a/lib/request.php b/lib/request.php index baa67064..3a7fe30a 100644 --- a/lib/request.php +++ b/lib/request.php @@ -17,6 +17,27 @@ */ class request { + /** + * MIME type for JSON + * + * @const string + */ + const MIME_JSON = 'application/json'; + + /** + * MIME type for HTML + * + * @const string + */ + const MIME_HTML = 'text/html'; + + /** + * MIME type for XHTML + * + * @const string + */ + const MIME_XHTML = 'application/xhtml+xml'; + /** * Input stream to use for PUT parameter parsing. * @@ -66,15 +87,7 @@ class request } // decide if we are in JSON API or HTML context - if ( - (array_key_exists('HTTP_X_REQUESTED_WITH', $_SERVER) && - $_SERVER['HTTP_X_REQUESTED_WITH'] == 'JSONHttpRequest') || - (array_key_exists('HTTP_ACCEPT', $_SERVER) && - strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false) - ) - { - $this->_isJsonApi = true; - } + $this->_isJsonApi = $this->_detectJsonRequest(); // parse parameters, depending on request type switch (array_key_exists('REQUEST_METHOD', $_SERVER) ? $_SERVER['REQUEST_METHOD'] : 'GET') @@ -168,4 +181,80 @@ class request { self::$_inputStream = $input; } + + /** + * detect the clients supported media type and decide if its a JSON API call or not + * + * Adapted from: http://stackoverflow.com/questions/3770513/detect-browser-language-in-php#3771447 + * + * @access private + * @return bool + */ + private function _detectJsonRequest() + { + $hasAcceptHeader = array_key_exists('HTTP_ACCEPT', $_SERVER); + $acceptHeader = $hasAcceptHeader ? $_SERVER['HTTP_ACCEPT'] : ''; + + // simple cases + if ( + (array_key_exists('HTTP_X_REQUESTED_WITH', $_SERVER) && + $_SERVER['HTTP_X_REQUESTED_WITH'] == 'JSONHttpRequest') || + ($hasAcceptHeader && + strpos($acceptHeader, self::MIME_JSON) !== false && + strpos($acceptHeader, self::MIME_HTML) === false && + strpos($acceptHeader, self::MIME_XHTML) === false) + ) + { + return true; + } + + // advanced case: media type negotiation + $mediaTypes = array(); + if ($hasAcceptHeader) + { + $mediaTypeRanges = explode(',', trim($acceptHeader)); + foreach ($mediaTypeRanges as $mediaTypeRange) + { + if (preg_match( + '#(\*/\*|[a-z\-]+/[a-z\-+*]+(?:\s*;\s*[^q]\S*)*)(?:\s*;\s*q\s*=\s*(0(?:\.\d{0,3})|1(?:\.0{0,3})))?#', + trim($mediaTypeRange), $match + )) + { + if (!isset($match[2])) + { + $match[2] = '1.0'; + } + else + { + $match[2] = (string) floatval($match[2]); + } + if (!isset($mediaTypes[$match[2]])) + { + $mediaTypes[$match[2]] = array(); + } + $mediaTypes[$match[2]][] = strtolower($match[1]); + } + } + krsort($mediaTypes); + foreach ($mediaTypes as $acceptedQuality => $acceptedValues) + { + if ($acceptedQuality === 0.0) continue; + foreach ($acceptedValues as $acceptedValue) + { + if ( + strpos($acceptedValue, self::MIME_HTML) === 0 || + strpos($acceptedValue, self::MIME_XHTML) === 0 + ) + { + return false; + } + elseif (strpos($acceptedValue, self::MIME_JSON) === 0) + { + return true; + } + } + } + } + return false; + } } \ No newline at end of file diff --git a/lib/zerobin.php b/lib/zerobin.php index 9e02fcb2..b279dd2e 100644 --- a/lib/zerobin.php +++ b/lib/zerobin.php @@ -151,7 +151,7 @@ class zerobin // output JSON or HTML if ($this->_request->isJsonApiCall()) { - header('Content-type: application/json'); + header('Content-type: ' . request::MIME_JSON); header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE'); header('Access-Control-Allow-Headers: X-Requested-With, Content-Type'); diff --git a/tst/request.php b/tst/request.php index 0df21927..cf4a4eb2 100644 --- a/tst/request.php +++ b/tst/request.php @@ -104,4 +104,52 @@ class requestTest extends PHPUnit_Framework_TestCase $this->assertEquals('foo', $request->getParam('pasteid')); $this->assertEquals('bar', $request->getParam('deletetoken')); } + + public function testReadWithNegotiation() + { + $this->reset(); + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['HTTP_ACCEPT'] = 'text/html,text/html; charset=UTF-8,application/xhtml+xml, application/xml;q=0.9,*/*;q=0.8, text/csv,application/json'; + $_SERVER['QUERY_STRING'] = 'foo'; + $request = new request; + $this->assertFalse($request->isJsonApiCall(), 'is HTML call'); + $this->assertEquals('foo', $request->getParam('pasteid')); + $this->assertEquals('read', $request->getOperation()); + } + + public function testReadWithXhtmlNegotiation() + { + $this->reset(); + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['HTTP_ACCEPT'] = 'application/xhtml+xml,text/html,text/html; charset=UTF-8, application/xml;q=0.9,*/*;q=0.8, text/csv,application/json'; + $_SERVER['QUERY_STRING'] = 'foo'; + $request = new request; + $this->assertFalse($request->isJsonApiCall(), 'is HTML call'); + $this->assertEquals('foo', $request->getParam('pasteid')); + $this->assertEquals('read', $request->getOperation()); + } + + public function testApiReadWithNegotiation() + { + $this->reset(); + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['HTTP_ACCEPT'] = 'text/plain,text/csv, application/xml;q=0.9, application/json, text/html,text/html; charset=UTF-8,application/xhtml+xml, */*;q=0.8'; + $_SERVER['QUERY_STRING'] = 'foo'; + $request = new request; + $this->assertTrue($request->isJsonApiCall(), 'is JSON Api call'); + $this->assertEquals('foo', $request->getParam('pasteid')); + $this->assertEquals('read', $request->getOperation()); + } + + public function testReadWithFailedNegotiation() + { + $this->reset(); + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['HTTP_ACCEPT'] = 'text/plain,text/csv, application/xml;q=0.9, */*;q=0.8'; + $_SERVER['QUERY_STRING'] = 'foo'; + $request = new request; + $this->assertFalse($request->isJsonApiCall(), 'is HTML call'); + $this->assertEquals('foo', $request->getParam('pasteid')); + $this->assertEquals('read', $request->getOperation()); + } } \ No newline at end of file