[e3d4e0a] | 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 | }
|
---|