aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--includes/libs/http/HttpAcceptParser.php110
-rw-r--r--tests/phpunit/includes/libs/http/HttpAcceptParserTest.php115
2 files changed, 176 insertions, 49 deletions
diff --git a/includes/libs/http/HttpAcceptParser.php b/includes/libs/http/HttpAcceptParser.php
index 93f5b0bce64d..25811ab7431f 100644
--- a/includes/libs/http/HttpAcceptParser.php
+++ b/includes/libs/http/HttpAcceptParser.php
@@ -13,66 +13,98 @@ namespace Wikimedia\Http;
class HttpAcceptParser {
/**
+ * Parse media types from an Accept header and sort them by q-factor.
+ *
+ * Note that his was mostly ported from,
+ * https://github.com/arlolra/negotiator/blob/full-parse-access/lib/mediaType.js
+ *
+ * @param string $accept
+ * @return array[]
+ * - type: (string)
+ * - subtype: (string)
+ * - q: (float) q-factor weighting
+ * - i: (int) index
+ * - params: (array)
+ */
+ public function parseAccept( $accept ): array {
+ $accepts = explode( ',', $accept ); // FIXME: Allow commas in quotes
+ $ret = [];
+
+ foreach ( $accepts as $i => $a ) {
+ preg_match( '!^([^\s/;]+)/([^;\s]+)\s*(?:;(.*))?$!D', trim( $a ), $matches );
+ if ( !$matches ) {
+ continue;
+ }
+ $q = 1;
+ $params = [];
+ if ( isset( $matches[3] ) ) {
+ $kvps = explode( ';', $matches[3] ); // FIXME: Allow semi-colon in quotes
+ foreach ( $kvps as $kv ) {
+ [ $key, $val ] = explode( '=', trim( $kv ), 2 );
+ $key = strtolower( trim( $key ) );
+ $val = trim( $val );
+ if ( $key === 'q' ) {
+ $q = (float)$val; // FIXME: Spec is stricter about this
+ } else {
+ if ( $val && $val[0] === '"' && $val[ strlen( $val ) - 1 ] === '"' ) {
+ $val = substr( $val, 1, strlen( $val ) - 2 );
+ }
+ $params[$key] = $val;
+ }
+ }
+ }
+ $ret[] = [
+ 'type' => $matches[1],
+ 'subtype' => $matches[2],
+ 'q' => $q,
+ 'i' => $i,
+ 'params' => $params,
+ ];
+ }
+
+ // Sort list. First by q values, then by order
+ usort( $ret, function ( $a, $b ) {
+ if ( $b['q'] > $a['q'] ) {
+ return 1;
+ } elseif ( $b['q'] === $a['q'] ) {
+ return $a['i'] - $b['i'];
+ } else {
+ return -1;
+ }
+ } );
+
+ return $ret;
+ }
+
+ /**
* Parses an HTTP header into a weight map, that is an associative array
* mapping values to their respective weights. Any header name preceding
* weight spec is ignored for convenience.
*
- * This implementation is partially based on the code at
- * http://www.thefutureoftheweb.com/blog/use-accept-language-header
- *
* Note that type parameters and accept extension like the "level" parameter
* are not supported, weights are derived from "q" values only.
*
- * @todo If additional type parameters are present, ignore them cleanly.
- * At present, they often confuse the result.
- *
- * See HTTP/1.1 section 14 for details.
+ * See RFC 7231 section 5.3.2 for details.
*
* @param string $rawHeader
*
* @return array
*/
public function parseWeights( $rawHeader ) {
- //FIXME: The code below was copied and adapted from WebRequest::getAcceptLang.
- // Move this utility class into core for reuse!
-
// first, strip header name
$rawHeader = preg_replace( '/^[-\w]+:\s*/', '', $rawHeader );
// Return values in lower case
$rawHeader = strtolower( $rawHeader );
- // Break up string into pieces (values and q factors)
- $value_parse = null;
- preg_match_all( '@([a-z\d*]+([-+/.][a-z\d*]+)*)\s*(;\s*q\s*=\s*(1(\.0{0,3})?|0(\.\d{0,3})?)?)?@',
- $rawHeader, $value_parse );
-
- if ( !count( $value_parse[1] ) ) {
- return [];
- }
-
- $values = $value_parse[1];
- $qvalues = $value_parse[4];
- $indices = range( 0, count( $value_parse[1] ) - 1 );
-
- // Set default q factor to 1
- foreach ( $indices as $index ) {
- if ( $qvalues[$index] === '' ) {
- $qvalues[$index] = 1;
- } elseif ( $qvalues[$index] == 0 ) {
- unset( $values[$index], $qvalues[$index], $indices[$index] );
- } else {
- $qvalues[$index] = (float)$qvalues[$index];
- }
- }
-
- // Sort list. First by $qvalues, then by order. Reorder $values the same way
- array_multisort( $qvalues, SORT_DESC, SORT_NUMERIC, $indices, $values );
+ $accepts = $this->parseAccept( $rawHeader );
// Create a list like "en" => 0.8
- $weights = array_combine( $values, $qvalues );
-
- return $weights;
+ return array_reduce( $accepts, function ( $prev, $next ) {
+ $type = "{$next['type']}/{$next['subtype']}";
+ $prev[$type] = $next['q'];
+ return $prev;
+ }, [] );
}
}
diff --git a/tests/phpunit/includes/libs/http/HttpAcceptParserTest.php b/tests/phpunit/includes/libs/http/HttpAcceptParserTest.php
index e4b47b46d57f..60e3addf2162 100644
--- a/tests/phpunit/includes/libs/http/HttpAcceptParserTest.php
+++ b/tests/phpunit/includes/libs/http/HttpAcceptParserTest.php
@@ -28,18 +28,17 @@ class HttpAcceptParserTest extends \PHPUnit\Framework\TestCase {
[ 'application/vnd.php.serialized' => 1, 'application/rdf+xml' => 1 ]
],
[ // #4
- 'foo; q=0.2, xoo; q=0,text/n3',
- [ 'text/n3' => 1, 'foo' => 0.2 ]
+ 'foo/*; q=0.2, xoo; q=0,text/n3',
+ [ 'text/n3' => 1, 'foo/*' => 0.2 ]
],
[ // #5
- '*; q=0.2, */*; q=0.1,text/*',
- [ 'text/*' => 1, '*' => 0.2, '*/*' => 0.1 ]
- ],
- // TODO: nicely ignore additional type paramerters
- //[ // #6
- // 'Foo; q=0.2, Xoo; level=3, Bar; charset=xyz; q=0.4',
- // [ 'xoo' => 1, 'bar' => 0.4, 'foo' => 0.1 ]
- //],
+ 'foo/*; q=0.2, */*; q=0.1,text/*',
+ [ 'text/*' => 1, 'foo/*' => 0.2, '*/*' => 0.1 ]
+ ],
+ [ // #6
+ 'Foo/*; q=0.2, Xoo/*; level=3, Bar/*; charset=xyz; q=0.4',
+ [ 'xoo/*' => 1, 'bar/*' => 0.4, 'foo/*' => 0.2 ]
+ ],
];
}
@@ -53,4 +52,100 @@ class HttpAcceptParserTest extends \PHPUnit\Framework\TestCase {
$this->assertEquals( $expected, $actual ); // shouldn't be sensitive to order
}
+ public function provideParseAccept() {
+ return [
+ [
+ // Sort by decending q
+ 'test/123; q=0.5, test/456; q=0.8',
+ [
+ [
+ 'type' => 'test',
+ 'subtype' => '456',
+ 'q' => 0.8,
+ 'i' => 1,
+ 'params' => []
+ ],
+ [
+ 'type' => 'test',
+ 'subtype' => '123',
+ 'q' => 0.5,
+ 'i' => 0,
+ 'params' => []
+ ],
+ ]
+ ],
+ [
+ // Sort by decending q, ascending order
+ 'test/123; q=0.5, test/789; q=0.8, test/456; q=0.8',
+ [
+ [
+ 'type' => 'test',
+ 'subtype' => '789',
+ 'q' => 0.8,
+ 'i' => 1,
+ 'params' => []
+ ],
+ [
+ 'type' => 'test',
+ 'subtype' => '456',
+ 'q' => 0.8,
+ 'i' => 2,
+ 'params' => []
+ ],
+ [
+ 'type' => 'test',
+ 'subtype' => '123',
+ 'q' => 0.5,
+ 'i' => 0,
+ 'params' => []
+ ]
+ ]
+ ],
+ [
+ // Test types and subtypes that contain non-alphanumeric characters
+ 'hi-ho/12.3; q=0.5, hi/ho+456; q=0.8',
+ [
+ [
+ 'type' => 'hi',
+ 'subtype' => 'ho+456',
+ 'q' => 0.8,
+ 'i' => 1,
+ 'params' => []
+ ],
+ [
+ 'type' => 'hi-ho',
+ 'subtype' => '12.3',
+ 'q' => 0.5,
+ 'i' => 0,
+ 'params' => []
+ ]
+ ]
+ ],
+ [
+ // Test for params
+ 'text/html; profile="https://www.mediawiki.org/wiki/Specs/HTML/0.0.0"',
+ [
+ [
+ 'type' => 'text',
+ 'subtype' => 'html',
+ 'q' => 1,
+ 'i' => 0,
+ 'params' => [
+ 'profile' => 'https://www.mediawiki.org/wiki/Specs/HTML/0.0.0'
+ ]
+ ]
+ ]
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideParseAccept
+ */
+ public function testParseAccept( $header, $expected ) {
+ $parser = new HttpAcceptParser();
+ $actual = $parser->parseAccept( $header );
+ $this->assertEquals( $expected, $actual );
+ }
+
}