1<?php 2 3declare(strict_types=1); 4 5namespace GuzzleHttp\Psr7; 6 7use Psr\Http\Message\RequestInterface; 8use Psr\Http\Message\ServerRequestInterface; 9use Psr\Http\Message\StreamInterface; 10use Psr\Http\Message\UriInterface; 11 12final 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 * Create a new stream based on the input type. 255 * 256 * Options is an associative array that can contain the following keys: 257 * - metadata: Array of custom metadata. 258 * - size: Size of the stream. 259 * 260 * This method accepts the following `$resource` types: 261 * - `Psr\Http\Message\StreamInterface`: Returns the value as-is. 262 * - `string`: Creates a stream object that uses the given string as the contents. 263 * - `resource`: Creates a stream object that wraps the given PHP stream resource. 264 * - `Iterator`: If the provided value implements `Iterator`, then a read-only 265 * stream object will be created that wraps the given iterable. Each time the 266 * stream is read from, data from the iterator will fill a buffer and will be 267 * continuously called until the buffer is equal to the requested read size. 268 * Subsequent read calls will first read from the buffer and then call `next` 269 * on the underlying iterator until it is exhausted. 270 * - `object` with `__toString()`: If the object has the `__toString()` method, 271 * the object will be cast to a string and then a stream will be returned that 272 * uses the string value. 273 * - `NULL`: When `null` is passed, an empty stream object is returned. 274 * - `callable` When a callable is passed, a read-only stream object will be 275 * created that invokes the given callable. The callable is invoked with the 276 * number of suggested bytes to read. The callable can return any number of 277 * bytes, but MUST return `false` when there is no more data to return. The 278 * stream object that wraps the callable will invoke the callable until the 279 * number of requested bytes are available. Any additional bytes will be 280 * buffered and used in subsequent reads. 281 * 282 * @param resource|string|int|float|bool|StreamInterface|callable|\Iterator|null $resource Entity body data 283 * @param array{size?: int, metadata?: array} $options Additional options 284 * 285 * @throws \InvalidArgumentException if the $resource arg is not valid. 286 */ 287 public static function streamFor($resource = '', array $options = []): StreamInterface 288 { 289 if (is_scalar($resource)) { 290 $stream = self::tryFopen('php://temp', 'r+'); 291 if ($resource !== '') { 292 fwrite($stream, (string) $resource); 293 fseek($stream, 0); 294 } 295 296 return new Stream($stream, $options); 297 } 298 299 switch (gettype($resource)) { 300 case 'resource': 301 /* 302 * The 'php://input' is a special stream with quirks and inconsistencies. 303 * We avoid using that stream by reading it into php://temp 304 */ 305 306 /** @var resource $resource */ 307 if ((\stream_get_meta_data($resource)['uri'] ?? '') === 'php://input') { 308 $stream = self::tryFopen('php://temp', 'w+'); 309 stream_copy_to_stream($resource, $stream); 310 fseek($stream, 0); 311 $resource = $stream; 312 } 313 314 return new Stream($resource, $options); 315 case 'object': 316 /** @var object $resource */ 317 if ($resource instanceof StreamInterface) { 318 return $resource; 319 } elseif ($resource instanceof \Iterator) { 320 return new PumpStream(function () use ($resource) { 321 if (!$resource->valid()) { 322 return false; 323 } 324 $result = $resource->current(); 325 $resource->next(); 326 327 return $result; 328 }, $options); 329 } elseif (method_exists($resource, '__toString')) { 330 return self::streamFor((string) $resource, $options); 331 } 332 break; 333 case 'NULL': 334 return new Stream(self::tryFopen('php://temp', 'r+'), $options); 335 } 336 337 if (is_callable($resource)) { 338 return new PumpStream($resource, $options); 339 } 340 341 throw new \InvalidArgumentException('Invalid resource type: '.gettype($resource)); 342 } 343 344 /** 345 * Safely opens a PHP stream resource using a filename. 346 * 347 * When fopen fails, PHP normally raises a warning. This function adds an 348 * error handler that checks for errors and throws an exception instead. 349 * 350 * @param string $filename File to open 351 * @param string $mode Mode used to open the file 352 * 353 * @return resource 354 * 355 * @throws \RuntimeException if the file cannot be opened 356 */ 357 public static function tryFopen(string $filename, string $mode) 358 { 359 $ex = null; 360 set_error_handler(static function (int $errno, string $errstr) use ($filename, $mode, &$ex): bool { 361 $ex = new \RuntimeException(sprintf( 362 'Unable to open "%s" using mode "%s": %s', 363 $filename, 364 $mode, 365 $errstr 366 )); 367 368 return true; 369 }); 370 371 try { 372 /** @var resource $handle */ 373 $handle = fopen($filename, $mode); 374 } catch (\Throwable $e) { 375 $ex = new \RuntimeException(sprintf( 376 'Unable to open "%s" using mode "%s": %s', 377 $filename, 378 $mode, 379 $e->getMessage() 380 ), 0, $e); 381 } 382 383 restore_error_handler(); 384 385 if ($ex) { 386 /** @var $ex \RuntimeException */ 387 throw $ex; 388 } 389 390 return $handle; 391 } 392 393 /** 394 * Safely gets the contents of a given stream. 395 * 396 * When stream_get_contents fails, PHP normally raises a warning. This 397 * function adds an error handler that checks for errors and throws an 398 * exception instead. 399 * 400 * @param resource $stream 401 * 402 * @throws \RuntimeException if the stream cannot be read 403 */ 404 public static function tryGetContents($stream): string 405 { 406 $ex = null; 407 set_error_handler(static function (int $errno, string $errstr) use (&$ex): bool { 408 $ex = new \RuntimeException(sprintf( 409 'Unable to read stream contents: %s', 410 $errstr 411 )); 412 413 return true; 414 }); 415 416 try { 417 /** @var string|false $contents */ 418 $contents = stream_get_contents($stream); 419 420 if ($contents === false) { 421 $ex = new \RuntimeException('Unable to read stream contents'); 422 } 423 } catch (\Throwable $e) { 424 $ex = new \RuntimeException(sprintf( 425 'Unable to read stream contents: %s', 426 $e->getMessage() 427 ), 0, $e); 428 } 429 430 restore_error_handler(); 431 432 if ($ex) { 433 /** @var $ex \RuntimeException */ 434 throw $ex; 435 } 436 437 return $contents; 438 } 439 440 /** 441 * Returns a UriInterface for the given value. 442 * 443 * This function accepts a string or UriInterface and returns a 444 * UriInterface for the given value. If the value is already a 445 * UriInterface, it is returned as-is. 446 * 447 * @param string|UriInterface $uri 448 * 449 * @throws \InvalidArgumentException 450 */ 451 public static function uriFor($uri): UriInterface 452 { 453 if ($uri instanceof UriInterface) { 454 return $uri; 455 } 456 457 if (is_string($uri)) { 458 return new Uri($uri); 459 } 460 461 throw new \InvalidArgumentException('URI must be a string or UriInterface'); 462 } 463} 464