1 | <?php
|
---|
2 |
|
---|
3 | declare(strict_types=1);
|
---|
4 |
|
---|
5 | namespace GuzzleHttp\Psr7;
|
---|
6 |
|
---|
7 | use Psr\Http\Message\RequestInterface;
|
---|
8 | use Psr\Http\Message\ServerRequestInterface;
|
---|
9 | use Psr\Http\Message\StreamInterface;
|
---|
10 | use Psr\Http\Message\UriInterface;
|
---|
11 |
|
---|
12 | final class Utils
|
---|
13 | {
|
---|
14 | /**
|
---|
15 | * Remove the items given by the keys, case insensitively from the data.
|
---|
16 | *
|
---|
17 | * @param (string|int)[] $keys
|
---|
18 | */
|
---|
19 | public static function caselessRemove(array $keys, array $data): array
|
---|
20 | {
|
---|
21 | $result = [];
|
---|
22 |
|
---|
23 | foreach ($keys as &$key) {
|
---|
24 | $key = strtolower((string) $key);
|
---|
25 | }
|
---|
26 |
|
---|
27 | foreach ($data as $k => $v) {
|
---|
28 | if (!in_array(strtolower((string) $k), $keys)) {
|
---|
29 | $result[$k] = $v;
|
---|
30 | }
|
---|
31 | }
|
---|
32 |
|
---|
33 | return $result;
|
---|
34 | }
|
---|
35 |
|
---|
36 | /**
|
---|
37 | * Copy the contents of a stream into another stream until the given number
|
---|
38 | * of bytes have been read.
|
---|
39 | *
|
---|
40 | * @param StreamInterface $source Stream to read from
|
---|
41 | * @param StreamInterface $dest Stream to write to
|
---|
42 | * @param int $maxLen Maximum number of bytes to read. Pass -1
|
---|
43 | * to read the entire stream.
|
---|
44 | *
|
---|
45 | * @throws \RuntimeException on error.
|
---|
46 | */
|
---|
47 | public static function copyToStream(StreamInterface $source, StreamInterface $dest, int $maxLen = -1): void
|
---|
48 | {
|
---|
49 | $bufferSize = 8192;
|
---|
50 |
|
---|
51 | if ($maxLen === -1) {
|
---|
52 | while (!$source->eof()) {
|
---|
53 | if (!$dest->write($source->read($bufferSize))) {
|
---|
54 | break;
|
---|
55 | }
|
---|
56 | }
|
---|
57 | } else {
|
---|
58 | $remaining = $maxLen;
|
---|
59 | while ($remaining > 0 && !$source->eof()) {
|
---|
60 | $buf = $source->read(min($bufferSize, $remaining));
|
---|
61 | $len = strlen($buf);
|
---|
62 | if (!$len) {
|
---|
63 | break;
|
---|
64 | }
|
---|
65 | $remaining -= $len;
|
---|
66 | $dest->write($buf);
|
---|
67 | }
|
---|
68 | }
|
---|
69 | }
|
---|
70 |
|
---|
71 | /**
|
---|
72 | * Copy the contents of a stream into a string until the given number of
|
---|
73 | * bytes have been read.
|
---|
74 | *
|
---|
75 | * @param StreamInterface $stream Stream to read
|
---|
76 | * @param int $maxLen Maximum number of bytes to read. Pass -1
|
---|
77 | * to read the entire stream.
|
---|
78 | *
|
---|
79 | * @throws \RuntimeException on error.
|
---|
80 | */
|
---|
81 | public static function copyToString(StreamInterface $stream, int $maxLen = -1): string
|
---|
82 | {
|
---|
83 | $buffer = '';
|
---|
84 |
|
---|
85 | if ($maxLen === -1) {
|
---|
86 | while (!$stream->eof()) {
|
---|
87 | $buf = $stream->read(1048576);
|
---|
88 | if ($buf === '') {
|
---|
89 | break;
|
---|
90 | }
|
---|
91 | $buffer .= $buf;
|
---|
92 | }
|
---|
93 |
|
---|
94 | return $buffer;
|
---|
95 | }
|
---|
96 |
|
---|
97 | $len = 0;
|
---|
98 | while (!$stream->eof() && $len < $maxLen) {
|
---|
99 | $buf = $stream->read($maxLen - $len);
|
---|
100 | if ($buf === '') {
|
---|
101 | break;
|
---|
102 | }
|
---|
103 | $buffer .= $buf;
|
---|
104 | $len = strlen($buffer);
|
---|
105 | }
|
---|
106 |
|
---|
107 | return $buffer;
|
---|
108 | }
|
---|
109 |
|
---|
110 | /**
|
---|
111 | * Calculate a hash of a stream.
|
---|
112 | *
|
---|
113 | * This method reads the entire stream to calculate a rolling hash, based
|
---|
114 | * on PHP's `hash_init` functions.
|
---|
115 | *
|
---|
116 | * @param StreamInterface $stream Stream to calculate the hash for
|
---|
117 | * @param string $algo Hash algorithm (e.g. md5, crc32, etc)
|
---|
118 | * @param bool $rawOutput Whether or not to use raw output
|
---|
119 | *
|
---|
120 | * @throws \RuntimeException on error.
|
---|
121 | */
|
---|
122 | public static function hash(StreamInterface $stream, string $algo, bool $rawOutput = false): string
|
---|
123 | {
|
---|
124 | $pos = $stream->tell();
|
---|
125 |
|
---|
126 | if ($pos > 0) {
|
---|
127 | $stream->rewind();
|
---|
128 | }
|
---|
129 |
|
---|
130 | $ctx = hash_init($algo);
|
---|
131 | while (!$stream->eof()) {
|
---|
132 | hash_update($ctx, $stream->read(1048576));
|
---|
133 | }
|
---|
134 |
|
---|
135 | $out = hash_final($ctx, $rawOutput);
|
---|
136 | $stream->seek($pos);
|
---|
137 |
|
---|
138 | return $out;
|
---|
139 | }
|
---|
140 |
|
---|
141 | /**
|
---|
142 | * Clone and modify a request with the given changes.
|
---|
143 | *
|
---|
144 | * This method is useful for reducing the number of clones needed to mutate
|
---|
145 | * a message.
|
---|
146 | *
|
---|
147 | * The changes can be one of:
|
---|
148 | * - method: (string) Changes the HTTP method.
|
---|
149 | * - set_headers: (array) Sets the given headers.
|
---|
150 | * - remove_headers: (array) Remove the given headers.
|
---|
151 | * - body: (mixed) Sets the given body.
|
---|
152 | * - uri: (UriInterface) Set the URI.
|
---|
153 | * - query: (string) Set the query string value of the URI.
|
---|
154 | * - version: (string) Set the protocol version.
|
---|
155 | *
|
---|
156 | * @param RequestInterface $request Request to clone and modify.
|
---|
157 | * @param array $changes Changes to apply.
|
---|
158 | */
|
---|
159 | public static function modifyRequest(RequestInterface $request, array $changes): RequestInterface
|
---|
160 | {
|
---|
161 | if (!$changes) {
|
---|
162 | return $request;
|
---|
163 | }
|
---|
164 |
|
---|
165 | $headers = $request->getHeaders();
|
---|
166 |
|
---|
167 | if (!isset($changes['uri'])) {
|
---|
168 | $uri = $request->getUri();
|
---|
169 | } else {
|
---|
170 | // Remove the host header if one is on the URI
|
---|
171 | if ($host = $changes['uri']->getHost()) {
|
---|
172 | $changes['set_headers']['Host'] = $host;
|
---|
173 |
|
---|
174 | if ($port = $changes['uri']->getPort()) {
|
---|
175 | $standardPorts = ['http' => 80, 'https' => 443];
|
---|
176 | $scheme = $changes['uri']->getScheme();
|
---|
177 | if (isset($standardPorts[$scheme]) && $port != $standardPorts[$scheme]) {
|
---|
178 | $changes['set_headers']['Host'] .= ':'.$port;
|
---|
179 | }
|
---|
180 | }
|
---|
181 | }
|
---|
182 | $uri = $changes['uri'];
|
---|
183 | }
|
---|
184 |
|
---|
185 | if (!empty($changes['remove_headers'])) {
|
---|
186 | $headers = self::caselessRemove($changes['remove_headers'], $headers);
|
---|
187 | }
|
---|
188 |
|
---|
189 | if (!empty($changes['set_headers'])) {
|
---|
190 | $headers = self::caselessRemove(array_keys($changes['set_headers']), $headers);
|
---|
191 | $headers = $changes['set_headers'] + $headers;
|
---|
192 | }
|
---|
193 |
|
---|
194 | if (isset($changes['query'])) {
|
---|
195 | $uri = $uri->withQuery($changes['query']);
|
---|
196 | }
|
---|
197 |
|
---|
198 | if ($request instanceof ServerRequestInterface) {
|
---|
199 | $new = (new ServerRequest(
|
---|
200 | $changes['method'] ?? $request->getMethod(),
|
---|
201 | $uri,
|
---|
202 | $headers,
|
---|
203 | $changes['body'] ?? $request->getBody(),
|
---|
204 | $changes['version'] ?? $request->getProtocolVersion(),
|
---|
205 | $request->getServerParams()
|
---|
206 | ))
|
---|
207 | ->withParsedBody($request->getParsedBody())
|
---|
208 | ->withQueryParams($request->getQueryParams())
|
---|
209 | ->withCookieParams($request->getCookieParams())
|
---|
210 | ->withUploadedFiles($request->getUploadedFiles());
|
---|
211 |
|
---|
212 | foreach ($request->getAttributes() as $key => $value) {
|
---|
213 | $new = $new->withAttribute($key, $value);
|
---|
214 | }
|
---|
215 |
|
---|
216 | return $new;
|
---|
217 | }
|
---|
218 |
|
---|
219 | return new Request(
|
---|
220 | $changes['method'] ?? $request->getMethod(),
|
---|
221 | $uri,
|
---|
222 | $headers,
|
---|
223 | $changes['body'] ?? $request->getBody(),
|
---|
224 | $changes['version'] ?? $request->getProtocolVersion()
|
---|
225 | );
|
---|
226 | }
|
---|
227 |
|
---|
228 | /**
|
---|
229 | * Read a line from the stream up to the maximum allowed buffer length.
|
---|
230 | *
|
---|
231 | * @param StreamInterface $stream Stream to read from
|
---|
232 | * @param int|null $maxLength Maximum buffer length
|
---|
233 | */
|
---|
234 | public static function readLine(StreamInterface $stream, ?int $maxLength = null): string
|
---|
235 | {
|
---|
236 | $buffer = '';
|
---|
237 | $size = 0;
|
---|
238 |
|
---|
239 | while (!$stream->eof()) {
|
---|
240 | if ('' === ($byte = $stream->read(1))) {
|
---|
241 | return $buffer;
|
---|
242 | }
|
---|
243 | $buffer .= $byte;
|
---|
244 | // Break when a new line is found or the max length - 1 is reached
|
---|
245 | if ($byte === "\n" || ++$size === $maxLength - 1) {
|
---|
246 | break;
|
---|
247 | }
|
---|
248 | }
|
---|
249 |
|
---|
250 | return $buffer;
|
---|
251 | }
|
---|
252 |
|
---|
253 | /**
|
---|
254 | * Redact the password in the user info part of a URI.
|
---|
255 | */
|
---|
256 | public static function redactUserInfo(UriInterface $uri): UriInterface
|
---|
257 | {
|
---|
258 | $userInfo = $uri->getUserInfo();
|
---|
259 |
|
---|
260 | if (false !== ($pos = \strpos($userInfo, ':'))) {
|
---|
261 | return $uri->withUserInfo(\substr($userInfo, 0, $pos), '***');
|
---|
262 | }
|
---|
263 |
|
---|
264 | return $uri;
|
---|
265 | }
|
---|
266 |
|
---|
267 | /**
|
---|
268 | * Create a new stream based on the input type.
|
---|
269 | *
|
---|
270 | * Options is an associative array that can contain the following keys:
|
---|
271 | * - metadata: Array of custom metadata.
|
---|
272 | * - size: Size of the stream.
|
---|
273 | *
|
---|
274 | * This method accepts the following `$resource` types:
|
---|
275 | * - `Psr\Http\Message\StreamInterface`: Returns the value as-is.
|
---|
276 | * - `string`: Creates a stream object that uses the given string as the contents.
|
---|
277 | * - `resource`: Creates a stream object that wraps the given PHP stream resource.
|
---|
278 | * - `Iterator`: If the provided value implements `Iterator`, then a read-only
|
---|
279 | * stream object will be created that wraps the given iterable. Each time the
|
---|
280 | * stream is read from, data from the iterator will fill a buffer and will be
|
---|
281 | * continuously called until the buffer is equal to the requested read size.
|
---|
282 | * Subsequent read calls will first read from the buffer and then call `next`
|
---|
283 | * on the underlying iterator until it is exhausted.
|
---|
284 | * - `object` with `__toString()`: If the object has the `__toString()` method,
|
---|
285 | * the object will be cast to a string and then a stream will be returned that
|
---|
286 | * uses the string value.
|
---|
287 | * - `NULL`: When `null` is passed, an empty stream object is returned.
|
---|
288 | * - `callable` When a callable is passed, a read-only stream object will be
|
---|
289 | * created that invokes the given callable. The callable is invoked with the
|
---|
290 | * number of suggested bytes to read. The callable can return any number of
|
---|
291 | * bytes, but MUST return `false` when there is no more data to return. The
|
---|
292 | * stream object that wraps the callable will invoke the callable until the
|
---|
293 | * number of requested bytes are available. Any additional bytes will be
|
---|
294 | * buffered and used in subsequent reads.
|
---|
295 | *
|
---|
296 | * @param resource|string|int|float|bool|StreamInterface|callable|\Iterator|null $resource Entity body data
|
---|
297 | * @param array{size?: int, metadata?: array} $options Additional options
|
---|
298 | *
|
---|
299 | * @throws \InvalidArgumentException if the $resource arg is not valid.
|
---|
300 | */
|
---|
301 | public static function streamFor($resource = '', array $options = []): StreamInterface
|
---|
302 | {
|
---|
303 | if (is_scalar($resource)) {
|
---|
304 | $stream = self::tryFopen('php://temp', 'r+');
|
---|
305 | if ($resource !== '') {
|
---|
306 | fwrite($stream, (string) $resource);
|
---|
307 | fseek($stream, 0);
|
---|
308 | }
|
---|
309 |
|
---|
310 | return new Stream($stream, $options);
|
---|
311 | }
|
---|
312 |
|
---|
313 | switch (gettype($resource)) {
|
---|
314 | case 'resource':
|
---|
315 | /*
|
---|
316 | * The 'php://input' is a special stream with quirks and inconsistencies.
|
---|
317 | * We avoid using that stream by reading it into php://temp
|
---|
318 | */
|
---|
319 |
|
---|
320 | /** @var resource $resource */
|
---|
321 | if ((\stream_get_meta_data($resource)['uri'] ?? '') === 'php://input') {
|
---|
322 | $stream = self::tryFopen('php://temp', 'w+');
|
---|
323 | stream_copy_to_stream($resource, $stream);
|
---|
324 | fseek($stream, 0);
|
---|
325 | $resource = $stream;
|
---|
326 | }
|
---|
327 |
|
---|
328 | return new Stream($resource, $options);
|
---|
329 | case 'object':
|
---|
330 | /** @var object $resource */
|
---|
331 | if ($resource instanceof StreamInterface) {
|
---|
332 | return $resource;
|
---|
333 | } elseif ($resource instanceof \Iterator) {
|
---|
334 | return new PumpStream(function () use ($resource) {
|
---|
335 | if (!$resource->valid()) {
|
---|
336 | return false;
|
---|
337 | }
|
---|
338 | $result = $resource->current();
|
---|
339 | $resource->next();
|
---|
340 |
|
---|
341 | return $result;
|
---|
342 | }, $options);
|
---|
343 | } elseif (method_exists($resource, '__toString')) {
|
---|
344 | return self::streamFor((string) $resource, $options);
|
---|
345 | }
|
---|
346 | break;
|
---|
347 | case 'NULL':
|
---|
348 | return new Stream(self::tryFopen('php://temp', 'r+'), $options);
|
---|
349 | }
|
---|
350 |
|
---|
351 | if (is_callable($resource)) {
|
---|
352 | return new PumpStream($resource, $options);
|
---|
353 | }
|
---|
354 |
|
---|
355 | throw new \InvalidArgumentException('Invalid resource type: '.gettype($resource));
|
---|
356 | }
|
---|
357 |
|
---|
358 | /**
|
---|
359 | * Safely opens a PHP stream resource using a filename.
|
---|
360 | *
|
---|
361 | * When fopen fails, PHP normally raises a warning. This function adds an
|
---|
362 | * error handler that checks for errors and throws an exception instead.
|
---|
363 | *
|
---|
364 | * @param string $filename File to open
|
---|
365 | * @param string $mode Mode used to open the file
|
---|
366 | *
|
---|
367 | * @return resource
|
---|
368 | *
|
---|
369 | * @throws \RuntimeException if the file cannot be opened
|
---|
370 | */
|
---|
371 | public static function tryFopen(string $filename, string $mode)
|
---|
372 | {
|
---|
373 | $ex = null;
|
---|
374 | set_error_handler(static function (int $errno, string $errstr) use ($filename, $mode, &$ex): bool {
|
---|
375 | $ex = new \RuntimeException(sprintf(
|
---|
376 | 'Unable to open "%s" using mode "%s": %s',
|
---|
377 | $filename,
|
---|
378 | $mode,
|
---|
379 | $errstr
|
---|
380 | ));
|
---|
381 |
|
---|
382 | return true;
|
---|
383 | });
|
---|
384 |
|
---|
385 | try {
|
---|
386 | /** @var resource $handle */
|
---|
387 | $handle = fopen($filename, $mode);
|
---|
388 | } catch (\Throwable $e) {
|
---|
389 | $ex = new \RuntimeException(sprintf(
|
---|
390 | 'Unable to open "%s" using mode "%s": %s',
|
---|
391 | $filename,
|
---|
392 | $mode,
|
---|
393 | $e->getMessage()
|
---|
394 | ), 0, $e);
|
---|
395 | }
|
---|
396 |
|
---|
397 | restore_error_handler();
|
---|
398 |
|
---|
399 | if ($ex) {
|
---|
400 | /** @var $ex \RuntimeException */
|
---|
401 | throw $ex;
|
---|
402 | }
|
---|
403 |
|
---|
404 | return $handle;
|
---|
405 | }
|
---|
406 |
|
---|
407 | /**
|
---|
408 | * Safely gets the contents of a given stream.
|
---|
409 | *
|
---|
410 | * When stream_get_contents fails, PHP normally raises a warning. This
|
---|
411 | * function adds an error handler that checks for errors and throws an
|
---|
412 | * exception instead.
|
---|
413 | *
|
---|
414 | * @param resource $stream
|
---|
415 | *
|
---|
416 | * @throws \RuntimeException if the stream cannot be read
|
---|
417 | */
|
---|
418 | public static function tryGetContents($stream): string
|
---|
419 | {
|
---|
420 | $ex = null;
|
---|
421 | set_error_handler(static function (int $errno, string $errstr) use (&$ex): bool {
|
---|
422 | $ex = new \RuntimeException(sprintf(
|
---|
423 | 'Unable to read stream contents: %s',
|
---|
424 | $errstr
|
---|
425 | ));
|
---|
426 |
|
---|
427 | return true;
|
---|
428 | });
|
---|
429 |
|
---|
430 | try {
|
---|
431 | /** @var string|false $contents */
|
---|
432 | $contents = stream_get_contents($stream);
|
---|
433 |
|
---|
434 | if ($contents === false) {
|
---|
435 | $ex = new \RuntimeException('Unable to read stream contents');
|
---|
436 | }
|
---|
437 | } catch (\Throwable $e) {
|
---|
438 | $ex = new \RuntimeException(sprintf(
|
---|
439 | 'Unable to read stream contents: %s',
|
---|
440 | $e->getMessage()
|
---|
441 | ), 0, $e);
|
---|
442 | }
|
---|
443 |
|
---|
444 | restore_error_handler();
|
---|
445 |
|
---|
446 | if ($ex) {
|
---|
447 | /** @var $ex \RuntimeException */
|
---|
448 | throw $ex;
|
---|
449 | }
|
---|
450 |
|
---|
451 | return $contents;
|
---|
452 | }
|
---|
453 |
|
---|
454 | /**
|
---|
455 | * Returns a UriInterface for the given value.
|
---|
456 | *
|
---|
457 | * This function accepts a string or UriInterface and returns a
|
---|
458 | * UriInterface for the given value. If the value is already a
|
---|
459 | * UriInterface, it is returned as-is.
|
---|
460 | *
|
---|
461 | * @param string|UriInterface $uri
|
---|
462 | *
|
---|
463 | * @throws \InvalidArgumentException
|
---|
464 | */
|
---|
465 | public static function uriFor($uri): UriInterface
|
---|
466 | {
|
---|
467 | if ($uri instanceof UriInterface) {
|
---|
468 | return $uri;
|
---|
469 | }
|
---|
470 |
|
---|
471 | if (is_string($uri)) {
|
---|
472 | return new Uri($uri);
|
---|
473 | }
|
---|
474 |
|
---|
475 | throw new \InvalidArgumentException('URI must be a string or UriInterface');
|
---|
476 | }
|
---|
477 | }
|
---|