Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
HttpAcceptNegotiator
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
4 / 4
16
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getBestSupportedKey
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 getFirstSupportedValue
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 valueMatches
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2
3namespace 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-or-later
14 * @author Daniel Kinzler
15 * @author Thiemo Kreuz
16 */
17class 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 none of the keys in $weights
49     *              is supported (null by 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 by 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}