PluginProbe
StockPack – Stock photos and AI images from Unsplash, Adobe Stock, Freepik and more / 2.5.6
StockPack – Stock photos and AI images from Unsplash, Adobe Stock, Freepik and more v2.5.6
3.7.2 3.7.1 3.7.0 3.6.0 3.6.1 3.5.2 trunk 2.0 2.1 2.2 2.3 2.4 2.4.1 2.4.2 2.4.3 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 2.5.6 2.5.7 2.6.0 2.6.1 All 73 releases
stockpack / vendor / guzzlehttp / psr7 / src / functions.php

functions.php in StockPack – Stock photos and AI images from Unsplash, Adobe Stock, Freepik and more 2.5.6, at vendor/guzzlehttp/psr7/src/functions.php

900 lines 26.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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|\Iterator $resource Entity body data
73 * @param array $options Additional options
74 *
75 * @return StreamInterface
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 ->withParsedBody($request->getParsedBody())
252 ->withQueryParams($request->getQueryParams())
253 ->withCookieParams($request->getCookieParams())
254 ->withUploadedFiles($request->getUploadedFiles());
255 }
256
257 return new Request(
258 isset($changes['method']) ? $changes['method'] : $request->getMethod(),
259 $uri,
260 $headers,
261 isset($changes['body']) ? $changes['body'] : $request->getBody(),
262 isset($changes['version'])
263 ? $changes['version']
264 : $request->getProtocolVersion()
265 );
266 }
267
268 /**
269 * Attempts to rewind a message body and throws an exception on failure.
270 *
271 * The body of the message will only be rewound if a call to `tell()` returns a
272 * value other than `0`.
273 *
274 * @param MessageInterface $message Message to rewind
275 *
276 * @throws \RuntimeException
277 */
278 function rewind_body(MessageInterface $message)
279 {
280 $body = $message->getBody();
281
282 if ($body->tell()) {
283 $body->rewind();
284 }
285 }
286
287 /**
288 * Safely opens a PHP stream resource using a filename.
289 *
290 * When fopen fails, PHP normally raises a warning. This function adds an
291 * error handler that checks for errors and throws an exception instead.
292 *
293 * @param string $filename File to open
294 * @param string $mode Mode used to open the file
295 *
296 * @return resource
297 * @throws \RuntimeException if the file cannot be opened
298 */
299 function try_fopen($filename, $mode)
300 {
301 $ex = null;
302 set_error_handler(function () use ($filename, $mode, &$ex) {
303 $ex = new \RuntimeException(sprintf(
304 'Unable to open %s using mode %s: %s',
305 $filename,
306 $mode,
307 func_get_args()[1]
308 ));
309 });
310
311 $handle = fopen($filename, $mode);
312 restore_error_handler();
313
314 if ($ex) {
315 /** @var $ex \RuntimeException */
316 throw $ex;
317 }
318
319 return $handle;
320 }
321
322 /**
323 * Copy the contents of a stream into a string until the given number of
324 * bytes have been read.
325 *
326 * @param StreamInterface $stream Stream to read
327 * @param int $maxLen Maximum number of bytes to read. Pass -1
328 * to read the entire stream.
329 * @return string
330 * @throws \RuntimeException on error.
331 */
332 function copy_to_string(StreamInterface $stream, $maxLen = -1)
333 {
334 $buffer = '';
335
336 if ($maxLen === -1) {
337 while (!$stream->eof()) {
338 $buf = $stream->read(1048576);
339 // Using a loose equality here to match on '' and false.
340 if ($buf == null) {
341 break;
342 }
343 $buffer .= $buf;
344 }
345 return $buffer;
346 }
347
348 $len = 0;
349 while (!$stream->eof() && $len < $maxLen) {
350 $buf = $stream->read($maxLen - $len);
351 // Using a loose equality here to match on '' and false.
352 if ($buf == null) {
353 break;
354 }
355 $buffer .= $buf;
356 $len = strlen($buffer);
357 }
358
359 return $buffer;
360 }
361
362 /**
363 * Copy the contents of a stream into another stream until the given number
364 * of bytes have been read.
365 *
366 * @param StreamInterface $source Stream to read from
367 * @param StreamInterface $dest Stream to write to
368 * @param int $maxLen Maximum number of bytes to read. Pass -1
369 * to read the entire stream.
370 *
371 * @throws \RuntimeException on error.
372 */
373 function copy_to_stream(
374 StreamInterface $source,
375 StreamInterface $dest,
376 $maxLen = -1
377 ) {
378 $bufferSize = 8192;
379
380 if ($maxLen === -1) {
381 while (!$source->eof()) {
382 if (!$dest->write($source->read($bufferSize))) {
383 break;
384 }
385 }
386 } else {
387 $remaining = $maxLen;
388 while ($remaining > 0 && !$source->eof()) {
389 $buf = $source->read(min($bufferSize, $remaining));
390 $len = strlen($buf);
391 if (!$len) {
392 break;
393 }
394 $remaining -= $len;
395 $dest->write($buf);
396 }
397 }
398 }
399
400 /**
401 * Calculate a hash of a Stream
402 *
403 * @param StreamInterface $stream Stream to calculate the hash for
404 * @param string $algo Hash algorithm (e.g. md5, crc32, etc)
405 * @param bool $rawOutput Whether or not to use raw output
406 *
407 * @return string Returns the hash of the stream
408 * @throws \RuntimeException on error.
409 */
410 function hash(
411 StreamInterface $stream,
412 $algo,
413 $rawOutput = false
414 ) {
415 $pos = $stream->tell();
416
417 if ($pos > 0) {
418 $stream->rewind();
419 }
420
421 $ctx = hash_init($algo);
422 while (!$stream->eof()) {
423 hash_update($ctx, $stream->read(1048576));
424 }
425
426 $out = hash_final($ctx, (bool) $rawOutput);
427 $stream->seek($pos);
428
429 return $out;
430 }
431
432 /**
433 * Read a line from the stream up to the maximum allowed buffer length
434 *
435 * @param StreamInterface $stream Stream to read from
436 * @param int $maxLength Maximum buffer length
437 *
438 * @return string
439 */
440 function readline(StreamInterface $stream, $maxLength = null)
441 {
442 $buffer = '';
443 $size = 0;
444
445 while (!$stream->eof()) {
446 // Using a loose equality here to match on '' and false.
447 if (null == ($byte = $stream->read(1))) {
448 return $buffer;
449 }
450 $buffer .= $byte;
451 // Break when a new line is found or the max length - 1 is reached
452 if ($byte === "\n" || ++$size === $maxLength - 1) {
453 break;
454 }
455 }
456
457 return $buffer;
458 }
459
460 /**
461 * Parses a request message string into a request object.
462 *
463 * @param string $message Request message string.
464 *
465 * @return Request
466 */
467 function parse_request($message)
468 {
469 $data = _parse_message($message);
470 $matches = [];
471 if (!preg_match('/^[\S]+\s+([a-zA-Z]+:\/\/|\/).*/', $data['start-line'], $matches)) {
472 throw new \InvalidArgumentException('Invalid request string');
473 }
474 $parts = explode(' ', $data['start-line'], 3);
475 $version = isset($parts[2]) ? explode('/', $parts[2])[1] : '1.1';
476
477 $request = new Request(
478 $parts[0],
479 $matches[1] === '/' ? _parse_request_uri($parts[1], $data['headers']) : $parts[1],
480 $data['headers'],
481 $data['body'],
482 $version
483 );
484
485 return $matches[1] === '/' ? $request : $request->withRequestTarget($parts[1]);
486 }
487
488 /**
489 * Parses a response message string into a response object.
490 *
491 * @param string $message Response message string.
492 *
493 * @return Response
494 */
495 function parse_response($message)
496 {
497 $data = _parse_message($message);
498 // According to https://tools.ietf.org/html/rfc7230#section-3.1.2 the space
499 // between status-code and reason-phrase is required. But browsers accept
500 // responses without space and reason as well.
501 if (!preg_match('/^HTTP\/.* [0-9]{3}( .*|$)/', $data['start-line'])) {
502 throw new \InvalidArgumentException('Invalid response string: ' . $data['start-line']);
503 }
504 $parts = explode(' ', $data['start-line'], 3);
505
506 return new Response(
507 $parts[1],
508 $data['headers'],
509 $data['body'],
510 explode('/', $parts[0])[1],
511 isset($parts[2]) ? $parts[2] : null
512 );
513 }
514
515 /**
516 * Parse a query string into an associative array.
517 *
518 * If multiple values are found for the same key, the value of that key
519 * value pair will become an array. This function does not parse nested
520 * PHP style arrays into an associative array (e.g., foo[a]=1&foo[b]=2 will
521 * be parsed into ['foo[a]' => '1', 'foo[b]' => '2']).
522 *
523 * @param string $str Query string to parse
524 * @param int|bool $urlEncoding How the query string is encoded
525 *
526 * @return array
527 */
528 function parse_query($str, $urlEncoding = true)
529 {
530 $result = [];
531
532 if ($str === '') {
533 return $result;
534 }
535
536 if ($urlEncoding === true) {
537 $decoder = function ($value) {
538 return rawurldecode(str_replace('+', ' ', $value));
539 };
540 } elseif ($urlEncoding === PHP_QUERY_RFC3986) {
541 $decoder = 'rawurldecode';
542 } elseif ($urlEncoding === PHP_QUERY_RFC1738) {
543 $decoder = 'urldecode';
544 } else {
545 $decoder = function ($str) { return $str; };
546 }
547
548 foreach (explode('&', $str) as $kvp) {
549 $parts = explode('=', $kvp, 2);
550 $key = $decoder($parts[0]);
551 $value = isset($parts[1]) ? $decoder($parts[1]) : null;
552 if (!isset($result[$key])) {
553 $result[$key] = $value;
554 } else {
555 if (!is_array($result[$key])) {
556 $result[$key] = [$result[$key]];
557 }
558 $result[$key][] = $value;
559 }
560 }
561
562 return $result;
563 }
564
565 /**
566 * Build a query string from an array of key value pairs.
567 *
568 * This function can use the return value of parse_query() to build a query
569 * string. This function does not modify the provided keys when an array is
570 * encountered (like http_build_query would).
571 *
572 * @param array $params Query string parameters.
573 * @param int|false $encoding Set to false to not encode, PHP_QUERY_RFC3986
574 * to encode using RFC3986, or PHP_QUERY_RFC1738
575 * to encode using RFC1738.
576 * @return string
577 */
578 function build_query(array $params, $encoding = PHP_QUERY_RFC3986)
579 {
580 if (!$params) {
581 return '';
582 }
583
584 if ($encoding === false) {
585 $encoder = function ($str) { return $str; };
586 } elseif ($encoding === PHP_QUERY_RFC3986) {
587 $encoder = 'rawurlencode';
588 } elseif ($encoding === PHP_QUERY_RFC1738) {
589 $encoder = 'urlencode';
590 } else {
591 throw new \InvalidArgumentException('Invalid type');
592 }
593
594 $qs = '';
595 foreach ($params as $k => $v) {
596 $k = $encoder($k);
597 if (!is_array($v)) {
598 $qs .= $k;
599 if ($v !== null) {
600 $qs .= '=' . $encoder($v);
601 }
602 $qs .= '&';
603 } else {
604 foreach ($v as $vv) {
605 $qs .= $k;
606 if ($vv !== null) {
607 $qs .= '=' . $encoder($vv);
608 }
609 $qs .= '&';
610 }
611 }
612 }
613
614 return $qs ? (string) substr($qs, 0, -1) : '';
615 }
616
617 /**
618 * Determines the mimetype of a file by looking at its extension.
619 *
620 * @param $filename
621 *
622 * @return null|string
623 */
624 function mimetype_from_filename($filename)
625 {
626 return mimetype_from_extension(pathinfo($filename, PATHINFO_EXTENSION));
627 }
628
629 /**
630 * Maps a file extensions to a mimetype.
631 *
632 * @param $extension string The file extension.
633 *
634 * @return string|null
635 * @link http://svn.apache.org/repos/asf/httpd/httpd/branches/1.3.x/conf/mime.types
636 */
637 function mimetype_from_extension($extension)
638 {
639 static $mimetypes = [
640 '3gp' => 'video/3gpp',
641 '7z' => 'application/x-7z-compressed',
642 'aac' => 'audio/x-aac',
643 'ai' => 'application/postscript',
644 'aif' => 'audio/x-aiff',
645 'asc' => 'text/plain',
646 'asf' => 'video/x-ms-asf',
647 'atom' => 'application/atom+xml',
648 'avi' => 'video/x-msvideo',
649 'bmp' => 'image/bmp',
650 'bz2' => 'application/x-bzip2',
651 'cer' => 'application/pkix-cert',
652 'crl' => 'application/pkix-crl',
653 'crt' => 'application/x-x509-ca-cert',
654 'css' => 'text/css',
655 'csv' => 'text/csv',
656 'cu' => 'application/cu-seeme',
657 'deb' => 'application/x-debian-package',
658 'doc' => 'application/msword',
659 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
660 'dvi' => 'application/x-dvi',
661 'eot' => 'application/vnd.ms-fontobject',
662 'eps' => 'application/postscript',
663 'epub' => 'application/epub+zip',
664 'etx' => 'text/x-setext',
665 'flac' => 'audio/flac',
666 'flv' => 'video/x-flv',
667 'gif' => 'image/gif',
668 'gz' => 'application/gzip',
669 'htm' => 'text/html',
670 'html' => 'text/html',
671 'ico' => 'image/x-icon',
672 'ics' => 'text/calendar',
673 'ini' => 'text/plain',
674 'iso' => 'application/x-iso9660-image',
675 'jar' => 'application/java-archive',
676 'jpe' => 'image/jpeg',
677 'jpeg' => 'image/jpeg',
678 'jpg' => 'image/jpeg',
679 'js' => 'text/javascript',
680 'json' => 'application/json',
681 'latex' => 'application/x-latex',
682 'log' => 'text/plain',
683 'm4a' => 'audio/mp4',
684 'm4v' => 'video/mp4',
685 'mid' => 'audio/midi',
686 'midi' => 'audio/midi',
687 'mov' => 'video/quicktime',
688 'mkv' => 'video/x-matroska',
689 'mp3' => 'audio/mpeg',
690 'mp4' => 'video/mp4',
691 'mp4a' => 'audio/mp4',
692 'mp4v' => 'video/mp4',
693 'mpe' => 'video/mpeg',
694 'mpeg' => 'video/mpeg',
695 'mpg' => 'video/mpeg',
696 'mpg4' => 'video/mp4',
697 'oga' => 'audio/ogg',
698 'ogg' => 'audio/ogg',
699 'ogv' => 'video/ogg',
700 'ogx' => 'application/ogg',
701 'pbm' => 'image/x-portable-bitmap',
702 'pdf' => 'application/pdf',
703 'pgm' => 'image/x-portable-graymap',
704 'png' => 'image/png',
705 'pnm' => 'image/x-portable-anymap',
706 'ppm' => 'image/x-portable-pixmap',
707 'ppt' => 'application/vnd.ms-powerpoint',
708 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
709 'ps' => 'application/postscript',
710 'qt' => 'video/quicktime',
711 'rar' => 'application/x-rar-compressed',
712 'ras' => 'image/x-cmu-raster',
713 'rss' => 'application/rss+xml',
714 'rtf' => 'application/rtf',
715 'sgm' => 'text/sgml',
716 'sgml' => 'text/sgml',
717 'svg' => 'image/svg+xml',
718 'swf' => 'application/x-shockwave-flash',
719 'tar' => 'application/x-tar',
720 'tif' => 'image/tiff',
721 'tiff' => 'image/tiff',
722 'torrent' => 'application/x-bittorrent',
723 'ttf' => 'application/x-font-ttf',
724 'txt' => 'text/plain',
725 'wav' => 'audio/x-wav',
726 'webm' => 'video/webm',
727 'webp' => 'image/webp',
728 'wma' => 'audio/x-ms-wma',
729 'wmv' => 'video/x-ms-wmv',
730 'woff' => 'application/x-font-woff',
731 'wsdl' => 'application/wsdl+xml',
732 'xbm' => 'image/x-xbitmap',
733 'xls' => 'application/vnd.ms-excel',
734 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
735 'xml' => 'application/xml',
736 'xpm' => 'image/x-xpixmap',
737 'xwd' => 'image/x-xwindowdump',
738 'yaml' => 'text/yaml',
739 'yml' => 'text/yaml',
740 'zip' => 'application/zip',
741 ];
742
743 $extension = strtolower($extension);
744
745 return isset($mimetypes[$extension])
746 ? $mimetypes[$extension]
747 : null;
748 }
749
750 /**
751 * Parses an HTTP message into an associative array.
752 *
753 * The array contains the "start-line" key containing the start line of
754 * the message, "headers" key containing an associative array of header
755 * array values, and a "body" key containing the body of the message.
756 *
757 * @param string $message HTTP request or response to parse.
758 *
759 * @return array
760 * @internal
761 */
762 function _parse_message($message)
763 {
764 if (!$message) {
765 throw new \InvalidArgumentException('Invalid message');
766 }
767
768 $message = ltrim($message, "\r\n");
769
770 $messageParts = preg_split("/\r?\n\r?\n/", $message, 2);
771
772 if ($messageParts === false || count($messageParts) !== 2) {
773 throw new \InvalidArgumentException('Invalid message: Missing header delimiter');
774 }
775
776 list($rawHeaders, $body) = $messageParts;
777 $rawHeaders .= "\r\n"; // Put back the delimiter we split previously
778 $headerParts = preg_split("/\r?\n/", $rawHeaders, 2);
779
780 if ($headerParts === false || count($headerParts) !== 2) {
781 throw new \InvalidArgumentException('Invalid message: Missing status line');
782 }
783
784 list($startLine, $rawHeaders) = $headerParts;
785
786 if (preg_match("/(?:^HTTP\/|^[A-Z]+ \S+ HTTP\/)(\d+(?:\.\d+)?)/i", $startLine, $matches) && $matches[1] === '1.0') {
787 // Header folding is deprecated for HTTP/1.1, but allowed in HTTP/1.0
788 $rawHeaders = preg_replace(Rfc7230::HEADER_FOLD_REGEX, ' ', $rawHeaders);
789 }
790
791 /** @var array[] $headerLines */
792 $count = preg_match_all(Rfc7230::HEADER_REGEX, $rawHeaders, $headerLines, PREG_SET_ORDER);
793
794 // If these aren't the same, then one line didn't match and there's an invalid header.
795 if ($count !== substr_count($rawHeaders, "\n")) {
796 // Folding is deprecated, see https://tools.ietf.org/html/rfc7230#section-3.2.4
797 if (preg_match(Rfc7230::HEADER_FOLD_REGEX, $rawHeaders)) {
798 throw new \InvalidArgumentException('Invalid header syntax: Obsolete line folding');
799 }
800
801 throw new \InvalidArgumentException('Invalid header syntax');
802 }
803
804 $headers = [];
805
806 foreach ($headerLines as $headerLine) {
807 $headers[$headerLine[1]][] = $headerLine[2];
808 }
809
810 return [
811 'start-line' => $startLine,
812 'headers' => $headers,
813 'body' => $body,
814 ];
815 }
816
817 /**
818 * Constructs a URI for an HTTP request message.
819 *
820 * @param string $path Path from the start-line
821 * @param array $headers Array of headers (each value an array).
822 *
823 * @return string
824 * @internal
825 */
826 function _parse_request_uri($path, array $headers)
827 {
828 $hostKey = array_filter(array_keys($headers), function ($k) {
829 return strtolower($k) === 'host';
830 });
831
832 // If no host is found, then a full URI cannot be constructed.
833 if (!$hostKey) {
834 return $path;
835 }
836
837 $host = $headers[reset($hostKey)][0];
838 $scheme = substr($host, -4) === ':443' ? 'https' : 'http';
839
840 return $scheme . '://' . $host . '/' . ltrim($path, '/');
841 }
842
843 /**
844 * Get a short summary of the message body
845 *
846 * Will return `null` if the response is not printable.
847 *
848 * @param MessageInterface $message The message to get the body summary
849 * @param int $truncateAt The maximum allowed size of the summary
850 *
851 * @return null|string
852 */
853 function get_message_body_summary(MessageInterface $message, $truncateAt = 120)
854 {
855 $body = $message->getBody();
856
857 if (!$body->isSeekable() || !$body->isReadable()) {
858 return null;
859 }
860
861 $size = $body->getSize();
862
863 if ($size === 0) {
864 return null;
865 }
866
867 $summary = $body->read($truncateAt);
868 $body->rewind();
869
870 if ($size > $truncateAt) {
871 $summary .= ' (truncated...)';
872 }
873
874 // Matches any printable character, including unicode characters:
875 // letters, marks, numbers, punctuation, spacing, and separators.
876 if (preg_match('/[^\pL\pM\pN\pP\pS\pZ\n\r\t]/', $summary)) {
877 return null;
878 }
879
880 return $summary;
881 }
882
883 /** @internal */
884 function _caseless_remove($keys, array $data)
885 {
886 $result = [];
887
888 foreach ($keys as &$key) {
889 $key = strtolower($key);
890 }
891
892 foreach ($data as $k => $v) {
893 if (!in_array(strtolower($k), $keys)) {
894 $result[$k] = $v;
895 }
896 }
897
898 return $result;
899 }
900