Final changes for the Use cases on the live site.
[yaffs-website] / vendor / guzzlehttp / psr7 / src / functions.php
1 <?php
2 namespace GuzzleHttp\Psr7;
3
4 use Psr\Http\Message\MessageInterface;
5 use Psr\Http\Message\RequestInterface;
6 use Psr\Http\Message\ResponseInterface;
7 use Psr\Http\Message\ServerRequestInterface;
8 use Psr\Http\Message\StreamInterface;
9 use Psr\Http\Message\UriInterface;
10
11 /**
12  * Returns the string representation of an HTTP message.
13  *
14  * @param MessageInterface $message Message to convert to a string.
15  *
16  * @return string
17  */
18 function str(MessageInterface $message)
19 {
20     if ($message instanceof RequestInterface) {
21         $msg = trim($message->getMethod() . ' '
22                 . $message->getRequestTarget())
23             . ' HTTP/' . $message->getProtocolVersion();
24         if (!$message->hasHeader('host')) {
25             $msg .= "\r\nHost: " . $message->getUri()->getHost();
26         }
27     } elseif ($message instanceof ResponseInterface) {
28         $msg = 'HTTP/' . $message->getProtocolVersion() . ' '
29             . $message->getStatusCode() . ' '
30             . $message->getReasonPhrase();
31     } else {
32         throw new \InvalidArgumentException('Unknown message type');
33     }
34
35     foreach ($message->getHeaders() as $name => $values) {
36         $msg .= "\r\n{$name}: " . implode(', ', $values);
37     }
38
39     return "{$msg}\r\n\r\n" . $message->getBody();
40 }
41
42 /**
43  * Returns a UriInterface for the given value.
44  *
45  * This function accepts a string or {@see Psr\Http\Message\UriInterface} and
46  * returns a UriInterface for the given value. If the value is already a
47  * `UriInterface`, it is returned as-is.
48  *
49  * @param string|UriInterface $uri
50  *
51  * @return UriInterface
52  * @throws \InvalidArgumentException
53  */
54 function uri_for($uri)
55 {
56     if ($uri instanceof UriInterface) {
57         return $uri;
58     } elseif (is_string($uri)) {
59         return new Uri($uri);
60     }
61
62     throw new \InvalidArgumentException('URI must be a string or UriInterface');
63 }
64
65 /**
66  * Create a new stream based on the input type.
67  *
68  * Options is an associative array that can contain the following keys:
69  * - metadata: Array of custom metadata.
70  * - size: Size of the stream.
71  *
72  * @param resource|string|null|int|float|bool|StreamInterface|callable $resource Entity body data
73  * @param array                                                        $options  Additional options
74  *
75  * @return Stream
76  * @throws \InvalidArgumentException if the $resource arg is not valid.
77  */
78 function stream_for($resource = '', array $options = [])
79 {
80     if (is_scalar($resource)) {
81         $stream = fopen('php://temp', 'r+');
82         if ($resource !== '') {
83             fwrite($stream, $resource);
84             fseek($stream, 0);
85         }
86         return new Stream($stream, $options);
87     }
88
89     switch (gettype($resource)) {
90         case 'resource':
91             return new Stream($resource, $options);
92         case 'object':
93             if ($resource instanceof StreamInterface) {
94                 return $resource;
95             } elseif ($resource instanceof \Iterator) {
96                 return new PumpStream(function () use ($resource) {
97                     if (!$resource->valid()) {
98                         return false;
99                     }
100                     $result = $resource->current();
101                     $resource->next();
102                     return $result;
103                 }, $options);
104             } elseif (method_exists($resource, '__toString')) {
105                 return stream_for((string) $resource, $options);
106             }
107             break;
108         case 'NULL':
109             return new Stream(fopen('php://temp', 'r+'), $options);
110     }
111
112     if (is_callable($resource)) {
113         return new PumpStream($resource, $options);
114     }
115
116     throw new \InvalidArgumentException('Invalid resource type: ' . gettype($resource));
117 }
118
119 /**
120  * Parse an array of header values containing ";" separated data into an
121  * array of associative arrays representing the header key value pair
122  * data of the header. When a parameter does not contain a value, but just
123  * contains a key, this function will inject a key with a '' string value.
124  *
125  * @param string|array $header Header to parse into components.
126  *
127  * @return array Returns the parsed header values.
128  */
129 function parse_header($header)
130 {
131     static $trimmed = "\"'  \n\t\r";
132     $params = $matches = [];
133
134     foreach (normalize_header($header) as $val) {
135         $part = [];
136         foreach (preg_split('/;(?=([^"]*"[^"]*")*[^"]*$)/', $val) as $kvp) {
137             if (preg_match_all('/<[^>]+>|[^=]+/', $kvp, $matches)) {
138                 $m = $matches[0];
139                 if (isset($m[1])) {
140                     $part[trim($m[0], $trimmed)] = trim($m[1], $trimmed);
141                 } else {
142                     $part[] = trim($m[0], $trimmed);
143                 }
144             }
145         }
146         if ($part) {
147             $params[] = $part;
148         }
149     }
150
151     return $params;
152 }
153
154 /**
155  * Converts an array of header values that may contain comma separated
156  * headers into an array of headers with no comma separated values.
157  *
158  * @param string|array $header Header to normalize.
159  *
160  * @return array Returns the normalized header field values.
161  */
162 function normalize_header($header)
163 {
164     if (!is_array($header)) {
165         return array_map('trim', explode(',', $header));
166     }
167
168     $result = [];
169     foreach ($header as $value) {
170         foreach ((array) $value as $v) {
171             if (strpos($v, ',') === false) {
172                 $result[] = $v;
173                 continue;
174             }
175             foreach (preg_split('/,(?=([^"]*"[^"]*")*[^"]*$)/', $v) as $vv) {
176                 $result[] = trim($vv);
177             }
178         }
179     }
180
181     return $result;
182 }
183
184 /**
185  * Clone and modify a request with the given changes.
186  *
187  * The changes can be one of:
188  * - method: (string) Changes the HTTP method.
189  * - set_headers: (array) Sets the given headers.
190  * - remove_headers: (array) Remove the given headers.
191  * - body: (mixed) Sets the given body.
192  * - uri: (UriInterface) Set the URI.
193  * - query: (string) Set the query string value of the URI.
194  * - version: (string) Set the protocol version.
195  *
196  * @param RequestInterface $request Request to clone and modify.
197  * @param array            $changes Changes to apply.
198  *
199  * @return RequestInterface
200  */
201 function modify_request(RequestInterface $request, array $changes)
202 {
203     if (!$changes) {
204         return $request;
205     }
206
207     $headers = $request->getHeaders();
208
209     if (!isset($changes['uri'])) {
210         $uri = $request->getUri();
211     } else {
212         // Remove the host header if one is on the URI
213         if ($host = $changes['uri']->getHost()) {
214             $changes['set_headers']['Host'] = $host;
215
216             if ($port = $changes['uri']->getPort()) {
217                 $standardPorts = ['http' => 80, 'https' => 443];
218                 $scheme = $changes['uri']->getScheme();
219                 if (isset($standardPorts[$scheme]) && $port != $standardPorts[$scheme]) {
220                     $changes['set_headers']['Host'] .= ':'.$port;
221                 }
222             }
223         }
224         $uri = $changes['uri'];
225     }
226
227     if (!empty($changes['remove_headers'])) {
228         $headers = _caseless_remove($changes['remove_headers'], $headers);
229     }
230
231     if (!empty($changes['set_headers'])) {
232         $headers = _caseless_remove(array_keys($changes['set_headers']), $headers);
233         $headers = $changes['set_headers'] + $headers;
234     }
235
236     if (isset($changes['query'])) {
237         $uri = $uri->withQuery($changes['query']);
238     }
239
240     if ($request instanceof ServerRequestInterface) {
241         return new ServerRequest(
242             isset($changes['method']) ? $changes['method'] : $request->getMethod(),
243             $uri,
244             $headers,
245             isset($changes['body']) ? $changes['body'] : $request->getBody(),
246             isset($changes['version'])
247                 ? $changes['version']
248                 : $request->getProtocolVersion(),
249             $request->getServerParams()
250         );
251     }
252
253     return new Request(
254         isset($changes['method']) ? $changes['method'] : $request->getMethod(),
255         $uri,
256         $headers,
257         isset($changes['body']) ? $changes['body'] : $request->getBody(),
258         isset($changes['version'])
259             ? $changes['version']
260             : $request->getProtocolVersion()
261     );
262 }
263
264 /**
265  * Attempts to rewind a message body and throws an exception on failure.
266  *
267  * The body of the message will only be rewound if a call to `tell()` returns a
268  * value other than `0`.
269  *
270  * @param MessageInterface $message Message to rewind
271  *
272  * @throws \RuntimeException
273  */
274 function rewind_body(MessageInterface $message)
275 {
276     $body = $message->getBody();
277
278     if ($body->tell()) {
279         $body->rewind();
280     }
281 }
282
283 /**
284  * Safely opens a PHP stream resource using a filename.
285  *
286  * When fopen fails, PHP normally raises a warning. This function adds an
287  * error handler that checks for errors and throws an exception instead.
288  *
289  * @param string $filename File to open
290  * @param string $mode     Mode used to open the file
291  *
292  * @return resource
293  * @throws \RuntimeException if the file cannot be opened
294  */
295 function try_fopen($filename, $mode)
296 {
297     $ex = null;
298     set_error_handler(function () use ($filename, $mode, &$ex) {
299         $ex = new \RuntimeException(sprintf(
300             'Unable to open %s using mode %s: %s',
301             $filename,
302             $mode,
303             func_get_args()[1]
304         ));
305     });
306
307     $handle = fopen($filename, $mode);
308     restore_error_handler();
309
310     if ($ex) {
311         /** @var $ex \RuntimeException */
312         throw $ex;
313     }
314
315     return $handle;
316 }
317
318 /**
319  * Copy the contents of a stream into a string until the given number of
320  * bytes have been read.
321  *
322  * @param StreamInterface $stream Stream to read
323  * @param int             $maxLen Maximum number of bytes to read. Pass -1
324  *                                to read the entire stream.
325  * @return string
326  * @throws \RuntimeException on error.
327  */
328 function copy_to_string(StreamInterface $stream, $maxLen = -1)
329 {
330     $buffer = '';
331
332     if ($maxLen === -1) {
333         while (!$stream->eof()) {
334             $buf = $stream->read(1048576);
335             // Using a loose equality here to match on '' and false.
336             if ($buf == null) {
337                 break;
338             }
339             $buffer .= $buf;
340         }
341         return $buffer;
342     }
343
344     $len = 0;
345     while (!$stream->eof() && $len < $maxLen) {
346         $buf = $stream->read($maxLen - $len);
347         // Using a loose equality here to match on '' and false.
348         if ($buf == null) {
349             break;
350         }
351         $buffer .= $buf;
352         $len = strlen($buffer);
353     }
354
355     return $buffer;
356 }
357
358 /**
359  * Copy the contents of a stream into another stream until the given number
360  * of bytes have been read.
361  *
362  * @param StreamInterface $source Stream to read from
363  * @param StreamInterface $dest   Stream to write to
364  * @param int             $maxLen Maximum number of bytes to read. Pass -1
365  *                                to read the entire stream.
366  *
367  * @throws \RuntimeException on error.
368  */
369 function copy_to_stream(
370     StreamInterface $source,
371     StreamInterface $dest,
372     $maxLen = -1
373 ) {
374     $bufferSize = 8192;
375
376     if ($maxLen === -1) {
377         while (!$source->eof()) {
378             if (!$dest->write($source->read($bufferSize))) {
379                 break;
380             }
381         }
382     } else {
383         $remaining = $maxLen;
384         while ($remaining > 0 && !$source->eof()) {
385             $buf = $source->read(min($bufferSize, $remaining));
386             $len = strlen($buf);
387             if (!$len) {
388                 break;
389             }
390             $remaining -= $len;
391             $dest->write($buf);
392         }
393     }
394 }
395
396 /**
397  * Calculate a hash of a Stream
398  *
399  * @param StreamInterface $stream    Stream to calculate the hash for
400  * @param string          $algo      Hash algorithm (e.g. md5, crc32, etc)
401  * @param bool            $rawOutput Whether or not to use raw output
402  *
403  * @return string Returns the hash of the stream
404  * @throws \RuntimeException on error.
405  */
406 function hash(
407     StreamInterface $stream,
408     $algo,
409     $rawOutput = false
410 ) {
411     $pos = $stream->tell();
412
413     if ($pos > 0) {
414         $stream->rewind();
415     }
416
417     $ctx = hash_init($algo);
418     while (!$stream->eof()) {
419         hash_update($ctx, $stream->read(1048576));
420     }
421
422     $out = hash_final($ctx, (bool) $rawOutput);
423     $stream->seek($pos);
424
425     return $out;
426 }
427
428 /**
429  * Read a line from the stream up to the maximum allowed buffer length
430  *
431  * @param StreamInterface $stream    Stream to read from
432  * @param int             $maxLength Maximum buffer length
433  *
434  * @return string|bool
435  */
436 function readline(StreamInterface $stream, $maxLength = null)
437 {
438     $buffer = '';
439     $size = 0;
440
441     while (!$stream->eof()) {
442         // Using a loose equality here to match on '' and false.
443         if (null == ($byte = $stream->read(1))) {
444             return $buffer;
445         }
446         $buffer .= $byte;
447         // Break when a new line is found or the max length - 1 is reached
448         if ($byte === "\n" || ++$size === $maxLength - 1) {
449             break;
450         }
451     }
452
453     return $buffer;
454 }
455
456 /**
457  * Parses a request message string into a request object.
458  *
459  * @param string $message Request message string.
460  *
461  * @return Request
462  */
463 function parse_request($message)
464 {
465     $data = _parse_message($message);
466     $matches = [];
467     if (!preg_match('/^[\S]+\s+([a-zA-Z]+:\/\/|\/).*/', $data['start-line'], $matches)) {
468         throw new \InvalidArgumentException('Invalid request string');
469     }
470     $parts = explode(' ', $data['start-line'], 3);
471     $version = isset($parts[2]) ? explode('/', $parts[2])[1] : '1.1';
472
473     $request = new Request(
474         $parts[0],
475         $matches[1] === '/' ? _parse_request_uri($parts[1], $data['headers']) : $parts[1],
476         $data['headers'],
477         $data['body'],
478         $version
479     );
480
481     return $matches[1] === '/' ? $request : $request->withRequestTarget($parts[1]);
482 }
483
484 /**
485  * Parses a response message string into a response object.
486  *
487  * @param string $message Response message string.
488  *
489  * @return Response
490  */
491 function parse_response($message)
492 {
493     $data = _parse_message($message);
494     // According to https://tools.ietf.org/html/rfc7230#section-3.1.2 the space
495     // between status-code and reason-phrase is required. But browsers accept
496     // responses without space and reason as well.
497     if (!preg_match('/^HTTP\/.* [0-9]{3}( .*|$)/', $data['start-line'])) {
498         throw new \InvalidArgumentException('Invalid response string');
499     }
500     $parts = explode(' ', $data['start-line'], 3);
501
502     return new Response(
503         $parts[1],
504         $data['headers'],
505         $data['body'],
506         explode('/', $parts[0])[1],
507         isset($parts[2]) ? $parts[2] : null
508     );
509 }
510
511 /**
512  * Parse a query string into an associative array.
513  *
514  * If multiple values are found for the same key, the value of that key
515  * value pair will become an array. This function does not parse nested
516  * PHP style arrays into an associative array (e.g., foo[a]=1&foo[b]=2 will
517  * be parsed into ['foo[a]' => '1', 'foo[b]' => '2']).
518  *
519  * @param string      $str         Query string to parse
520  * @param bool|string $urlEncoding How the query string is encoded
521  *
522  * @return array
523  */
524 function parse_query($str, $urlEncoding = true)
525 {
526     $result = [];
527
528     if ($str === '') {
529         return $result;
530     }
531
532     if ($urlEncoding === true) {
533         $decoder = function ($value) {
534             return rawurldecode(str_replace('+', ' ', $value));
535         };
536     } elseif ($urlEncoding == PHP_QUERY_RFC3986) {
537         $decoder = 'rawurldecode';
538     } elseif ($urlEncoding == PHP_QUERY_RFC1738) {
539         $decoder = 'urldecode';
540     } else {
541         $decoder = function ($str) { return $str; };
542     }
543
544     foreach (explode('&', $str) as $kvp) {
545         $parts = explode('=', $kvp, 2);
546         $key = $decoder($parts[0]);
547         $value = isset($parts[1]) ? $decoder($parts[1]) : null;
548         if (!isset($result[$key])) {
549             $result[$key] = $value;
550         } else {
551             if (!is_array($result[$key])) {
552                 $result[$key] = [$result[$key]];
553             }
554             $result[$key][] = $value;
555         }
556     }
557
558     return $result;
559 }
560
561 /**
562  * Build a query string from an array of key value pairs.
563  *
564  * This function can use the return value of parse_query() to build a query
565  * string. This function does not modify the provided keys when an array is
566  * encountered (like http_build_query would).
567  *
568  * @param array     $params   Query string parameters.
569  * @param int|false $encoding Set to false to not encode, PHP_QUERY_RFC3986
570  *                            to encode using RFC3986, or PHP_QUERY_RFC1738
571  *                            to encode using RFC1738.
572  * @return string
573  */
574 function build_query(array $params, $encoding = PHP_QUERY_RFC3986)
575 {
576     if (!$params) {
577         return '';
578     }
579
580     if ($encoding === false) {
581         $encoder = function ($str) { return $str; };
582     } elseif ($encoding === PHP_QUERY_RFC3986) {
583         $encoder = 'rawurlencode';
584     } elseif ($encoding === PHP_QUERY_RFC1738) {
585         $encoder = 'urlencode';
586     } else {
587         throw new \InvalidArgumentException('Invalid type');
588     }
589
590     $qs = '';
591     foreach ($params as $k => $v) {
592         $k = $encoder($k);
593         if (!is_array($v)) {
594             $qs .= $k;
595             if ($v !== null) {
596                 $qs .= '=' . $encoder($v);
597             }
598             $qs .= '&';
599         } else {
600             foreach ($v as $vv) {
601                 $qs .= $k;
602                 if ($vv !== null) {
603                     $qs .= '=' . $encoder($vv);
604                 }
605                 $qs .= '&';
606             }
607         }
608     }
609
610     return $qs ? (string) substr($qs, 0, -1) : '';
611 }
612
613 /**
614  * Determines the mimetype of a file by looking at its extension.
615  *
616  * @param $filename
617  *
618  * @return null|string
619  */
620 function mimetype_from_filename($filename)
621 {
622     return mimetype_from_extension(pathinfo($filename, PATHINFO_EXTENSION));
623 }
624
625 /**
626  * Maps a file extensions to a mimetype.
627  *
628  * @param $extension string The file extension.
629  *
630  * @return string|null
631  * @link http://svn.apache.org/repos/asf/httpd/httpd/branches/1.3.x/conf/mime.types
632  */
633 function mimetype_from_extension($extension)
634 {
635     static $mimetypes = [
636         '7z' => 'application/x-7z-compressed',
637         'aac' => 'audio/x-aac',
638         'ai' => 'application/postscript',
639         'aif' => 'audio/x-aiff',
640         'asc' => 'text/plain',
641         'asf' => 'video/x-ms-asf',
642         'atom' => 'application/atom+xml',
643         'avi' => 'video/x-msvideo',
644         'bmp' => 'image/bmp',
645         'bz2' => 'application/x-bzip2',
646         'cer' => 'application/pkix-cert',
647         'crl' => 'application/pkix-crl',
648         'crt' => 'application/x-x509-ca-cert',
649         'css' => 'text/css',
650         'csv' => 'text/csv',
651         'cu' => 'application/cu-seeme',
652         'deb' => 'application/x-debian-package',
653         'doc' => 'application/msword',
654         'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
655         'dvi' => 'application/x-dvi',
656         'eot' => 'application/vnd.ms-fontobject',
657         'eps' => 'application/postscript',
658         'epub' => 'application/epub+zip',
659         'etx' => 'text/x-setext',
660         'flac' => 'audio/flac',
661         'flv' => 'video/x-flv',
662         'gif' => 'image/gif',
663         'gz' => 'application/gzip',
664         'htm' => 'text/html',
665         'html' => 'text/html',
666         'ico' => 'image/x-icon',
667         'ics' => 'text/calendar',
668         'ini' => 'text/plain',
669         'iso' => 'application/x-iso9660-image',
670         'jar' => 'application/java-archive',
671         'jpe' => 'image/jpeg',
672         'jpeg' => 'image/jpeg',
673         'jpg' => 'image/jpeg',
674         'js' => 'text/javascript',
675         'json' => 'application/json',
676         'latex' => 'application/x-latex',
677         'log' => 'text/plain',
678         'm4a' => 'audio/mp4',
679         'm4v' => 'video/mp4',
680         'mid' => 'audio/midi',
681         'midi' => 'audio/midi',
682         'mov' => 'video/quicktime',
683         'mp3' => 'audio/mpeg',
684         'mp4' => 'video/mp4',
685         'mp4a' => 'audio/mp4',
686         'mp4v' => 'video/mp4',
687         'mpe' => 'video/mpeg',
688         'mpeg' => 'video/mpeg',
689         'mpg' => 'video/mpeg',
690         'mpg4' => 'video/mp4',
691         'oga' => 'audio/ogg',
692         'ogg' => 'audio/ogg',
693         'ogv' => 'video/ogg',
694         'ogx' => 'application/ogg',
695         'pbm' => 'image/x-portable-bitmap',
696         'pdf' => 'application/pdf',
697         'pgm' => 'image/x-portable-graymap',
698         'png' => 'image/png',
699         'pnm' => 'image/x-portable-anymap',
700         'ppm' => 'image/x-portable-pixmap',
701         'ppt' => 'application/vnd.ms-powerpoint',
702         'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
703         'ps' => 'application/postscript',
704         'qt' => 'video/quicktime',
705         'rar' => 'application/x-rar-compressed',
706         'ras' => 'image/x-cmu-raster',
707         'rss' => 'application/rss+xml',
708         'rtf' => 'application/rtf',
709         'sgm' => 'text/sgml',
710         'sgml' => 'text/sgml',
711         'svg' => 'image/svg+xml',
712         'swf' => 'application/x-shockwave-flash',
713         'tar' => 'application/x-tar',
714         'tif' => 'image/tiff',
715         'tiff' => 'image/tiff',
716         'torrent' => 'application/x-bittorrent',
717         'ttf' => 'application/x-font-ttf',
718         'txt' => 'text/plain',
719         'wav' => 'audio/x-wav',
720         'webm' => 'video/webm',
721         'wma' => 'audio/x-ms-wma',
722         'wmv' => 'video/x-ms-wmv',
723         'woff' => 'application/x-font-woff',
724         'wsdl' => 'application/wsdl+xml',
725         'xbm' => 'image/x-xbitmap',
726         'xls' => 'application/vnd.ms-excel',
727         'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
728         'xml' => 'application/xml',
729         'xpm' => 'image/x-xpixmap',
730         'xwd' => 'image/x-xwindowdump',
731         'yaml' => 'text/yaml',
732         'yml' => 'text/yaml',
733         'zip' => 'application/zip',
734     ];
735
736     $extension = strtolower($extension);
737
738     return isset($mimetypes[$extension])
739         ? $mimetypes[$extension]
740         : null;
741 }
742
743 /**
744  * Parses an HTTP message into an associative array.
745  *
746  * The array contains the "start-line" key containing the start line of
747  * the message, "headers" key containing an associative array of header
748  * array values, and a "body" key containing the body of the message.
749  *
750  * @param string $message HTTP request or response to parse.
751  *
752  * @return array
753  * @internal
754  */
755 function _parse_message($message)
756 {
757     if (!$message) {
758         throw new \InvalidArgumentException('Invalid message');
759     }
760
761     // Iterate over each line in the message, accounting for line endings
762     $lines = preg_split('/(\\r?\\n)/', $message, -1, PREG_SPLIT_DELIM_CAPTURE);
763     $result = ['start-line' => array_shift($lines), 'headers' => [], 'body' => ''];
764     array_shift($lines);
765
766     for ($i = 0, $totalLines = count($lines); $i < $totalLines; $i += 2) {
767         $line = $lines[$i];
768         // If two line breaks were encountered, then this is the end of body
769         if (empty($line)) {
770             if ($i < $totalLines - 1) {
771                 $result['body'] = implode('', array_slice($lines, $i + 2));
772             }
773             break;
774         }
775         if (strpos($line, ':')) {
776             $parts = explode(':', $line, 2);
777             $key = trim($parts[0]);
778             $value = isset($parts[1]) ? trim($parts[1]) : '';
779             $result['headers'][$key][] = $value;
780         }
781     }
782
783     return $result;
784 }
785
786 /**
787  * Constructs a URI for an HTTP request message.
788  *
789  * @param string $path    Path from the start-line
790  * @param array  $headers Array of headers (each value an array).
791  *
792  * @return string
793  * @internal
794  */
795 function _parse_request_uri($path, array $headers)
796 {
797     $hostKey = array_filter(array_keys($headers), function ($k) {
798         return strtolower($k) === 'host';
799     });
800
801     // If no host is found, then a full URI cannot be constructed.
802     if (!$hostKey) {
803         return $path;
804     }
805
806     $host = $headers[reset($hostKey)][0];
807     $scheme = substr($host, -4) === ':443' ? 'https' : 'http';
808
809     return $scheme . '://' . $host . '/' . ltrim($path, '/');
810 }
811
812 /** @internal */
813 function _caseless_remove($keys, array $data)
814 {
815     $result = [];
816
817     foreach ($keys as &$key) {
818         $key = strtolower($key);
819     }
820
821     foreach ($data as $k => $v) {
822         if (!in_array(strtolower($k), $keys)) {
823             $result[$k] = $v;
824         }
825     }
826
827     return $result;
828 }