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:
Amir Sarabadani 2017-05-30 01:41:15 +02:00
parent e8f69a7035
commit 4ec5164b4e
5 changed files with 427 additions and 0 deletions

View file

@ -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',

View 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;
}
}

View 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;
}
}

View 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 );
}
}

View 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
}
}