xref: /plugin/davcal/vendor/sabre/http/lib/functions.php (revision a1a3b6794e0e143a4a8b51d3185ce2d339be61ab)
1*a1a3b679SAndreas Boehler<?php
2*a1a3b679SAndreas Boehler
3*a1a3b679SAndreas Boehlernamespace Sabre\HTTP;
4*a1a3b679SAndreas Boehler
5*a1a3b679SAndreas Boehleruse DateTime;
6*a1a3b679SAndreas Boehler
7*a1a3b679SAndreas Boehler/**
8*a1a3b679SAndreas Boehler * A collection of useful helpers for parsing or generating various HTTP
9*a1a3b679SAndreas Boehler * headers.
10*a1a3b679SAndreas Boehler *
11*a1a3b679SAndreas Boehler * @copyright Copyright (C) 2007-2014 fruux GmbH. All rights reserved.
12*a1a3b679SAndreas Boehler * @author Evert Pot (http://evertpot.com/)
13*a1a3b679SAndreas Boehler * @license http://sabre.io/license/ Modified BSD License
14*a1a3b679SAndreas Boehler */
15*a1a3b679SAndreas Boehler
16*a1a3b679SAndreas Boehler/**
17*a1a3b679SAndreas Boehler * Parses a HTTP date-string.
18*a1a3b679SAndreas Boehler *
19*a1a3b679SAndreas Boehler * This method returns false if the date is invalid.
20*a1a3b679SAndreas Boehler *
21*a1a3b679SAndreas Boehler * The following formats are supported:
22*a1a3b679SAndreas Boehler *    Sun, 06 Nov 1994 08:49:37 GMT    ; IMF-fixdate
23*a1a3b679SAndreas Boehler *    Sunday, 06-Nov-94 08:49:37 GMT   ; obsolete RFC 850 format
24*a1a3b679SAndreas Boehler *    Sun Nov  6 08:49:37 1994         ; ANSI C's asctime() format
25*a1a3b679SAndreas Boehler *
26*a1a3b679SAndreas Boehler * See:
27*a1a3b679SAndreas Boehler *   http://tools.ietf.org/html/rfc7231#section-7.1.1.1
28*a1a3b679SAndreas Boehler *
29*a1a3b679SAndreas Boehler * @param string $dateString
30*a1a3b679SAndreas Boehler * @return bool|DateTime
31*a1a3b679SAndreas Boehler */
32*a1a3b679SAndreas Boehlerfunction parseDate($dateString) {
33*a1a3b679SAndreas Boehler
34*a1a3b679SAndreas Boehler    // Only the format is checked, valid ranges are checked by strtotime below
35*a1a3b679SAndreas Boehler    $month = '(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)';
36*a1a3b679SAndreas Boehler    $weekday = '(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)';
37*a1a3b679SAndreas Boehler    $wkday = '(Mon|Tue|Wed|Thu|Fri|Sat|Sun)';
38*a1a3b679SAndreas Boehler    $time = '([0-1]\d|2[0-3])(\:[0-5]\d){2}';
39*a1a3b679SAndreas Boehler    $date3 = $month . ' ([12]\d|3[01]| [1-9])';
40*a1a3b679SAndreas Boehler    $date2 = '(0[1-9]|[12]\d|3[01])\-' . $month . '\-\d{2}';
41*a1a3b679SAndreas Boehler    // 4-digit year cannot begin with 0 - unix timestamp begins in 1970
42*a1a3b679SAndreas Boehler    $date1 = '(0[1-9]|[12]\d|3[01]) ' . $month . ' [1-9]\d{3}';
43*a1a3b679SAndreas Boehler
44*a1a3b679SAndreas Boehler    // ANSI C's asctime() format
45*a1a3b679SAndreas Boehler    // 4-digit year cannot begin with 0 - unix timestamp begins in 1970
46*a1a3b679SAndreas Boehler    $asctime_date = $wkday . ' ' . $date3 . ' ' . $time . ' [1-9]\d{3}';
47*a1a3b679SAndreas Boehler    // RFC 850, obsoleted by RFC 1036
48*a1a3b679SAndreas Boehler    $rfc850_date = $weekday . ', ' . $date2 . ' ' . $time . ' GMT';
49*a1a3b679SAndreas Boehler    // RFC 822, updated by RFC 1123
50*a1a3b679SAndreas Boehler    $rfc1123_date = $wkday . ', ' . $date1 . ' ' . $time . ' GMT';
51*a1a3b679SAndreas Boehler    // allowed date formats by RFC 2616
52*a1a3b679SAndreas Boehler    $HTTP_date = "($rfc1123_date|$rfc850_date|$asctime_date)";
53*a1a3b679SAndreas Boehler
54*a1a3b679SAndreas Boehler    // allow for space around the string and strip it
55*a1a3b679SAndreas Boehler    $dateString = trim($dateString, ' ');
56*a1a3b679SAndreas Boehler    if (!preg_match('/^' . $HTTP_date . '$/', $dateString))
57*a1a3b679SAndreas Boehler        return false;
58*a1a3b679SAndreas Boehler
59*a1a3b679SAndreas Boehler    // append implicit GMT timezone to ANSI C time format
60*a1a3b679SAndreas Boehler    if (strpos($dateString, ' GMT') === false)
61*a1a3b679SAndreas Boehler        $dateString .= ' GMT';
62*a1a3b679SAndreas Boehler
63*a1a3b679SAndreas Boehler    try {
64*a1a3b679SAndreas Boehler        return new DateTime($dateString, new \DateTimeZone('UTC'));
65*a1a3b679SAndreas Boehler    } catch (\Exception $e) {
66*a1a3b679SAndreas Boehler        return false;
67*a1a3b679SAndreas Boehler    }
68*a1a3b679SAndreas Boehler
69*a1a3b679SAndreas Boehler}
70*a1a3b679SAndreas Boehler
71*a1a3b679SAndreas Boehler/**
72*a1a3b679SAndreas Boehler * Transforms a DateTime object to a valid HTTP/1.1 Date header value
73*a1a3b679SAndreas Boehler *
74*a1a3b679SAndreas Boehler * @param DateTime $dateTime
75*a1a3b679SAndreas Boehler * @return string
76*a1a3b679SAndreas Boehler */
77*a1a3b679SAndreas Boehlerfunction toDate(DateTime $dateTime) {
78*a1a3b679SAndreas Boehler
79*a1a3b679SAndreas Boehler    // We need to clone it, as we don't want to affect the existing
80*a1a3b679SAndreas Boehler    // DateTime.
81*a1a3b679SAndreas Boehler    $dateTime = clone $dateTime;
82*a1a3b679SAndreas Boehler    $dateTime->setTimeZone(new \DateTimeZone('GMT'));
83*a1a3b679SAndreas Boehler    return $dateTime->format('D, d M Y H:i:s \G\M\T');
84*a1a3b679SAndreas Boehler
85*a1a3b679SAndreas Boehler}
86*a1a3b679SAndreas Boehler
87*a1a3b679SAndreas Boehler/**
88*a1a3b679SAndreas Boehler * This function can be used to aid with content negotiation.
89*a1a3b679SAndreas Boehler *
90*a1a3b679SAndreas Boehler * It takes 2 arguments, the $acceptHeaderValue, which usually comes from
91*a1a3b679SAndreas Boehler * an Accept header, and $availableOptions, which contains an array of
92*a1a3b679SAndreas Boehler * items that the server can support.
93*a1a3b679SAndreas Boehler *
94*a1a3b679SAndreas Boehler * The result of this function will be the 'best possible option'. If no
95*a1a3b679SAndreas Boehler * best possible option could be found, null is returned.
96*a1a3b679SAndreas Boehler *
97*a1a3b679SAndreas Boehler * When it's null you can according to the spec either return a default, or
98*a1a3b679SAndreas Boehler * you can choose to emit 406 Not Acceptable.
99*a1a3b679SAndreas Boehler *
100*a1a3b679SAndreas Boehler * The method also accepts sending 'null' for the $acceptHeaderValue,
101*a1a3b679SAndreas Boehler * implying that no accept header was sent.
102*a1a3b679SAndreas Boehler *
103*a1a3b679SAndreas Boehler * @param string|null $acceptHeaderValue
104*a1a3b679SAndreas Boehler * @param array $availableOptions
105*a1a3b679SAndreas Boehler * @return string|null
106*a1a3b679SAndreas Boehler */
107*a1a3b679SAndreas Boehlerfunction negotiateContentType($acceptHeaderValue, array $availableOptions) {
108*a1a3b679SAndreas Boehler
109*a1a3b679SAndreas Boehler    if (!$acceptHeaderValue) {
110*a1a3b679SAndreas Boehler        // Grabbing the first in the list.
111*a1a3b679SAndreas Boehler        return reset($availableOptions);
112*a1a3b679SAndreas Boehler    }
113*a1a3b679SAndreas Boehler
114*a1a3b679SAndreas Boehler    $proposals = array_map(
115*a1a3b679SAndreas Boehler        'Sabre\HTTP\parseMimeType',
116*a1a3b679SAndreas Boehler        explode(',', $acceptHeaderValue)
117*a1a3b679SAndreas Boehler    );
118*a1a3b679SAndreas Boehler
119*a1a3b679SAndreas Boehler    // Ensuring array keys are reset.
120*a1a3b679SAndreas Boehler    $availableOptions = array_values($availableOptions);
121*a1a3b679SAndreas Boehler
122*a1a3b679SAndreas Boehler    $options = array_map(
123*a1a3b679SAndreas Boehler        'Sabre\HTTP\parseMimeType',
124*a1a3b679SAndreas Boehler        $availableOptions
125*a1a3b679SAndreas Boehler    );
126*a1a3b679SAndreas Boehler
127*a1a3b679SAndreas Boehler    $lastQuality = 0;
128*a1a3b679SAndreas Boehler    $lastSpecificity = 0;
129*a1a3b679SAndreas Boehler    $lastOptionIndex = 0;
130*a1a3b679SAndreas Boehler    $lastChoice = null;
131*a1a3b679SAndreas Boehler
132*a1a3b679SAndreas Boehler    foreach ($proposals as $proposal) {
133*a1a3b679SAndreas Boehler
134*a1a3b679SAndreas Boehler        // Ignoring broken values.
135*a1a3b679SAndreas Boehler        if (is_null($proposal)) continue;
136*a1a3b679SAndreas Boehler
137*a1a3b679SAndreas Boehler        // If the quality is lower we don't have to bother comparing.
138*a1a3b679SAndreas Boehler        if ($proposal['quality'] < $lastQuality) {
139*a1a3b679SAndreas Boehler            continue;
140*a1a3b679SAndreas Boehler        }
141*a1a3b679SAndreas Boehler
142*a1a3b679SAndreas Boehler        foreach ($options as $optionIndex => $option) {
143*a1a3b679SAndreas Boehler
144*a1a3b679SAndreas Boehler            if ($proposal['type'] !== '*' && $proposal['type'] !== $option['type']) {
145*a1a3b679SAndreas Boehler                // no match on type.
146*a1a3b679SAndreas Boehler                continue;
147*a1a3b679SAndreas Boehler            }
148*a1a3b679SAndreas Boehler            if ($proposal['subType'] !== '*' && $proposal['subType'] !== $option['subType']) {
149*a1a3b679SAndreas Boehler                // no match on subtype.
150*a1a3b679SAndreas Boehler                continue;
151*a1a3b679SAndreas Boehler            }
152*a1a3b679SAndreas Boehler
153*a1a3b679SAndreas Boehler            // Any parameters appearing on the options must appear on
154*a1a3b679SAndreas Boehler            // proposals.
155*a1a3b679SAndreas Boehler            foreach ($option['parameters'] as $paramName => $paramValue) {
156*a1a3b679SAndreas Boehler                if (!array_key_exists($paramName, $proposal['parameters'])) {
157*a1a3b679SAndreas Boehler                    continue 2;
158*a1a3b679SAndreas Boehler                }
159*a1a3b679SAndreas Boehler                if ($paramValue !== $proposal['parameters'][$paramName]) {
160*a1a3b679SAndreas Boehler                    continue 2;
161*a1a3b679SAndreas Boehler                }
162*a1a3b679SAndreas Boehler            }
163*a1a3b679SAndreas Boehler
164*a1a3b679SAndreas Boehler            // If we got here, we have a match on parameters, type and
165*a1a3b679SAndreas Boehler            // subtype. We need to calculate a score for how specific the
166*a1a3b679SAndreas Boehler            // match was.
167*a1a3b679SAndreas Boehler            $specificity =
168*a1a3b679SAndreas Boehler                ($proposal['type'] !== '*' ? 20 : 0) +
169*a1a3b679SAndreas Boehler                ($proposal['subType'] !== '*' ? 10 : 0) +
170*a1a3b679SAndreas Boehler                count($option['parameters']);
171*a1a3b679SAndreas Boehler
172*a1a3b679SAndreas Boehler
173*a1a3b679SAndreas Boehler            // Does this entry win?
174*a1a3b679SAndreas Boehler            if (
175*a1a3b679SAndreas Boehler                ($proposal['quality'] > $lastQuality) ||
176*a1a3b679SAndreas Boehler                ($proposal['quality'] === $lastQuality && $specificity > $lastSpecificity) ||
177*a1a3b679SAndreas Boehler                ($proposal['quality'] === $lastQuality && $specificity === $lastSpecificity && $optionIndex < $lastOptionIndex)
178*a1a3b679SAndreas Boehler            ) {
179*a1a3b679SAndreas Boehler
180*a1a3b679SAndreas Boehler                $lastQuality = $proposal['quality'];
181*a1a3b679SAndreas Boehler                $lastSpecificity = $specificity;
182*a1a3b679SAndreas Boehler                $lastOptionIndex = $optionIndex;
183*a1a3b679SAndreas Boehler                $lastChoice = $availableOptions[$optionIndex];
184*a1a3b679SAndreas Boehler
185*a1a3b679SAndreas Boehler            }
186*a1a3b679SAndreas Boehler
187*a1a3b679SAndreas Boehler        }
188*a1a3b679SAndreas Boehler
189*a1a3b679SAndreas Boehler    }
190*a1a3b679SAndreas Boehler
191*a1a3b679SAndreas Boehler    return $lastChoice;
192*a1a3b679SAndreas Boehler
193*a1a3b679SAndreas Boehler}
194*a1a3b679SAndreas Boehler
195*a1a3b679SAndreas Boehler/**
196*a1a3b679SAndreas Boehler * Parses the Prefer header, as defined in RFC7240.
197*a1a3b679SAndreas Boehler *
198*a1a3b679SAndreas Boehler * Input can be given as a single header value (string) or multiple headers
199*a1a3b679SAndreas Boehler * (array of string).
200*a1a3b679SAndreas Boehler *
201*a1a3b679SAndreas Boehler * This method will return a key->value array with the various Prefer
202*a1a3b679SAndreas Boehler * parameters.
203*a1a3b679SAndreas Boehler *
204*a1a3b679SAndreas Boehler * Prefer: return=minimal will result in:
205*a1a3b679SAndreas Boehler *
206*a1a3b679SAndreas Boehler * [ 'return' => 'minimal' ]
207*a1a3b679SAndreas Boehler *
208*a1a3b679SAndreas Boehler * Prefer: foo, wait=10 will result in:
209*a1a3b679SAndreas Boehler *
210*a1a3b679SAndreas Boehler * [ 'foo' => true, 'wait' => '10']
211*a1a3b679SAndreas Boehler *
212*a1a3b679SAndreas Boehler * This method also supports the formats from older drafts of RFC7240, and
213*a1a3b679SAndreas Boehler * it will automatically map them to the new values, as the older values
214*a1a3b679SAndreas Boehler * are still pretty common.
215*a1a3b679SAndreas Boehler *
216*a1a3b679SAndreas Boehler * Parameters are currently discarded. There's no known prefer value that
217*a1a3b679SAndreas Boehler * uses them.
218*a1a3b679SAndreas Boehler *
219*a1a3b679SAndreas Boehler * @param string|string[] $header
220*a1a3b679SAndreas Boehler * @return array
221*a1a3b679SAndreas Boehler */
222*a1a3b679SAndreas Boehlerfunction parsePrefer($input) {
223*a1a3b679SAndreas Boehler
224*a1a3b679SAndreas Boehler    $token = '[!#$%&\'*+\-.^_`~A-Za-z0-9]+';
225*a1a3b679SAndreas Boehler
226*a1a3b679SAndreas Boehler    // Work in progress
227*a1a3b679SAndreas Boehler    $word = '(?: [a-zA-Z0-9]+ | "[a-zA-Z0-9]*" )';
228*a1a3b679SAndreas Boehler
229*a1a3b679SAndreas Boehler    $regex = <<<REGEX
230*a1a3b679SAndreas Boehler/
231*a1a3b679SAndreas Boehler^
232*a1a3b679SAndreas Boehler(?<name> $token)      # Prefer property name
233*a1a3b679SAndreas Boehler\s*                   # Optional space
234*a1a3b679SAndreas Boehler(?: = \s*             # Prefer property value
235*a1a3b679SAndreas Boehler   (?<value> $word)
236*a1a3b679SAndreas Boehler)?
237*a1a3b679SAndreas Boehler(?: \s* ; (?: .*))?   # Prefer parameters (ignored)
238*a1a3b679SAndreas Boehler$
239*a1a3b679SAndreas Boehler/x
240*a1a3b679SAndreas BoehlerREGEX;
241*a1a3b679SAndreas Boehler
242*a1a3b679SAndreas Boehler    $output = [];
243*a1a3b679SAndreas Boehler    foreach (getHeaderValues($input) as $value) {
244*a1a3b679SAndreas Boehler
245*a1a3b679SAndreas Boehler        if (!preg_match($regex, $value, $matches)) {
246*a1a3b679SAndreas Boehler            // Ignore
247*a1a3b679SAndreas Boehler            continue;
248*a1a3b679SAndreas Boehler        }
249*a1a3b679SAndreas Boehler
250*a1a3b679SAndreas Boehler        // Mapping old values to their new counterparts
251*a1a3b679SAndreas Boehler        switch ($matches['name']) {
252*a1a3b679SAndreas Boehler            case 'return-asynch' :
253*a1a3b679SAndreas Boehler                $output['respond-async'] = true;
254*a1a3b679SAndreas Boehler                break;
255*a1a3b679SAndreas Boehler            case 'return-representation' :
256*a1a3b679SAndreas Boehler                $output['return'] = 'representation';
257*a1a3b679SAndreas Boehler                break;
258*a1a3b679SAndreas Boehler            case 'return-minimal' :
259*a1a3b679SAndreas Boehler                $output['return'] = 'minimal';
260*a1a3b679SAndreas Boehler                break;
261*a1a3b679SAndreas Boehler            case 'strict' :
262*a1a3b679SAndreas Boehler                $output['handling'] = 'strict';
263*a1a3b679SAndreas Boehler                break;
264*a1a3b679SAndreas Boehler            case 'lenient' :
265*a1a3b679SAndreas Boehler                $output['handling'] = 'lenient';
266*a1a3b679SAndreas Boehler                break;
267*a1a3b679SAndreas Boehler            default :
268*a1a3b679SAndreas Boehler                if (isset($matches['value'])) {
269*a1a3b679SAndreas Boehler                    $value = trim($matches['value'], '"');
270*a1a3b679SAndreas Boehler                } else {
271*a1a3b679SAndreas Boehler                    $value = true;
272*a1a3b679SAndreas Boehler                }
273*a1a3b679SAndreas Boehler                $output[strtolower($matches['name'])] = empty($value) ? true : $value;
274*a1a3b679SAndreas Boehler                break;
275*a1a3b679SAndreas Boehler        }
276*a1a3b679SAndreas Boehler
277*a1a3b679SAndreas Boehler    }
278*a1a3b679SAndreas Boehler
279*a1a3b679SAndreas Boehler    return $output;
280*a1a3b679SAndreas Boehler
281*a1a3b679SAndreas Boehler}
282*a1a3b679SAndreas Boehler
283*a1a3b679SAndreas Boehler/**
284*a1a3b679SAndreas Boehler * This method splits up headers into all their individual values.
285*a1a3b679SAndreas Boehler *
286*a1a3b679SAndreas Boehler * A HTTP header may have more than one header, such as this:
287*a1a3b679SAndreas Boehler *   Cache-Control: private, no-store
288*a1a3b679SAndreas Boehler *
289*a1a3b679SAndreas Boehler * Header values are always split with a comma.
290*a1a3b679SAndreas Boehler *
291*a1a3b679SAndreas Boehler * You can pass either a string, or an array. The resulting value is always
292*a1a3b679SAndreas Boehler * an array with each spliced value.
293*a1a3b679SAndreas Boehler *
294*a1a3b679SAndreas Boehler * If the second headers argument is set, this value will simply be merged
295*a1a3b679SAndreas Boehler * in. This makes it quicker to merge an old list of values with a new set.
296*a1a3b679SAndreas Boehler *
297*a1a3b679SAndreas Boehler * @param string|string[] $values
298*a1a3b679SAndreas Boehler * @param string|string[] $values2
299*a1a3b679SAndreas Boehler * @return string[]
300*a1a3b679SAndreas Boehler */
301*a1a3b679SAndreas Boehlerfunction getHeaderValues($values, $values2 = null) {
302*a1a3b679SAndreas Boehler
303*a1a3b679SAndreas Boehler    $values = (array)$values;
304*a1a3b679SAndreas Boehler    if ($values2) {
305*a1a3b679SAndreas Boehler        $values = array_merge($values, (array)$values2);
306*a1a3b679SAndreas Boehler    }
307*a1a3b679SAndreas Boehler    foreach ($values as $l1) {
308*a1a3b679SAndreas Boehler        foreach (explode(',', $l1) as $l2) {
309*a1a3b679SAndreas Boehler            $result[] = trim($l2);
310*a1a3b679SAndreas Boehler        }
311*a1a3b679SAndreas Boehler    }
312*a1a3b679SAndreas Boehler    return $result;
313*a1a3b679SAndreas Boehler
314*a1a3b679SAndreas Boehler}
315*a1a3b679SAndreas Boehler
316*a1a3b679SAndreas Boehler/**
317*a1a3b679SAndreas Boehler * Parses a mime-type and splits it into:
318*a1a3b679SAndreas Boehler *
319*a1a3b679SAndreas Boehler * 1. type
320*a1a3b679SAndreas Boehler * 2. subtype
321*a1a3b679SAndreas Boehler * 3. quality
322*a1a3b679SAndreas Boehler * 4. parameters
323*a1a3b679SAndreas Boehler *
324*a1a3b679SAndreas Boehler * @param string $str
325*a1a3b679SAndreas Boehler * @return array
326*a1a3b679SAndreas Boehler */
327*a1a3b679SAndreas Boehlerfunction parseMimeType($str) {
328*a1a3b679SAndreas Boehler
329*a1a3b679SAndreas Boehler    $parameters = [];
330*a1a3b679SAndreas Boehler    // If no q= parameter appears, then quality = 1.
331*a1a3b679SAndreas Boehler    $quality = 1;
332*a1a3b679SAndreas Boehler
333*a1a3b679SAndreas Boehler    $parts = explode(';', $str);
334*a1a3b679SAndreas Boehler
335*a1a3b679SAndreas Boehler    // The first part is the mime-type.
336*a1a3b679SAndreas Boehler    $mimeType = array_shift($parts);
337*a1a3b679SAndreas Boehler
338*a1a3b679SAndreas Boehler    $mimeType = explode('/', trim($mimeType));
339*a1a3b679SAndreas Boehler    if (count($mimeType) !== 2) {
340*a1a3b679SAndreas Boehler        // Illegal value
341*a1a3b679SAndreas Boehler        return null;
342*a1a3b679SAndreas Boehler    }
343*a1a3b679SAndreas Boehler    list($type, $subType) = $mimeType;
344*a1a3b679SAndreas Boehler
345*a1a3b679SAndreas Boehler    foreach ($parts as $part) {
346*a1a3b679SAndreas Boehler
347*a1a3b679SAndreas Boehler        $part = trim($part);
348*a1a3b679SAndreas Boehler        if (strpos($part, '=')) {
349*a1a3b679SAndreas Boehler            list($partName, $partValue) =
350*a1a3b679SAndreas Boehler                explode('=', $part, 2);
351*a1a3b679SAndreas Boehler        } else {
352*a1a3b679SAndreas Boehler            $partName = $part;
353*a1a3b679SAndreas Boehler            $partValue = null;
354*a1a3b679SAndreas Boehler        }
355*a1a3b679SAndreas Boehler
356*a1a3b679SAndreas Boehler        // The quality parameter, if it appears, also marks the end of
357*a1a3b679SAndreas Boehler        // the parameter list. Anything after the q= counts as an
358*a1a3b679SAndreas Boehler        // 'accept extension' and could introduce new semantics in
359*a1a3b679SAndreas Boehler        // content-negotation.
360*a1a3b679SAndreas Boehler        if ($partName !== 'q') {
361*a1a3b679SAndreas Boehler            $parameters[$partName] = $part;
362*a1a3b679SAndreas Boehler        } else {
363*a1a3b679SAndreas Boehler            $quality = (float)$partValue;
364*a1a3b679SAndreas Boehler            break; // Stop parsing parts
365*a1a3b679SAndreas Boehler        }
366*a1a3b679SAndreas Boehler
367*a1a3b679SAndreas Boehler    }
368*a1a3b679SAndreas Boehler
369*a1a3b679SAndreas Boehler    return [
370*a1a3b679SAndreas Boehler        'type'       => $type,
371*a1a3b679SAndreas Boehler        'subType'    => $subType,
372*a1a3b679SAndreas Boehler        'quality'    => $quality,
373*a1a3b679SAndreas Boehler        'parameters' => $parameters,
374*a1a3b679SAndreas Boehler    ];
375*a1a3b679SAndreas Boehler
376*a1a3b679SAndreas Boehler}
377*a1a3b679SAndreas Boehler
378*a1a3b679SAndreas Boehler/**
379*a1a3b679SAndreas Boehler * Encodes the path of a url.
380*a1a3b679SAndreas Boehler *
381*a1a3b679SAndreas Boehler * slashes (/) are treated as path-separators.
382*a1a3b679SAndreas Boehler *
383*a1a3b679SAndreas Boehler * @param string $path
384*a1a3b679SAndreas Boehler * @return string
385*a1a3b679SAndreas Boehler */
386*a1a3b679SAndreas Boehlerfunction encodePath($path) {
387*a1a3b679SAndreas Boehler
388*a1a3b679SAndreas Boehler    return preg_replace_callback('/([^A-Za-z0-9_\-\.~\(\)\/:@])/', function($match) {
389*a1a3b679SAndreas Boehler
390*a1a3b679SAndreas Boehler        return '%' . sprintf('%02x', ord($match[0]));
391*a1a3b679SAndreas Boehler
392*a1a3b679SAndreas Boehler    }, $path);
393*a1a3b679SAndreas Boehler
394*a1a3b679SAndreas Boehler}
395*a1a3b679SAndreas Boehler
396*a1a3b679SAndreas Boehler/**
397*a1a3b679SAndreas Boehler * Encodes a 1 segment of a path
398*a1a3b679SAndreas Boehler *
399*a1a3b679SAndreas Boehler * Slashes are considered part of the name, and are encoded as %2f
400*a1a3b679SAndreas Boehler *
401*a1a3b679SAndreas Boehler * @param string $pathSegment
402*a1a3b679SAndreas Boehler * @return string
403*a1a3b679SAndreas Boehler */
404*a1a3b679SAndreas Boehlerfunction encodePathSegment($pathSegment) {
405*a1a3b679SAndreas Boehler
406*a1a3b679SAndreas Boehler    return preg_replace_callback('/([^A-Za-z0-9_\-\.~\(\):@])/', function($match) {
407*a1a3b679SAndreas Boehler
408*a1a3b679SAndreas Boehler        return '%' . sprintf('%02x', ord($match[0]));
409*a1a3b679SAndreas Boehler
410*a1a3b679SAndreas Boehler    }, $pathSegment);
411*a1a3b679SAndreas Boehler}
412*a1a3b679SAndreas Boehler
413*a1a3b679SAndreas Boehler/**
414*a1a3b679SAndreas Boehler * Decodes a url-encoded path
415*a1a3b679SAndreas Boehler *
416*a1a3b679SAndreas Boehler * @param string $path
417*a1a3b679SAndreas Boehler * @return string
418*a1a3b679SAndreas Boehler */
419*a1a3b679SAndreas Boehlerfunction decodePath($path) {
420*a1a3b679SAndreas Boehler
421*a1a3b679SAndreas Boehler    return decodePathSegment($path);
422*a1a3b679SAndreas Boehler
423*a1a3b679SAndreas Boehler}
424*a1a3b679SAndreas Boehler
425*a1a3b679SAndreas Boehler/**
426*a1a3b679SAndreas Boehler * Decodes a url-encoded path segment
427*a1a3b679SAndreas Boehler *
428*a1a3b679SAndreas Boehler * @param string $path
429*a1a3b679SAndreas Boehler * @return string
430*a1a3b679SAndreas Boehler */
431*a1a3b679SAndreas Boehlerfunction decodePathSegment($path) {
432*a1a3b679SAndreas Boehler
433*a1a3b679SAndreas Boehler    $path = rawurldecode($path);
434*a1a3b679SAndreas Boehler    $encoding = mb_detect_encoding($path, ['UTF-8', 'ISO-8859-1']);
435*a1a3b679SAndreas Boehler
436*a1a3b679SAndreas Boehler    switch ($encoding) {
437*a1a3b679SAndreas Boehler
438*a1a3b679SAndreas Boehler        case 'ISO-8859-1' :
439*a1a3b679SAndreas Boehler            $path = utf8_encode($path);
440*a1a3b679SAndreas Boehler
441*a1a3b679SAndreas Boehler    }
442*a1a3b679SAndreas Boehler
443*a1a3b679SAndreas Boehler    return $path;
444*a1a3b679SAndreas Boehler
445*a1a3b679SAndreas Boehler}
446