diff --git a/core/composer.json b/core/composer.json index eaf5983..c6c66aa 100644 --- a/core/composer.json +++ b/core/composer.json @@ -13,7 +13,8 @@ "symfony/yaml": "2.1.*", "twig/twig": "1.8.*", "doctrine/common": "2.3.*", - "kriswallsmith/assetic": "1.1.*" + "kriswallsmith/assetic": "1.1.*", + "bitworking/mimeparse": "*" }, "minimum-stability": "alpha" } diff --git a/core/vendor/bitworking/mimeparse/.gitignore b/core/vendor/bitworking/mimeparse/.gitignore new file mode 100644 index 0000000..7e8f330 --- /dev/null +++ b/core/vendor/bitworking/mimeparse/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +phpunit.xml +composer.lock +build +vendor +*.phar diff --git a/core/vendor/bitworking/mimeparse/.travis.yml b/core/vendor/bitworking/mimeparse/.travis.yml new file mode 100644 index 0000000..4414a94 --- /dev/null +++ b/core/vendor/bitworking/mimeparse/.travis.yml @@ -0,0 +1,11 @@ +language: php + +php: + - 5.3 + - 5.4 + +before_script: + - curl -s http://getcomposer.org/installer | php -- --quiet + - php composer.phar install + +script: phpunit diff --git a/core/vendor/bitworking/mimeparse/LICENSE b/core/vendor/bitworking/mimeparse/LICENSE new file mode 100644 index 0000000..c337658 --- /dev/null +++ b/core/vendor/bitworking/mimeparse/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2010 Joe Gregorio + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/core/vendor/bitworking/mimeparse/README.md b/core/vendor/bitworking/mimeparse/README.md new file mode 100644 index 0000000..02b1232 --- /dev/null +++ b/core/vendor/bitworking/mimeparse/README.md @@ -0,0 +1,68 @@ +# Bitworking\Mimeparse + +[![Build Status](https://secure.travis-ci.org/ramsey/mimeparse.png)](http://travis-ci.org/ramsey/mimeparse) + +This library provides basic functionality for parsing mime-types names and +matching them against a list of media-ranges. See [section 14.1][http-accept] of +[RFC 2616 (the HTTP specification)][http] for a complete explanation. More +information on the library can be found in the XML.com article +"[Just use Media Types?][jgregorio-restful]" + +This library was taken from the [original mimeparse library][mimeparse] +on Google Project Hosting and has been cleaned up to conform to [PSR-0][], +[PSR-1][], and [PSR-2][] standards. It also now has support for [Composer][]. +The Bitworking namespace is a nod to [Joe Gregorio][jgregorio], the original +author of this library. + +## Examples + +Use Mimeparse to specify a list of media types your application supports and +compare that to the list of media types the user agent accepts (via the +[HTTP Accept][http-accept] header; `$_SERVER['HTTP_ACCEPT']`). Mimeparse will +give you the best match to send back to the user agent for your list of +supported types or `null` if there is no best match. + +```php +.", + "type": "library", + "keywords": ["http", "accept", "mime"], + "homepage": "http://code.google.com/p/mimeparse/", + "license": "MIT", + "authors": [ + { + "name": "Joe Gregorio", + "homepage": "http://bitworking.org" + }, + { + "name": "Andrew \"Venom\" K." + }, + { + "name": "Ben Ramsey", + "homepage": "http://benramsey.com" + } + ], + "support": { + "issues": "https://github.com/ramsey/mimeparse/issues", + "source": "https://github.com/ramsey/mimeparse" + }, + "require": { + "php": ">=5.3.3" + }, + "autoload": { + "psr-0": { "Bitworking": "src/" } + } +} diff --git a/core/vendor/bitworking/mimeparse/phpunit.xml.dist b/core/vendor/bitworking/mimeparse/phpunit.xml.dist new file mode 100644 index 0000000..d3ce1a2 --- /dev/null +++ b/core/vendor/bitworking/mimeparse/phpunit.xml.dist @@ -0,0 +1,22 @@ + + + + + ./tests + + + + + ./src + + + + + + + + + diff --git a/core/vendor/bitworking/mimeparse/src/Bitworking/Mimeparse.php b/core/vendor/bitworking/mimeparse/src/Bitworking/Mimeparse.php new file mode 100644 index 0000000..1b25caa --- /dev/null +++ b/core/vendor/bitworking/mimeparse/src/Bitworking/Mimeparse.php @@ -0,0 +1,248 @@ + "0.5" ), "xml") + * + * @param string $mediaRange + * @return array ($type, $subtype, $params, $genericSubtype) + * @throws UnexpectedValueException when $mediaRange does not include a + * valid subtype + */ + public static function parseMediaRange($mediaRange) + { + $parts = explode(';', $mediaRange); + + $params = array(); + foreach ($parts as $i => $param) { + if (strpos($param, '=') !== false) { + list($k, $v) = explode('=', trim($param)); + $params[$k] = $v; + } + } + + $fullType = trim($parts[0]); + + // Java URLConnection class sends an Accept header that includes a + // single "*". Turn it into a legal wildcard. + if ($fullType == '*') { + $fullType = '*/*'; + } + + list($type, $subtype) = explode('/', $fullType); + + if (!$subtype) { + throw new \UnexpectedValueException('Malformed media-range: '.$mediaRange); + } + + $plusPos = strpos($subtype, '+'); + if (false !== $plusPos) { + $genericSubtype = substr($subtype, $plusPos + 1); + } else { + $genericSubtype = $subtype; + } + + return array(trim($type), trim($subtype), $params, $genericSubtype); + } + + + /** + * Parses a media-range via Mimeparse::parseMediaRange() and guarantees that + * there is a value for the "q" param, filling it in with a proper default + * if necessary. + * + * @param string $mediaRange + * @return array ($type, $subtype, $params, $genericSubtype) + */ + protected static function parseAndNormalizeMediaRange($mediaRange) + { + $parsedMediaRange = self::parseMediaRange($mediaRange); + $params = $parsedMediaRange[2]; + + if (!isset($params['q']) + || !is_numeric($params['q']) + || floatval($params['q']) > 1 + || floatval($params['q']) < 0 + ) { + $parsedMediaRange[2]['q'] = '1'; + } + + return $parsedMediaRange; + } + + /** + * Find the best match for a given mime-type against a list of + * media-ranges that have already been parsed by + * Mimeparse::parseAndNormalizeMediaRange() + * + * Returns the fitness and the "q" quality parameter of the best match, or + * an array [-1, 0] if no match was found. Just as for + * Mimeparse::quality(), $parsedRanges must be an array of parsed + * media-ranges. + * + * @param string $mimeType + * @param array $parsedRanges + * @return array ($bestFitQuality, $bestFitness) + */ + protected static function qualityAndFitnessParsed($mimeType, $parsedRanges) + { + $bestFitness = -1; + $bestFitQuality = 0; + list($targetType, $targetSubtype, $targetParams) = self::parseAndNormalizeMediaRange($mimeType); + + foreach ($parsedRanges as $item) { + list($type, $subtype, $params) = $item; + + if (($type == $targetType || $type == '*' || $targetType == '*') + && ($subtype == $targetSubtype || $subtype == '*' || $targetSubtype == '*')) { + + $paramMatches = 0; + foreach ($targetParams as $k => $v) { + if ($k != 'q' && isset($params[$k]) && $v == $params[$k]) { + $paramMatches++; + } + } + + $fitness = ($type == $targetType) ? 100 : 0; + $fitness += ($subtype == $targetSubtype) ? 10 : 0; + $fitness += $paramMatches; + + if ($fitness > $bestFitness) { + $bestFitness = $fitness; + $bestFitQuality = $params['q']; + } + } + } + + return array((float) $bestFitQuality, $bestFitness); + } + + /** + * Find the best match for a given mime-type against a list of + * media-ranges that have already been parsed by + * Mimeparse::parseAndNormalizeMediaRange() + * + * Returns the "q" quality parameter of the best match, 0 if no match was + * found. This function behaves the same as Mimeparse::quality() except + * that $parsedRanges must be an array of parsed media-ranges. + * + * @param string $mimeType + * @param array $parsedRanges + * @return float $q + */ + protected static function qualityParsed($mimeType, $parsedRanges) + { + list($q, $fitness) = self::qualityAndFitnessParsed($mimeType, $parsedRanges); + return $q; + } + + /** + * Returns the quality "q" of a mime-type when compared against the + * media-ranges in ranges. For example: + * + * Mimeparse::quality("text/html", "text/*;q=0.3, text/html;q=0.7, + * text/html;level=1, text/html;level=2;q=0.4, *\/*;q=0.5") + * => 0.7 + * + * @param string $mimeType + * @param string $ranges + * @return float + */ + public static function quality($mimeType, $ranges) + { + $parsedRanges = explode(',', $ranges); + + foreach ($parsedRanges as $i => $r) { + $parsedRanges[$i] = self::parseAndNormalizeMediaRange($r); + } + + return self::qualityParsed($mimeType, $parsedRanges); + } + + /** + * Takes a list of supported mime-types and finds the best match for all + * the media-ranges listed in header. The value of $header must be a + * string that conforms to the format of the HTTP Accept: header. The + * value of $supported is an array of mime-types. + * + * In case of ties the mime-type with the lowest index in $supported will + * be used. + * + * Mimeparse::bestMatch(array("application/xbel+xml", "text/xml"), "text/*;q=0.5,*\/*; q=0.1") + * => "text/xml" + * + * @param array $supported + * @param string $header + * @return mixed $mimeType or NULL + */ + public static function bestMatch($supported, $header) + { + $parsedHeader = explode(',', $header); + + foreach ($parsedHeader as $i => $r) { + $parsedHeader[$i] = self::parseAndNormalizeMediaRange($r); + } + + $weightedMatches = array(); + foreach ($supported as $index => $mimeType) { + list($quality, $fitness) = self::qualityAndFitnessParsed($mimeType, $parsedHeader); + if (!empty($quality)) { + // Mime-types closer to the beginning of the array are + // preferred. This preference score is used to break ties. + $preference = 0 - $index; + $weightedMatches[] = array( + array($quality, $fitness, $preference), + $mimeType + ); + } + } + + // Note that since fitness and preference are present in + // $weightedMatches they will also be used when sorting (after quality + // level). + array_multisort($weightedMatches); + $firstChoice = array_pop($weightedMatches); + + return (empty($firstChoice[0][0]) ? null : $firstChoice[1]); + } + + /** + * Disable access to constructor + * + * @codeCoverageIgnore + */ + private function __construct() + { + } +} + diff --git a/core/vendor/bitworking/mimeparse/tests/Bitworking/MimeparseTest.php b/core/vendor/bitworking/mimeparse/tests/Bitworking/MimeparseTest.php new file mode 100644 index 0000000..5a47427 --- /dev/null +++ b/core/vendor/bitworking/mimeparse/tests/Bitworking/MimeparseTest.php @@ -0,0 +1,210 @@ + '1'), + 'xml' + ); + + $this->assertEquals($expected, Mimeparse::parseMediaRange('application/xml; q=1')); + } + + public function testParseMediaRangeWithGenericSubtype() + { + $expected = array( + 'application', + 'xhtml+xml', + array('q' => '1'), + 'xml' + ); + + $this->assertEquals($expected, Mimeparse::parseMediaRange('application/xhtml+xml; q=1')); + } + + public function testParseMediaRangeWithSingleWildCard() + { + $expected = array( + '*', + '*', + array(), + '*' + ); + + $this->assertEquals($expected, Mimeparse::parseMediaRange('*')); + } + + /** + * @expectedException UnexpectedValueException + * @expectedExceptionMessage Malformed media-range: application/;q=1 + */ + public function testParseMediaRangeWithMalformedMediaRange() + { + $parsed = Mimeparse::parseMediaRange('application/;q=1'); + } + + /** + * Testing this protected method because it includes a lot of parsing + * functionality that we wish to isolate from other tests. + * + * @covers Bitworking\Mimeparse::parseAndNormalizeMediaRange + */ + public function testParseAndNormalizeMediaRange() + { + $method = new \ReflectionMethod('Bitworking\Mimeparse', 'parseAndNormalizeMediaRange'); + $method->setAccessible(true); + + $expected1 = array( + 0 => 'application', + 1 => 'xml', + 2 => array('q' => '1'), + 3 => 'xml', + ); + + $this->assertEquals($expected1, $method->invoke(null, 'application/xml;q=1')); + $this->assertEquals($expected1, $method->invoke(null, 'application/xml')); + $this->assertEquals($expected1, $method->invoke(null, 'application/xml;q=')); + + $expected2 = array( + 0 => 'application', + 1 => 'xml', + 2 => array('q' => '1', 'b' => 'other'), + 3 => 'xml', + ); + + $this->assertEquals($expected2, $method->invoke(null, 'application/xml ; q=1;b=other')); + $this->assertEquals($expected2, $method->invoke(null, 'application/xml ; q=2;b=other')); + + // Java URLConnection class sends an Accept header that includes a single "*" + $this->assertEquals(array( + 0 => '*', + 1 => '*', + 2 => array('q' => '.2'), + 3 => '*', + ), $method->invoke(null, ' *; q=.2')); + } + + /** + * @covers Bitworking\Mimeparse::quality + * @covers Bitworking\Mimeparse::qualityParsed + * @covers Bitworking\Mimeparse::qualityAndFitnessParsed + */ + public function testQuality() + { + $accept = 'text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5'; + + $this->assertEquals(1, Mimeparse::quality('text/html;level=1', $accept)); + $this->assertEquals(0.7, Mimeparse::quality('text/html', $accept)); + $this->assertEquals(0.3, Mimeparse::quality('text/plain', $accept)); + $this->assertEquals(0.5, Mimeparse::quality('image/jpeg', $accept)); + $this->assertEquals(0.4, Mimeparse::quality('text/html;level=2', $accept)); + $this->assertEquals(0.7, Mimeparse::quality('text/html;level=3', $accept)); + } + + /** + * @covers Bitworking\Mimeparse::bestMatch + */ + public function testBestMatch() + { + $supportedMimeTypes1 = array('application/xbel+xml', 'application/xml'); + + // direct match + $this->assertEquals('application/xbel+xml', Mimeparse::bestMatch($supportedMimeTypes1, 'application/xbel+xml')); + + // direct match with a q parameter + $this->assertEquals('application/xbel+xml', Mimeparse::bestMatch($supportedMimeTypes1, 'application/xbel+xml; q=1')); + + // direct match of our second choice with a q parameter + $this->assertEquals('application/xml', Mimeparse::bestMatch($supportedMimeTypes1, 'application/xml; q=1')); + + // match using a subtype wildcard + $this->assertEquals('application/xbel+xml', Mimeparse::bestMatch($supportedMimeTypes1, 'application/*; q=1')); + + // match using a type wildcard + $this->assertEquals('application/xbel+xml', Mimeparse::bestMatch($supportedMimeTypes1, '* / *')); + + + $supportedMimeTypes2 = array('application/xbel+xml', 'text/xml'); + + // match using a type versus a lower weighted subtype + $this->assertEquals('text/xml', Mimeparse::bestMatch($supportedMimeTypes2, 'text/ *;q=0.5,* / *;q=0.1')); + + // fail to match anything + $this->assertEquals(null, Mimeparse::bestMatch($supportedMimeTypes2, 'text/html,application/atom+xml; q=0.9')); + + + $supportedMimeTypes3 = array('application/json', 'text/html'); + + // common Ajax scenario + $this->assertEquals('application/json', Mimeparse::bestMatch($supportedMimeTypes3, 'application/json, text/javascript, */*')); + + // verify fitness sorting + $this->assertEquals('application/json', Mimeparse::bestMatch($supportedMimeTypes3, 'application/json, text/html;q=0.9')); + + + $supportedMimeTypes4 = array('image/*', 'application/xml'); + + // match using a type wildcard + $this->assertEquals('image/*', Mimeparse::bestMatch($supportedMimeTypes4, 'image/png')); + + // match using a wildcard for both requested and supported + $this->assertEquals('image/*', Mimeparse::bestMatch($supportedMimeTypes4, 'image/*')); + } + + /** + * @covers Bitworking\Mimeparse::bestMatch + * @see http://shiflett.org/blog/2011/may/the-accept-header + * @see http://code.google.com/p/mimeparse/issues/detail?id=15 + */ + public function testZeroQuality() + { + $supportedMimeTypes = array('application/json'); + $httpAcceptHeader = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8,application/json;q=0.0'; + + $this->assertNull(Mimeparse::bestMatch($supportedMimeTypes, $httpAcceptHeader)); + $this->assertNull(Mimeparse::bestMatch($supportedMimeTypes, 'application/xml,*/*;q=0')); + } + + /** + * @covers Bitworking\Mimeparse::bestMatch + */ + public function testStarSlashStarWithItemOfZeroQuality() + { + $supportedMimeTypes = array('text/plain', 'application/json'); + $httpAcceptHeader = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8,application/json;q=0.0'; + + $this->assertEquals('text/plain', Mimeparse::bestMatch($supportedMimeTypes, $httpAcceptHeader)); + } + + /** + * @covers Bitworking\Mimeparse::bestMatch + * @see http://code.google.com/p/mimeparse/issues/detail?id=10 + */ + public function testStarSlashStarWithHigherQualityThanMoreSpecificType() + { + $supportedMimeTypes = array('image/jpeg', 'text/plain'); + $httpAcceptHeader = 'text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5'; + + $this->assertEquals('image/jpeg', Mimeparse::bestMatch($supportedMimeTypes, $httpAcceptHeader)); + } + + /** + * @covers Bitworking\Mimeparse::bestMatch + */ + public function testBestMatchWithTies() + { + $supportedMimeTypes1 = array('text/html', 'application/json', 'application/hal+xml', 'application/hal+json'); + $supportedMimeTypes2 = array('text/html', 'application/hal+json', 'application/json', 'application/hal+xml'); + $httpAcceptHeader = 'application/*, text/*;q=0.8'; + + $this->assertEquals('application/json', Mimeparse::bestMatch($supportedMimeTypes1, $httpAcceptHeader)); + $this->assertEquals('application/hal+json', Mimeparse::bestMatch($supportedMimeTypes2, $httpAcceptHeader)); + } +} diff --git a/core/vendor/bitworking/mimeparse/tests/bootstrap.php b/core/vendor/bitworking/mimeparse/tests/bootstrap.php new file mode 100644 index 0000000..19834b8 --- /dev/null +++ b/core/vendor/bitworking/mimeparse/tests/bootstrap.php @@ -0,0 +1,13 @@ + $vendorDir . '/symfony/class-loader/', 'SessionHandlerInterface' => $vendorDir . '/symfony/http-foundation/Symfony/Component/HttpFoundation/Resources/stubs', 'Doctrine\\Common' => $vendorDir . '/doctrine/common/lib/', + 'Bitworking' => $vendorDir . '/bitworking/mimeparse/src/', 'Assetic' => $vendorDir . '/kriswallsmith/assetic/src/', );