PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.20
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.20
1.10.20 1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 All 164 releases
woocommerce-pos / vendor_prefixed / guzzlehttp / psr7 / src / Utils.php

Utils.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.10.20, at vendor_prefixed/guzzlehttp/psr7/src/Utils.php

742 lines 31.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare (strict_types=1);
4 namespace WCPOS\Vendor\GuzzleHttp\Psr7;
5
6 use WCPOS\Vendor\GuzzleHttp\Psr7\Exception\TimeoutException;
7 use WCPOS\Vendor\Psr\Http\Message\RequestInterface;
8 use WCPOS\Vendor\Psr\Http\Message\StreamInterface;
9 use WCPOS\Vendor\Psr\Http\Message\UriInterface;
10 final class Utils
11 {
12 private function __construct()
13 {
14 }
15 /**
16 * Converts ASCII uppercase letters in a string to lowercase.
17 *
18 * Unlike strtolower(), which honors LC_CTYPE before PHP 8.2, the
19 * conversion is locale-independent and leaves every non-ASCII byte
20 * unchanged, as HTTP protocol elements require.
21 */
22 public static function asciiToLower(string $string) : string
23 {
24 return \strtr($string, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
25 }
26 /**
27 * Converts ASCII lowercase letters in a string to uppercase.
28 *
29 * Unlike strtoupper(), which honors LC_CTYPE before PHP 8.2, the
30 * conversion is locale-independent and leaves every non-ASCII byte
31 * unchanged, as HTTP protocol elements require.
32 */
33 public static function asciiToUpper(string $string) : string
34 {
35 return \strtr($string, 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
36 }
37 /**
38 * Converts the first character of a string to uppercase when it is an
39 * ASCII lowercase letter.
40 *
41 * Unlike ucfirst(), which honors LC_CTYPE before PHP 8.2, the conversion
42 * is locale-independent and leaves every non-ASCII byte unchanged, as
43 * HTTP protocol elements require.
44 */
45 public static function asciiUcFirst(string $string) : string
46 {
47 if ($string === '') {
48 return '';
49 }
50 return self::asciiToUpper($string[0]) . \substr($string, 1);
51 }
52 /**
53 * Checks whether the haystack contains the needle, comparing ASCII
54 * letters case-insensitively and without locale sensitivity.
55 */
56 public static function caselessContains(string $haystack, string $needle) : bool
57 {
58 return \str_contains(self::asciiToLower($haystack), self::asciiToLower($needle));
59 }
60 /**
61 * Checks whether two strings are equal, comparing ASCII letters
62 * case-insensitively and without locale sensitivity.
63 */
64 public static function caselessEquals(string $left, string $right) : bool
65 {
66 return self::asciiToLower($left) === self::asciiToLower($right);
67 }
68 /**
69 * Remove the items given by the keys from the data, case-insensitively.
70 *
71 * @param array<array-key, string|int> $keys
72 */
73 public static function caselessRemove(array $keys, array $data) : array
74 {
75 $result = [];
76 foreach ($keys as &$key) {
77 $key = self::asciiToLower((string) $key);
78 }
79 foreach ($data as $k => $v) {
80 if (!\in_array(self::asciiToLower((string) $k), $keys)) {
81 $result[$k] = $v;
82 }
83 }
84 return $result;
85 }
86 /**
87 * Copy the contents of a stream into another stream until the given number
88 * of bytes have been read, returning the number of bytes copied as an
89 * `int`. On 32-bit PHP, an unbounded copy larger than `PHP_INT_MAX` bytes
90 * cannot be represented by that return type. 64-bit PHP is not affected.
91 *
92 * The destination must accept writes that make positive progress. Streams
93 * that return 0 as a backpressure or drop signal (a `BufferStream` at its
94 * high water mark, or a full `DroppingStream`) will cause this method to
95 * throw. For full copies, use a normal writable stream such as a file or
96 * `php://temp` stream.
97 *
98 * Throws `TimeoutException` when PHP-style timeout metadata can be detected
99 * after a source read or destination write cannot make progress.
100 *
101 * @param StreamInterface $source Stream to read from
102 * @param StreamInterface $dest Stream to write to
103 * @param int $maxLen Maximum number of bytes to read. Pass -1
104 * to read the entire stream.
105 *
106 * @throws \RuntimeException on error.
107 */
108 public static function copyToStream(StreamInterface $source, StreamInterface $dest, int $maxLen = -1) : int
109 {
110 $bufferSize = 8192;
111 $copied = 0;
112 if ($maxLen === -1) {
113 while (!$source->eof()) {
114 $buf = StreamTimeout::read($source, $bufferSize, 'Unable to read from stream: timed out');
115 if ($buf === '') {
116 break;
117 }
118 self::writeAll($dest, $buf);
119 $copied = Integers::add($copied, \strlen($buf));
120 }
121 } else {
122 $remaining = $maxLen;
123 while ($remaining > 0 && !$source->eof()) {
124 $buf = StreamTimeout::read($source, \min($bufferSize, $remaining), 'Unable to read from stream: timed out');
125 $len = \strlen($buf);
126 if (!$len) {
127 break;
128 }
129 $remaining -= $len;
130 self::writeAll($dest, $buf);
131 $copied = Integers::add($copied, $len);
132 }
133 }
134 return $copied;
135 }
136 private static function writeAll(StreamInterface $dest, string $buf) : void
137 {
138 $written = 0;
139 $len = \strlen($buf);
140 while ($written < $len) {
141 try {
142 $result = $dest->write(\substr($buf, $written));
143 } catch (TimeoutException $e) {
144 throw $e;
145 } catch (\RuntimeException $e) {
146 StreamTimeout::throwIfWriteTimedOut($dest, $e);
147 throw $e;
148 }
149 if ($result <= 0) {
150 StreamTimeout::throwIfWriteTimedOut($dest);
151 throw new \RuntimeException('Unable to write to stream');
152 }
153 $written += $result;
154 }
155 }
156 /**
157 * Copy the contents of a stream into a string until the given number of
158 * bytes have been read.
159 *
160 * Throws `TimeoutException` when PHP-style timeout metadata can be detected
161 * after a stream read cannot make progress.
162 *
163 * @param StreamInterface $stream Stream to read
164 * @param int $maxLen Maximum number of bytes to read. Pass -1
165 * to read the entire stream.
166 *
167 * @throws \RuntimeException on error.
168 */
169 public static function copyToString(StreamInterface $stream, int $maxLen = -1) : string
170 {
171 $buffer = '';
172 if ($maxLen === -1) {
173 while (!$stream->eof()) {
174 $buf = StreamTimeout::read($stream, 1048576, 'Unable to read from stream: timed out');
175 if ($buf === '') {
176 break;
177 }
178 $buffer .= $buf;
179 }
180 return $buffer;
181 }
182 $len = 0;
183 while (!$stream->eof() && $len < $maxLen) {
184 $buf = StreamTimeout::read($stream, $maxLen - $len, 'Unable to read from stream: timed out');
185 if ($buf === '') {
186 break;
187 }
188 $buffer .= $buf;
189 $len = \strlen($buffer);
190 }
191 return $buffer;
192 }
193 /**
194 * Calculate a hash of a stream.
195 *
196 * This method reads the entire stream to calculate a rolling hash, based on
197 * PHP's `hash_init` functions.
198 *
199 * Throws `TimeoutException` when PHP-style timeout metadata can be detected
200 * after a stream read cannot make progress.
201 *
202 * @param StreamInterface $stream Stream to calculate the hash for
203 * @param string $algo Hash algorithm (e.g. md5, crc32, etc)
204 * @param bool $rawOutput Whether or not to use raw output
205 *
206 * @throws \RuntimeException on error.
207 */
208 public static function hash(StreamInterface $stream, string $algo, bool $rawOutput = \false) : string
209 {
210 $pos = $stream->tell();
211 if ($pos > 0) {
212 $stream->rewind();
213 }
214 $ctx = \hash_init($algo);
215 while (!$stream->eof()) {
216 $buf = StreamTimeout::read($stream, 1048576, 'Unable to calculate stream hash: timed out');
217 if ($buf === '') {
218 break;
219 }
220 \hash_update($ctx, $buf);
221 }
222 $out = \hash_final($ctx, $rawOutput);
223 $stream->seek($pos);
224 return $out;
225 }
226 /**
227 * Clone and modify a request with the given changes.
228 *
229 * This method is useful for reducing the number of clones needed to mutate
230 * a message.
231 *
232 * The changes can be one of:
233 * - method: (string) Changes the HTTP method.
234 * - set_headers: (array) Sets the given headers. Values must be strings
235 * or non-empty arrays of strings.
236 * - remove_headers: (array) Remove the given headers. Values may be
237 * strings or integers.
238 * - body: (mixed) Sets the given body. Present non-null values are
239 * converted with self::streamFor(), including resources, streams,
240 * iterators, callable arrays, closures, invokable objects, and stringable
241 * objects. String inputs remain literal bodies.
242 * - uri: (UriInterface) Set the URI. When the URI contains a host, the
243 * Host header is updated from it, and combining this with an explicit
244 * Host entry in set_headers throws an InvalidArgumentException. Apply
245 * an intentional Host override separately with withHeader() afterwards.
246 * - query: (string) Set the query string value of the URI.
247 * - version: (string) Set the protocol version.
248 *
249 * @param RequestInterface $request Request to clone and modify.
250 * @param array{
251 * method?: string,
252 * set_headers?: array<array-key, string|non-empty-array<array-key, string>>,
253 * remove_headers?: array<array-key, string|int>,
254 * body?: resource|string|StreamInterface|callable|\Iterator|\Stringable,
255 * uri?: UriInterface,
256 * query?: string,
257 * version?: string
258 * } $changes Changes to apply.
259 */
260 public static function modifyRequest(RequestInterface $request, array $changes) : RequestInterface
261 {
262 if (!$changes) {
263 return $request;
264 }
265 self::assertValidModifyRequestChanges($changes);
266 $headers = $request->getHeaders();
267 if (!isset($changes['uri'])) {
268 $uri = $request->getUri();
269 } else {
270 /** @var UriInterface */
271 $uri = $changes['uri'];
272 $host = $uri->getHost();
273 if ($host !== '') {
274 Uri::assertValidHost($host);
275 if (isset($changes['set_headers']) && \is_array($changes['set_headers'])) {
276 foreach (\array_keys($changes['set_headers']) as $header) {
277 if (self::asciiToLower((string) $header) === 'host') {
278 throw new \InvalidArgumentException('Cannot modify request with both a URI containing a host and an explicit Host header.');
279 }
280 }
281 }
282 $changes['set_headers']['Host'] = $host;
283 $port = $uri->getPort();
284 if ($port !== null) {
285 $standardPorts = ['http' => 80, 'https' => 443];
286 $scheme = $uri->getScheme();
287 if (!isset($standardPorts[$scheme]) || $port != $standardPorts[$scheme]) {
288 $changes['set_headers']['Host'] .= ':' . $port;
289 }
290 }
291 }
292 }
293 if (!empty($changes['remove_headers'])) {
294 $headers = self::caselessRemove($changes['remove_headers'], $headers);
295 }
296 if (!empty($changes['set_headers'])) {
297 $headers = self::caselessRemove(\array_keys($changes['set_headers']), $headers);
298 $headers = $changes['set_headers'] + $headers;
299 }
300 if (isset($changes['query'])) {
301 $uri = $uri->withQuery($changes['query']);
302 }
303 $hasHost = \false;
304 foreach (\array_keys($headers) as $header) {
305 if (self::asciiToLower((string) $header) === 'host') {
306 $hasHost = \true;
307 break;
308 }
309 }
310 // Match Request::__construct() by adding a Host header when one is not provided.
311 if (!$hasHost && $uri->getHost() !== '') {
312 $host = $uri->getHost();
313 Uri::assertValidHost($host);
314 if (($port = $uri->getPort()) !== null) {
315 $host .= ':' . $port;
316 }
317 $headers = ['Host' => [$host]] + $headers;
318 }
319 $new = $request;
320 if (isset($changes['method'])) {
321 $new = $new->withMethod($changes['method']);
322 }
323 if (isset($changes['uri']) || isset($changes['query'])) {
324 $new = $new->withUri($uri, \true);
325 }
326 if ($headers !== $new->getHeaders()) {
327 foreach (\array_keys($new->getHeaders()) as $header) {
328 /** @var RequestInterface */
329 $new = $new->withoutHeader((string) $header);
330 }
331 $addedHeaders = [];
332 foreach ($headers as $header => $value) {
333 $header = (string) $header;
334 $normalized = self::asciiToLower($header);
335 if (isset($addedHeaders[$normalized])) {
336 /** @var RequestInterface */
337 $new = $new->withAddedHeader($addedHeaders[$normalized], $value);
338 } else {
339 /** @var RequestInterface */
340 $new = $new->withHeader($header, $value);
341 $addedHeaders[$normalized] = $header;
342 }
343 }
344 }
345 if (isset($changes['body'])) {
346 /** @var RequestInterface */
347 $new = $new->withBody(self::streamFor($changes['body']));
348 }
349 if (isset($changes['version'])) {
350 /** @var RequestInterface */
351 $new = $new->withProtocolVersion($changes['version']);
352 }
353 return $new;
354 }
355 /**
356 * @param array<array-key, mixed> $changes
357 */
358 private static function assertValidModifyRequestChanges(array $changes) : void
359 {
360 foreach (['method', 'query', 'version'] as $key) {
361 if (\array_key_exists($key, $changes) && !\is_string($changes[$key])) {
362 self::assertValidModifyRequestChange($key, 'string', $changes[$key]);
363 }
364 }
365 if (\array_key_exists('uri', $changes) && !$changes['uri'] instanceof UriInterface) {
366 self::assertValidModifyRequestChange('uri', 'UriInterface', $changes['uri']);
367 }
368 if (\array_key_exists('body', $changes) && $changes['body'] === null) {
369 self::assertValidModifyRequestChange('body', 'resource|string|StreamInterface|callable|\\Iterator|\\Stringable', $changes['body']);
370 }
371 if (\array_key_exists('set_headers', $changes)) {
372 if (!\is_array($changes['set_headers'])) {
373 self::assertValidModifyRequestChange('set_headers', 'array<array-key, string|non-empty-array<array-key, string>>', $changes['set_headers']);
374 } else {
375 foreach ($changes['set_headers'] as $header => $value) {
376 $headerPath = \sprintf('set_headers.%s', (string) $header);
377 if (\is_array($value)) {
378 if ($value === []) {
379 self::assertValidModifyRequestChange($headerPath, 'string|non-empty-array<array-key, string>', $value);
380 break;
381 }
382 foreach ($value as $index => $item) {
383 if (!\is_string($item)) {
384 self::assertValidModifyRequestChange(\sprintf('%s.%s', $headerPath, (string) $index), 'string', $item);
385 break 2;
386 }
387 }
388 } elseif (!\is_string($value)) {
389 self::assertValidModifyRequestChange($headerPath, 'string|non-empty-array<array-key, string>', $value);
390 break;
391 }
392 }
393 }
394 }
395 if (!\array_key_exists('remove_headers', $changes)) {
396 return;
397 }
398 if (!\is_array($changes['remove_headers'])) {
399 self::assertValidModifyRequestChange('remove_headers', 'array<array-key, string|int>', $changes['remove_headers']);
400 return;
401 }
402 foreach ($changes['remove_headers'] as $index => $header) {
403 if (!\is_string($header) && !\is_int($header)) {
404 self::assertValidModifyRequestChange(\sprintf('remove_headers.%s', (string) $index), 'string|int', $header);
405 return;
406 }
407 }
408 }
409 /**
410 * @param mixed $value
411 */
412 private static function assertValidModifyRequestChange(string $key, string $expected, $value) : void
413 {
414 throw new \InvalidArgumentException(\sprintf('Utils::modifyRequest() change "%s" must be %s; %s provided.', DiagnosticValue::escape($key), $expected, \get_debug_type($value)));
415 }
416 /**
417 * Read a line from the stream up to the maximum allowed buffer length.
418 *
419 * Throws `TimeoutException` when PHP-style timeout metadata can be detected
420 * after a stream read cannot make progress.
421 *
422 * @param StreamInterface $stream Stream to read from
423 * @param int|null $maxLength Maximum buffer length
424 */
425 public static function readLine(StreamInterface $stream, ?int $maxLength = null) : string
426 {
427 $buffer = '';
428 $size = 0;
429 while (!$stream->eof()) {
430 if ('' === ($byte = StreamTimeout::read($stream, 1, 'Unable to read line from stream: timed out'))) {
431 return $buffer;
432 }
433 $buffer .= $byte;
434 // Break when a new line is found or the max length - 1 is reached
435 if ($byte === "\n" || ++$size === $maxLength - 1) {
436 break;
437 }
438 }
439 return $buffer;
440 }
441 /**
442 * Redact the user info part of a URI.
443 *
444 * Returns the URI with the whole userinfo component replaced by "***"
445 * when one is present, so neither the username nor the password survives
446 * into logs and diagnostics. A URI without userinfo is returned
447 * unchanged.
448 */
449 public static function redactUserInfo(#[\SensitiveParameter] UriInterface $uri) : UriInterface
450 {
451 return $uri->getUserInfo() === '' ? $uri : $uri->withUserInfo('***');
452 }
453 /**
454 * Redacts the userinfo of a raw URI string wherever it appears in a
455 * subject string.
456 *
457 * The needle is taken verbatim from the raw URI rather than from parsed
458 * components, so credentials that URI normalization would rewrite, such
459 * as raw control bytes or unencoded reserved characters, are still found
460 * in text that embeds the URI exactly as given, for example transport
461 * error messages. A URI without "://" is treated as authority-form: a
462 * host and port with optional userinfo.
463 *
464 * A URI that does not parse has no trustworthy authority boundary, so
465 * everything between any scheme and its last "@" is redacted as a safe-side
466 * fallback.
467 *
468 * @param string $subject Text that may embed the URI
469 * @param string $uri Raw URI whose userinfo is redacted in the text
470 */
471 public static function redactUserInfoInString(string $subject, string $uri) : string
472 {
473 if (\strpos($uri, '@') === \false) {
474 return $subject;
475 }
476 $schemePosition = \strpos($uri, '://');
477 $remainder = $schemePosition === \false ? $uri : \substr($uri, $schemePosition + 3);
478 if (\parse_url($schemePosition === \false ? 'http://' . $uri : $uri) === \false) {
479 // Raw '/', '?', or '#' separators may sit inside the credentials
480 // of a URI that defeats parse_url(), so the redaction cannot stop
481 // at the apparent authority.
482 $atPosition = \strrpos($remainder, '@');
483 if ($atPosition === \false || $atPosition === 0) {
484 return $subject;
485 }
486 return \str_replace(\substr($remainder, 0, $atPosition) . '@', '***@', $subject);
487 }
488 $authority = \substr($remainder, 0, \strcspn($remainder, '/?#'));
489 $atPosition = \strrpos($authority, '@');
490 if ($atPosition === \false || $atPosition === 0) {
491 // A parseable URI with '@' only past its authority, or with an
492 // empty userinfo, carries no credentials to redact.
493 return $subject;
494 }
495 return \str_replace(\substr($authority, 0, $atPosition) . '@', '***@', $subject);
496 }
497 /**
498 * Formats a URI for automatic diagnostics.
499 *
500 * The whole userinfo component is replaced by "***", and the query and
501 * fragment are removed. The scheme, host, port, and path are preserved.
502 * The returned string is diagnostic-escaped and the formatter never
503 * throws.
504 */
505 public static function redactUriForMessage(#[\SensitiveParameter] UriInterface $uri) : string
506 {
507 try {
508 $raw = (string) $uri;
509 } catch (\Throwable $e) {
510 return '[unavailable URI]';
511 }
512 try {
513 if ($uri->getUserInfo() !== '') {
514 $uri = $uri->withUserInfo('***');
515 }
516 return DiagnosticValue::escape((string) $uri->withQuery('')->withFragment(''));
517 } catch (\Throwable $e) {
518 return self::redactUriStringForMessage($raw);
519 }
520 }
521 /**
522 * Formats a raw URI for automatic diagnostics, including malformed input.
523 *
524 * The whole userinfo component is replaced by "***", and the query and
525 * fragment are removed. The scheme, host, port, and path are preserved
526 * where their boundaries can be determined safely. The returned string is
527 * diagnostic-escaped and the formatter never throws.
528 */
529 public static function redactUriStringForMessage(#[\SensitiveParameter] string $uri) : string
530 {
531 try {
532 $uri = self::redactUserInfoInString($uri, $uri);
533 return DiagnosticValue::escape(\substr($uri, 0, \strcspn($uri, '?#')));
534 } catch (\Throwable $e) {
535 return '[unavailable URI]';
536 }
537 }
538 /**
539 * Create a new stream based on the input type.
540 *
541 * Options are provided as an associative array that can contain the
542 * following keys:
543 * - metadata: Array of custom metadata.
544 * - size: Size of the stream.
545 *
546 * This method accepts the following `$resource` types:
547 * - `Psr\Http\Message\StreamInterface`: Returns the value as-is.
548 * - `string`: Creates a stream object that uses the given string as the
549 * contents.
550 * - `resource`: Creates a stream object that wraps the given PHP stream
551 * resource.
552 * - `Iterator`: If the provided value implements `Iterator`, then a
553 * read-only stream object will be created that wraps the given iterable.
554 * Each time the stream is read from, data from the iterator will fill a
555 * buffer and will be continuously called until the buffer is equal to the
556 * requested read size. Yielded strings, integers, finite floats,
557 * booleans, `null`, and stringable objects are converted to string
558 * chunks; non-finite floats and other values throw
559 * `UnexpectedValueException` when the stream is read. Values that
560 * stringify to an empty string are skipped while the iterator advances.
561 * Subsequent read calls will first read from the buffer and then call
562 * `next` on the underlying iterator until it is exhausted.
563 * - `object` with `__toString()`: If the object has the `__toString()`
564 * method, the object will be cast to a string and then a stream will be
565 * returned that uses the string value.
566 * - `NULL`: When `null` is passed, an empty stream object is returned.
567 * - `callable`: When a callable array, closure, or invokable object is
568 * passed and no earlier resource or object rule applies, a read-only
569 * stream object will be created that invokes the given callable. The
570 * callable is invoked with the suggested number of bytes to read. The
571 * callable can return fewer or more bytes than requested, but MUST return
572 * a non-empty string to provide data and MUST return `false` or `null`
573 * when there is no more data to return. Any additional bytes will be
574 * buffered and used in subsequent reads. String inputs are always treated
575 * as string bodies, even when they name callable functions.
576 *
577 * @param resource|string|StreamInterface|callable|\Iterator|\Stringable|null $resource Entity body data
578 * @param array{size?: int, metadata?: array} $options Additional options
579 *
580 * @throws \InvalidArgumentException if the $resource arg is not valid.
581 */
582 public static function streamFor($resource = '', array $options = []) : StreamInterface
583 {
584 if (\is_scalar($resource)) {
585 if (!\is_string($resource)) {
586 throw new \InvalidArgumentException(\sprintf('Cannot create a stream from %s; pass a string, resource, StreamInterface, Stringable, Iterator, callable, or null.', \get_debug_type($resource)));
587 }
588 $stream = self::tryFopen('php://temp', 'r+');
589 if ($resource !== '') {
590 \fwrite($stream, $resource);
591 \fseek($stream, 0);
592 }
593 return new Stream($stream, $options);
594 }
595 switch (\gettype($resource)) {
596 case 'resource':
597 /*
598 * The 'php://input' is a special stream with quirks and inconsistencies.
599 * We avoid using that stream by reading it into php://temp
600 */
601 /** @var resource $resource */
602 if ((\stream_get_meta_data($resource)['uri'] ?? '') === 'php://input') {
603 $stream = self::tryFopen('php://temp', 'w+');
604 \stream_copy_to_stream($resource, $stream);
605 \fseek($stream, 0);
606 $resource = $stream;
607 }
608 return new Stream($resource, $options);
609 case 'object':
610 /** @var object $resource */
611 if ($resource instanceof StreamInterface) {
612 return $resource;
613 } elseif ($resource instanceof \Iterator) {
614 return new PumpStream(function (int $length) use($resource) {
615 while ($resource->valid()) {
616 $result = $resource->current();
617 $resource->next();
618 if (\is_float($result) && !\is_finite($result)) {
619 throw new \UnexpectedValueException('Iterator must not yield non-finite float values');
620 }
621 if ($result === null || \is_scalar($result)) {
622 $data = (string) $result;
623 } elseif (\is_object($result) && \method_exists($result, '__toString')) {
624 $data = (string) $result;
625 } else {
626 throw new \UnexpectedValueException('Iterator must yield scalar, null, or stringable values');
627 }
628 if ($data !== '') {
629 return $data;
630 }
631 }
632 return \false;
633 }, $options);
634 } elseif (\method_exists($resource, '__toString')) {
635 return self::streamFor((string) $resource, $options);
636 }
637 break;
638 case 'NULL':
639 return new Stream(self::tryFopen('php://temp', 'r+'), $options);
640 }
641 if (\is_callable($resource)) {
642 return new PumpStream($resource, $options);
643 }
644 throw new \InvalidArgumentException('Invalid resource type: ' . \get_debug_type($resource));
645 }
646 /**
647 * Safely opens a PHP stream resource using a filename.
648 *
649 * When `fopen()` fails, PHP normally raises a warning. This function adds
650 * an error handler that checks for errors and throws an exception instead.
651 *
652 * @param string $filename File to open
653 * @param string $mode Mode used to open the file
654 *
655 * @return resource
656 *
657 * @throws \RuntimeException if the file cannot be opened
658 */
659 public static function tryFopen(string $filename, string $mode)
660 {
661 $ex = null;
662 \set_error_handler(static function (int $errno, string $errstr) use($filename, $mode, &$ex) : bool {
663 $ex = new \RuntimeException(\sprintf('Unable to open %s using mode %s: %s', DiagnosticValue::escape($filename), DiagnosticValue::escape($mode), DiagnosticValue::escape($errstr)));
664 return \true;
665 });
666 try {
667 /** @var resource $handle */
668 $handle = \fopen($filename, $mode);
669 } catch (\Throwable $e) {
670 $ex = new \RuntimeException(\sprintf('Unable to open %s using mode %s: %s', DiagnosticValue::escape($filename), DiagnosticValue::escape($mode), $e->getMessage()), 0, $e);
671 }
672 \restore_error_handler();
673 if ($ex) {
674 /** @var \RuntimeException $ex */
675 throw $ex;
676 }
677 return $handle;
678 }
679 /**
680 * Safely gets the contents of a given stream.
681 *
682 * When `stream_get_contents()` fails, PHP normally raises a warning. This
683 * function adds an error handler that checks for errors and throws an
684 * exception instead.
685 *
686 * Throws `TimeoutException` when PHP-style timeout metadata can be detected
687 * after a stream read cannot make progress.
688 *
689 * @param resource $stream
690 *
691 * @throws \RuntimeException if the stream cannot be read
692 */
693 public static function tryGetContents($stream) : string
694 {
695 $ex = null;
696 \set_error_handler(static function (int $errno, string $errstr) use(&$ex) : bool {
697 $ex = new \RuntimeException(\sprintf('Unable to read stream contents: %s', DiagnosticValue::escape($errstr)));
698 return \true;
699 });
700 try {
701 /** @var string|false $contents */
702 $contents = \stream_get_contents($stream);
703 if ($contents === \false) {
704 $ex = StreamTimeout::isResourceReadTimedOut($stream) ? new TimeoutException('Unable to read stream contents: timed out') : new \RuntimeException('Unable to read stream contents');
705 } elseif (StreamTimeout::isResourceReadTimedOut($stream)) {
706 $ex = new TimeoutException('Unable to read stream contents: timed out');
707 }
708 } catch (TimeoutException $e) {
709 $ex = $e;
710 } catch (\Throwable $e) {
711 $ex = StreamTimeout::isResourceReadTimedOut($stream) ? new TimeoutException('Unable to read stream contents: timed out', 0, $e) : new \RuntimeException(\sprintf('Unable to read stream contents: %s', $e->getMessage()), 0, $e);
712 }
713 \restore_error_handler();
714 if ($ex) {
715 /** @var \RuntimeException $ex */
716 throw $ex;
717 }
718 return $contents;
719 }
720 /**
721 * Returns a `UriInterface` for the given value.
722 *
723 * This function accepts a string or `UriInterface` and returns a
724 * `UriInterface` for the given value. If the value is already a
725 * `UriInterface`, it is returned as-is.
726 *
727 * @param string|UriInterface $uri
728 *
729 * @throws \InvalidArgumentException
730 */
731 public static function uriFor($uri) : UriInterface
732 {
733 if ($uri instanceof UriInterface) {
734 return $uri;
735 }
736 if (\is_string($uri)) {
737 return new Uri($uri);
738 }
739 throw new \InvalidArgumentException('URI must be a string or UriInterface');
740 }
741 }
742