summaryrefslogtreecommitdiff
path: root/www/wiki/includes/libs/http/HttpAcceptNegotiator.php
blob: 4de8e77e394f43a8b64394dfa31e1f4d0ae38f6c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
<?php

namespace Wikimedia\Http;

/**
 * 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-or-later
 * @author Daniel Kinzler
 * @author Thiemo Kreuz
 */
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;
	}

}