Merge "Add string length limits"
[lhc/web/wiklou.git] / includes / libs / http / HttpAcceptNegotiator.php
1 <?php
2
3 namespace Wikimedia\Http;
4
5 /**
6 * Utility for negotiating a value from a set of supported values using a preference list.
7 * This is intended for use with HTTP headers like Accept, Accept-Language, Accept-Encoding, etc.
8 * See RFC 2616 section 14 for details.
9 *
10 * To use this with a request header, first parse the header value into an array of weights
11 * using HttpAcceptParser, then call getBestSupportedKey.
12 *
13 * @license GPL-2.0+
14 * @author Daniel Kinzler
15 * @author Thiemo Kreuz
16 */
17 class HttpAcceptNegotiator {
18
19 /**
20 * @var string[]
21 */
22 private $supportedValues;
23
24 /**
25 * @var string
26 */
27 private $defaultValue;
28
29 /**
30 * @param string[] $supported A list of supported values.
31 */
32 public function __construct( array $supported ) {
33 $this->supportedValues = $supported;
34 $this->defaultValue = reset( $supported );
35 }
36
37 /**
38 * Returns the best supported key from the given weight map. Of the keys from the
39 * $weights parameter that are also in the list of supported values supplied to
40 * the constructor, this returns the key that has the highest weight associated
41 * with it. If two keys have the same weight, the more specific key is preferred,
42 * as required by RFC2616 section 14. Keys that map to 0 or false are ignored.
43 * If no matching key is found, $default is returned.
44 *
45 * @param float[] $weights An associative array mapping accepted values to their
46 * respective weights.
47 *
48 * @param null|string $default The value to return if non of the keys in $weights
49 * is supported (null per default).
50 *
51 * @return null|string The best supported key from the $weights parameter.
52 */
53 public function getBestSupportedKey( array $weights, $default = null ) {
54 // Make sure we correctly bias against wildcards and ranges, see RFC2616, section 14.
55 foreach ( $weights as $name => &$weight ) {
56 if ( $name === '*' || $name === '*/*' ) {
57 $weight -= 0.000002;
58 } elseif ( substr( $name, -2 ) === '/*' ) {
59 $weight -= 0.000001;
60 }
61 }
62
63 // Sort $weights by value and...
64 asort( $weights );
65
66 // remove any keys with values equal to 0 or false (HTTP/1.1 section 3.9)
67 $weights = array_filter( $weights );
68
69 // ...use the ordered list of keys
70 $preferences = array_reverse( array_keys( $weights ) );
71
72 $value = $this->getFirstSupportedValue( $preferences, $default );
73 return $value;
74 }
75
76 /**
77 * Returns the first supported value from the given preference list. Of the values from
78 * the $preferences parameter that are also in the list of supported values supplied
79 * to the constructor, this returns the value that has the lowest index in the list.
80 * If no such value is found, $default is returned.
81 *
82 * @param string[] $preferences A list of acceptable values, in order of preference.
83 *
84 * @param null|string $default The value to return if non of the keys in $weights
85 * is supported (null per default).
86 *
87 * @return null|string The best supported key from the $weights parameter.
88 */
89 public function getFirstSupportedValue( array $preferences, $default = null ) {
90 foreach ( $preferences as $value ) {
91 foreach ( $this->supportedValues as $supported ) {
92 if ( $this->valueMatches( $value, $supported ) ) {
93 return $supported;
94 }
95 }
96 }
97
98 return $default;
99 }
100
101 /**
102 * Returns true if the given acceptable value matches the given supported value,
103 * according to the HTTP specification. The following rules are used:
104 *
105 * - comparison is case-insensitive
106 * - if $accepted and $supported are equal, they match
107 * - if $accepted is `*` or `*` followed by `/*`, it matches any $supported value.
108 * - if both $accepted and $supported contain a `/`, and $accepted ends with `/*`,
109 * they match if the part before the first `/` is equal.
110 *
111 * @param string $accepted An accepted value (may contain wildcards)
112 * @param string $supported A supported value.
113 *
114 * @return bool Whether the given supported value matches the given accepted value.
115 */
116 private function valueMatches( $accepted, $supported ) {
117 // RDF 2045: MIME types are case insensitive.
118 // full match
119 if ( strcasecmp( $accepted, $supported ) === 0 ) {
120 return true;
121 }
122
123 // wildcard match (HTTP/1.1 section 14.1, 14.2, 14.3)
124 if ( $accepted === '*' || $accepted === '*/*' ) {
125 return true;
126 }
127
128 // wildcard match (HTTP/1.1 section 14.1)
129 if ( substr( $accepted, -2 ) === '/*'
130 && strncasecmp( $accepted, $supported, strlen( $accepted ) - 2 ) === 0
131 ) {
132 return true;
133 }
134
135 return false;
136 }
137
138 }