Move HttpAcceptNegotiator and HttpAcceptParser from Wikibase to core
This will be needed for implementing Special:PageData Bug: T163923 Change-Id: I2315d7dcdfa5973998917af311ebecc855b37f73
This commit is contained in:
parent
e8f69a7035
commit
4ec5164b4e
5 changed files with 427 additions and 0 deletions
|
|
@ -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',
|
||||
|
|
|
|||
139
includes/http/HttpAcceptNegotiator.php
Normal file
139
includes/http/HttpAcceptNegotiator.php
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Utility for negotiating a value from a set of supported values using a preference list.
|
||||
* This is intended for use with HTTP headers like Accept, Accept-Language, Accept-Encoding, etc.
|
||||
* See RFC 2616 section 14 for details.
|
||||
*
|
||||
* To use this with a request header, first parse the header value into an array of weights
|
||||
* using HttpAcceptParser, then call getBestSupportedKey.
|
||||
*
|
||||
* @license GPL-2.0+
|
||||
* @author Daniel Kinzler
|
||||
* @author Thiemo Mättig
|
||||
*/
|
||||
|
||||
namespace MediaWiki\Http;
|
||||
|
||||
class HttpAcceptNegotiator {
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $supportedValues;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $defaultValue;
|
||||
|
||||
/**
|
||||
* @param string[] $supported A list of supported values.
|
||||
*/
|
||||
public function __construct( array $supported ) {
|
||||
$this->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;
|
||||
}
|
||||
|
||||
}
|
||||
78
includes/http/HttpAcceptParser.php
Normal file
78
includes/http/HttpAcceptParser.php
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Utility for parsing a HTTP Accept header value into a weight map. May also be used with
|
||||
* other, similar headers like Accept-Language, Accept-Encoding, etc.
|
||||
*
|
||||
* @license GPL-2.0+
|
||||
* @author Daniel Kinzler
|
||||
*/
|
||||
|
||||
namespace MediaWiki\Http;
|
||||
|
||||
class HttpAcceptParser {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @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 );
|
||||
|
||||
// Create a list like "en" => 0.8
|
||||
$weights = array_combine( $values, $qvalues );
|
||||
|
||||
return $weights;
|
||||
}
|
||||
|
||||
}
|
||||
151
tests/phpunit/includes/http/HttpAcceptNegotiatorTest.php
Normal file
151
tests/phpunit/includes/http/HttpAcceptNegotiatorTest.php
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
<?php
|
||||
|
||||
use MediaWiki\Http\HttpAcceptNegotiator;
|
||||
|
||||
/**
|
||||
* @covers MediaWiki\Http\HttpAcceptNegotiator
|
||||
*
|
||||
* @license GPL-2.0+
|
||||
* @author Daniel Kinzler
|
||||
*/
|
||||
class HttpAcceptNegotiatorTest extends \PHPUnit_Framework_TestCase {
|
||||
|
||||
public function provideGetFirstSupportedValue() {
|
||||
return [
|
||||
[ // #0: empty
|
||||
[], // supported
|
||||
[], // accepted
|
||||
null, // default
|
||||
null, // expected
|
||||
],
|
||||
[ // #1: simple
|
||||
[ 'text/foo', 'text/BAR', 'application/zuul' ], // supported
|
||||
[ 'text/xzy', 'text/bar' ], // accepted
|
||||
null, // default
|
||||
'text/BAR', // expected
|
||||
],
|
||||
[ // #2: default
|
||||
[ 'text/foo', 'text/BAR', 'application/zuul' ], // supported
|
||||
[ 'text/xzy', 'text/xoo' ], // accepted
|
||||
'X', // default
|
||||
'X', // expected
|
||||
],
|
||||
[ // #3: preference
|
||||
[ 'text/foo', 'text/bar', 'application/zuul' ], // supported
|
||||
[ 'text/xoo', 'text/BAR', 'text/foo' ], // accepted
|
||||
null, // default
|
||||
'text/bar', // expected
|
||||
],
|
||||
[ // #4: * wildcard
|
||||
[ 'text/foo', 'text/BAR', 'application/zuul' ], // supported
|
||||
[ 'text/xoo', '*' ], // accepted
|
||||
null, // default
|
||||
'text/foo', // expected
|
||||
],
|
||||
[ // #5: */* wildcard
|
||||
[ 'text/foo', 'text/BAR', 'application/zuul' ], // supported
|
||||
[ 'text/xoo', '*/*' ], // accepted
|
||||
null, // default
|
||||
'text/foo', // expected
|
||||
],
|
||||
[ // #6: text/* wildcard
|
||||
[ 'text/foo', 'text/BAR', 'application/zuul' ], // supported
|
||||
[ 'application/*', 'text/foo' ], // accepted
|
||||
null, // default
|
||||
'application/zuul', // expected
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideGetFirstSupportedValue
|
||||
*/
|
||||
public function testGetFirstSupportedValue( $supported, $accepted, $default, $expected ) {
|
||||
$negotiator = new HttpAcceptNegotiator( $supported );
|
||||
$actual = $negotiator->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 );
|
||||
}
|
||||
|
||||
}
|
||||
57
tests/phpunit/includes/http/HttpAcceptParserTest.php
Normal file
57
tests/phpunit/includes/http/HttpAcceptParserTest.php
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
use MediaWiki\Http\HttpAcceptParser;
|
||||
|
||||
/**
|
||||
* @covers MediaWiki\Http\HttpAcceptParser
|
||||
*
|
||||
* @license GPL-2.0+
|
||||
* @author Daniel Kinzler
|
||||
*/
|
||||
class HttpAcceptParserTest extends \PHPUnit_Framework_TestCase {
|
||||
|
||||
public function provideParseWeights() {
|
||||
return [
|
||||
[ // #0
|
||||
'',
|
||||
[]
|
||||
],
|
||||
[ // #1
|
||||
'Foo/Bar',
|
||||
[ 'foo/bar' => 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
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in a new issue