'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',
--- /dev/null
+<?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;
+ }
+
+}
--- /dev/null
+<?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;
+ }
+
+}
--- /dev/null
+<?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 );
+ }
+
+}
--- /dev/null
+<?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
+ }
+
+}