Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
44 / 44
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
HttpAcceptParser
100.00% covered (success)
100.00%
44 / 44
100.00% covered (success)
100.00%
2 / 2
13
100.00% covered (success)
100.00%
1 / 1
 parseAccept
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
1 / 1
12
 parseWeights
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/**
4 * Utility for parsing a HTTP Accept header value into a weight map. May also be used with
5 * other, similar headers like Accept-Language, Accept-Encoding, etc.
6 *
7 * @license GPL-2.0-or-later
8 * @author Daniel Kinzler
9 */
10
11namespace Wikimedia\Http;
12
13class HttpAcceptParser {
14
15    /**
16     * Parse media types from an Accept header and sort them by q-factor.
17     *
18     * Note that this was mostly ported from
19     * https://github.com/arlolra/negotiator/blob/full-parse-access/lib/mediaType.js
20     *
21     * @param string $accept
22     * @return array[]
23     *  - type: (string)
24     *  - subtype: (string)
25     *  - q: (float) q-factor weighting
26     *  - i: (int) index
27     *  - params: (array)
28     */
29    public function parseAccept( $accept ): array {
30        // FIXME: Allow commas in quotes
31        $accepts = explode( ',', $accept );
32        $ret = [];
33
34        foreach ( $accepts as $i => $a ) {
35            if ( !preg_match( '!^([^\s/;]+)/([^;\s]+)\s*(?:;(.*))?$!D', trim( $a ), $matches ) ) {
36                continue;
37            }
38
39            $q = 1;
40            $params = [];
41            if ( isset( $matches[3] ) ) {
42                // FIXME: Allow semi-colon in quotes
43                $kvps = explode( ';', $matches[3] );
44                foreach ( $kvps as $kv ) {
45                    $kvArray = explode( '=', trim( $kv ), 2 );
46                    if ( count( $kvArray ) != 2 ) {
47                        continue;
48                    }
49                    [ $key, $val ] = $kvArray;
50                    $key = strtolower( trim( $key ) );
51                    $val = trim( $val );
52                    if ( $key === 'q' ) {
53                        // FIXME: Spec is stricter about this
54                        $q = (float)$val;
55                    } else {
56                        if ( $val && $val[0] === '"' && str_ends_with( $val, '"' ) ) {
57                            $val = substr( $val, 1, -1 );
58                        }
59                        $params[$key] = $val;
60                    }
61                }
62            }
63            $ret[] = [
64                'type' => $matches[1],
65                'subtype' => $matches[2],
66                'q' => $q,
67                'i' => $i,
68                'params' => $params,
69            ];
70        }
71
72        // Sort list. First by q values, then by order
73        usort( $ret, static function ( $a, $b ) {
74            if ( $b['q'] > $a['q'] ) {
75                return 1;
76            } elseif ( $b['q'] === $a['q'] ) {
77                return $a['i'] - $b['i'];
78            } else {
79                return -1;
80            }
81        } );
82
83        return $ret;
84    }
85
86    /**
87     * Parses an HTTP header into a weight map, that is an associative array
88     * mapping values to their respective weights. Any header name preceding
89     * weight spec is ignored for convenience.
90     *
91     * Note that type parameters and accept extension like the "level" parameter
92     * are not supported, weights are derived from "q" values only.
93     *
94     * See RFC 7231 section 5.3.2 for details.
95     *
96     * @param string $rawHeader
97     *
98     * @return array
99     */
100    public function parseWeights( $rawHeader ) {
101        // first, strip header name
102        $rawHeader = preg_replace( '/^[-\w]+:\s*/', '', $rawHeader );
103
104        // Return values in lower case
105        $rawHeader = strtolower( $rawHeader );
106
107        $accepts = $this->parseAccept( $rawHeader );
108
109        // Create a list like "en" => 0.8
110        return array_reduce( $accepts, static function ( $prev, $next ) {
111            $type = "{$next['type']}/{$next['subtype']}";
112            $prev[$type] = $next['q'];
113            return $prev;
114        }, [] );
115    }
116
117}