From: Amir Sarabadani Date: Mon, 29 May 2017 23:41:15 +0000 (+0200) Subject: Move HttpAcceptNegotiator and HttpAcceptParser from Wikibase to core X-Git-Tag: 1.31.0-rc.0~3013^2 X-Git-Url: https://git.heureux-cyclage.org/?p=lhc%2Fweb%2Fwiklou.git;a=commitdiff_plain;h=4ec5164b4edc2ff64efd2f26f78d81c85c2c654e Move HttpAcceptNegotiator and HttpAcceptParser from Wikibase to core This will be needed for implementing Special:PageData Bug: T163923 Change-Id: I2315d7dcdfa5973998917af311ebecc855b37f73 --- diff --git a/autoload.php b/autoload.php index 6d2b66b004..3d97a749cc 100644 --- a/autoload.php +++ b/autoload.php @@ -872,6 +872,8 @@ $wgAutoloadLocalClasses = [ 'MediaWiki\\Diff\\ComplexityException' => __DIR__ . '/includes/diff/ComplexityException.php', 'MediaWiki\\Diff\\WordAccumulator' => __DIR__ . '/includes/diff/WordAccumulator.php', 'MediaWiki\\HeaderCallback' => __DIR__ . '/includes/HeaderCallback.php', + 'MediaWiki\\Http\\HttpAcceptNegotiator' => __DIR__ . '/includes/http/HttpAcceptNegotiator.php', + 'MediaWiki\\Http\\HttpAcceptParser' => __DIR__ . '/includes/http/HttpAcceptParser.php', 'MediaWiki\\Interwiki\\ClassicInterwikiLookup' => __DIR__ . '/includes/interwiki/ClassicInterwikiLookup.php', 'MediaWiki\\Interwiki\\InterwikiLookup' => __DIR__ . '/includes/interwiki/InterwikiLookup.php', 'MediaWiki\\Interwiki\\InterwikiLookupAdapter' => __DIR__ . '/includes/interwiki/InterwikiLookupAdapter.php', diff --git a/includes/http/HttpAcceptNegotiator.php b/includes/http/HttpAcceptNegotiator.php new file mode 100644 index 0000000000..399aa34a86 --- /dev/null +++ b/includes/http/HttpAcceptNegotiator.php @@ -0,0 +1,139 @@ +supportedValues = $supported; + $this->defaultValue = reset( $supported ); + } + + /** + * Returns the best supported key from the given weight map. Of the keys from the + * $weights parameter that are also in the list of supported values supplied to + * the constructor, this returns the key that has the highest weight associated + * with it. If two keys have the same weight, the more specific key is preferred, + * as required by RFC2616 section 14. Keys that map to 0 or false are ignored. + * If no matching key is found, $default is returned. + * + * @param float[] $weights An associative array mapping accepted values to their + * respective weights. + * + * @param null|string $default The value to return if non of the keys in $weights + * is supported (null per default). + * + * @return null|string The best supported key from the $weights parameter. + */ + public function getBestSupportedKey( array $weights, $default = null ) { + // Make sure we correctly bias against wildcards and ranges, see RFC2616, section 14. + foreach ( $weights as $name => &$weight ) { + if ( $name === '*' || $name === '*/*' ) { + $weight -= 0.000002; + } elseif ( substr( $name, -2 ) === '/*' ) { + $weight -= 0.000001; + } + } + + // Sort $weights by value and... + asort( $weights ); + + // remove any keys with values equal to 0 or false (HTTP/1.1 section 3.9) + $weights = array_filter( $weights ); + + // ...use the ordered list of keys + $preferences = array_reverse( array_keys( $weights ) ); + + $value = $this->getFirstSupportedValue( $preferences, $default ); + return $value; + } + + /** + * Returns the first supported value from the given preference list. Of the values from + * the $preferences parameter that are also in the list of supported values supplied + * to the constructor, this returns the value that has the lowest index in the list. + * If no such value is found, $default is returned. + * + * @param string[] $preferences A list of acceptable values, in order of preference. + * + * @param null|string $default The value to return if non of the keys in $weights + * is supported (null per default). + * + * @return null|string The best supported key from the $weights parameter. + */ + public function getFirstSupportedValue( array $preferences, $default = null ) { + foreach ( $preferences as $value ) { + foreach ( $this->supportedValues as $supported ) { + if ( $this->valueMatches( $value, $supported ) ) { + return $supported; + } + } + } + + return $default; + } + + /** + * Returns true if the given acceptable value matches the given supported value, + * according to the HTTP specification. The following rules are used: + * + * - comparison is case-insensitive + * - if $accepted and $supported are equal, they match + * - if $accepted is `*` or `*` followed by `/*`, it matches any $supported value. + * - if both $accepted and $supported contain a `/`, and $accepted ends with `/*`, + * they match if the part before the first `/` is equal. + * + * @param string $accepted An accepted value (may contain wildcards) + * @param string $supported A supported value. + * + * @return bool Whether the given supported value matches the given accepted value. + */ + private function valueMatches( $accepted, $supported ) { + // RDF 2045: MIME types are case insensitive. + // full match + if ( strcasecmp( $accepted, $supported ) === 0 ) { + return true; + } + + // wildcard match (HTTP/1.1 section 14.1, 14.2, 14.3) + if ( $accepted === '*' || $accepted === '*/*' ) { + return true; + } + + // wildcard match (HTTP/1.1 section 14.1) + if ( substr( $accepted, -2 ) === '/*' + && strncasecmp( $accepted, $supported, strlen( $accepted ) - 2 ) === 0 + ) { + return true; + } + + return false; + } + +} diff --git a/includes/http/HttpAcceptParser.php b/includes/http/HttpAcceptParser.php new file mode 100644 index 0000000000..304c9831e6 --- /dev/null +++ b/includes/http/HttpAcceptParser.php @@ -0,0 +1,78 @@ + 0.8 + $weights = array_combine( $values, $qvalues ); + + return $weights; + } + +} diff --git a/tests/phpunit/includes/http/HttpAcceptNegotiatorTest.php b/tests/phpunit/includes/http/HttpAcceptNegotiatorTest.php new file mode 100644 index 0000000000..c194803f36 --- /dev/null +++ b/tests/phpunit/includes/http/HttpAcceptNegotiatorTest.php @@ -0,0 +1,151 @@ +getFirstSupportedValue( $accepted, $default ); + + $this->assertEquals( $expected, $actual ); + } + + public function provideGetBestSupportedKey() { + return [ + [ // #0: empty + [], // supported + [], // accepted + null, // default + null, // expected + ], + [ // #1: simple + [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported + [ 'text/xzy' => 1, 'text/bar' => 0.5 ], // accepted + null, // default + 'text/BAR', // expected + ], + [ // #2: default + [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported + [ 'text/xzy' => 1, 'text/xoo' => 0.5 ], // accepted + 'X', // default + 'X', // expected + ], + [ // #3: weighted + [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported + [ 'text/foo' => 0.3, 'text/BAR' => 0.8, 'application/zuul' => 0.5 ], // accepted + null, // default + 'text/BAR', // expected + ], + [ // #4: zero weight + [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported + [ 'text/foo' => 0, 'text/xoo' => 1 ], // accepted + null, // default + null, // expected + ], + [ // #5: * wildcard + [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported + [ 'text/xoo' => 0.5, '*' => 0.1 ], // accepted + null, // default + 'text/foo', // expected + ], + [ // #6: */* wildcard + [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported + [ 'text/xoo' => 0.5, '*/*' => 0.1 ], // accepted + null, // default + 'text/foo', // expected + ], + [ // #7: text/* wildcard + [ 'text/foo', 'text/BAR', 'application/zuul' ], // supported + [ 'text/foo' => 0.3, 'application/*' => 0.8 ], // accepted + null, // default + 'application/zuul', // expected + ], + [ // #8: Test specific format preferred over wildcard (T133314) + [ 'application/rdf+xml', 'text/json', 'text/html' ], // supported + [ '*/*' => 1, 'text/html' => 1 ], // accepted + null, // default + 'text/html', // expected + ], + [ // #9: Test specific format preferred over range (T133314) + [ 'application/rdf+xml', 'text/json', 'text/html' ], // supported + [ 'text/*' => 1, 'text/html' => 1 ], // accepted + null, // default + 'text/html', // expected + ], + [ // #10: Test range preferred over wildcard (T133314) + [ 'application/rdf+xml', 'text/html' ], // supported + [ '*/*' => 1, 'text/*' => 1 ], // accepted + null, // default + 'text/html', // expected + ], + ]; + } + + /** + * @dataProvider provideGetBestSupportedKey + */ + public function testGetBestSupportedKey( $supported, $accepted, $default, $expected ) { + $negotiator = new HttpAcceptNegotiator( $supported ); + $actual = $negotiator->getBestSupportedKey( $accepted, $default ); + + $this->assertEquals( $expected, $actual ); + } + +} diff --git a/tests/phpunit/includes/http/HttpAcceptParserTest.php b/tests/phpunit/includes/http/HttpAcceptParserTest.php new file mode 100644 index 0000000000..7e5fc74692 --- /dev/null +++ b/tests/phpunit/includes/http/HttpAcceptParserTest.php @@ -0,0 +1,57 @@ + 1 ] + ], + [ // #2 + 'Accept: text/plain', + [ 'text/plain' => 1 ] + ], + [ // #3 + 'Accept: application/vnd.php.serialized, application/rdf+xml', + [ '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 ] + ], + [ // #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 ] + //], + ]; + } + + /** + * @dataProvider provideParseWeights + */ + public function testParseWeights( $header, $expected ) { + $parser = new HttpAcceptParser(); + $actual = $parser->parseWeights( $header ); + + $this->assertEquals( $expected, $actual ); // shouldn't be sensitive to order + } + +}