1<?php 2 3namespace GuzzleHttp\Psr7; 4 5use Psr\Http\Message\RequestInterface; 6use Psr\Http\Message\ServerRequestInterface; 7use Psr\Http\Message\StreamInterface; 8use Psr\Http\Message\UriInterface; 9 10final class Utils 11{ 12 /** 13 * Remove the items given by the keys, case insensitively from the data. 14 * 15 * @param iterable<string> $keys 16 * 17 * @return array 18 */ 19 public static function caselessRemove($keys, array $data) 20 { 21 $result = []; 22 23 foreach ($keys as &$key) { 24 $key = strtolower($key); 25 } 26 27 foreach ($data as $k => $v) { 28 if (!in_array(strtolower($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, $maxLen = -1) 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 * @return string 80 * 81 * @throws \RuntimeException on error. 82 */ 83 public static function copyToString(StreamInterface $stream, $maxLen = -1) 84 { 85 $buffer = ''; 86 87 if ($maxLen === -1) { 88 while (!$stream->eof()) { 89 $buf = $stream->read(1048576); 90 // Using a loose equality here to match on '' and false. 91 if ($buf == null) { 92 break; 93 } 94 $buffer .= $buf; 95 } 96 return $buffer; 97 } 98 99 $len = 0; 100 while (!$stream->eof() && $len < $maxLen) { 101 $buf = $stream->read($maxLen - $len); 102 // Using a loose equality here to match on '' and false. 103 if ($buf == null) { 104 break; 105 } 106 $buffer .= $buf; 107 $len = strlen($buffer); 108 } 109 110 return $buffer; 111 } 112 113 /** 114 * Calculate a hash of a stream. 115 * 116 * This method reads the entire stream to calculate a rolling hash, based 117 * on PHP's `hash_init` functions. 118 * 119 * @param StreamInterface $stream Stream to calculate the hash for 120 * @param string $algo Hash algorithm (e.g. md5, crc32, etc) 121 * @param bool $rawOutput Whether or not to use raw output 122 * 123 * @return string Returns the hash of the stream 124 * 125 * @throws \RuntimeException on error. 126 */ 127 public static function hash(StreamInterface $stream, $algo, $rawOutput = false) 128 { 129 $pos = $stream->tell(); 130 131 if ($pos > 0) { 132 $stream->rewind(); 133 } 134 135 $ctx = hash_init($algo); 136 while (!$stream->eof()) { 137 hash_update($ctx, $stream->read(1048576)); 138 } 139 140 $out = hash_final($ctx, (bool) $rawOutput); 141 $stream->seek($pos); 142 143 return $out; 144 } 145 146 /** 147 * Clone and modify a request with the given changes. 148 * 149 * This method is useful for reducing the number of clones needed to mutate 150 * a message. 151 * 152 * The changes can be one of: 153 * - method: (string) Changes the HTTP method. 154 * - set_headers: (array) Sets the given headers. 155 * - remove_headers: (array) Remove the given headers. 156 * - body: (mixed) Sets the given body. 157 * - uri: (UriInterface) Set the URI. 158 * - query: (string) Set the query string value of the URI. 159 * - version: (string) Set the protocol version. 160 * 161 * @param RequestInterface $request Request to clone and modify. 162 * @param array $changes Changes to apply. 163 * 164 * @return RequestInterface 165 */ 166 public static function modifyRequest(RequestInterface $request, array $changes) 167 { 168 if (!$changes) { 169 return $request; 170 } 171 172 $headers = $request->getHeaders(); 173 174 if (!isset($changes['uri'])) { 175 $uri = $request->getUri(); 176 } else { 177 // Remove the host header if one is on the URI 178 if ($host = $changes['uri']->getHost()) { 179 $changes['set_headers']['Host'] = $host; 180 181 if ($port = $changes['uri']->getPort()) { 182 $standardPorts = ['http' => 80, 'https' => 443]; 183 $scheme = $changes['uri']->getScheme(); 184 if (isset($standardPorts[$scheme]) && $port != $standardPorts[$scheme]) { 185 $changes['set_headers']['Host'] .= ':' . $port; 186 } 187 } 188 } 189 $uri = $changes['uri']; 190 } 191 192 if (!empty($changes['remove_headers'])) { 193 $headers = self::caselessRemove($changes['remove_headers'], $headers); 194 } 195 196 if (!empty($changes['set_headers'])) { 197 $headers = self::caselessRemove(array_keys($changes['set_headers']), $headers); 198 $headers = $changes['set_headers'] + $headers; 199 } 200 201 if (isset($changes['query'])) { 202 $uri = $uri->withQuery($changes['query']); 203 } 204 205 if ($request instanceof ServerRequestInterface) { 206 $new = (new ServerRequest( 207 isset($changes['method']) ? $changes['method'] : $request->getMethod(), 208 $uri, 209 $headers, 210 isset($changes['body']) ? $changes['body'] : $request->getBody(), 211 isset($changes['version']) 212 ? $changes['version'] 213 : $request->getProtocolVersion(), 214 $request->getServerParams() 215 )) 216 ->withParsedBody($request->getParsedBody()) 217 ->withQueryParams($request->getQueryParams()) 218 ->withCookieParams($request->getCookieParams()) 219 ->withUploadedFiles($request->getUploadedFiles()); 220 221 foreach ($request->getAttributes() as $key => $value) { 222 $new = $new->withAttribute($key, $value); 223 } 224 225 return $new; 226 } 227 228 return new Request( 229 isset($changes['method']) ? $changes['method'] : $request->getMethod(), 230 $uri, 231 $headers, 232 isset($changes['body']) ? $changes['body'] : $request->getBody(), 233 isset($changes['version']) 234 ? $changes['version'] 235 : $request->getProtocolVersion() 236 ); 237 } 238 239 /** 240 * Read a line from the stream up to the maximum allowed buffer length. 241 * 242 * @param StreamInterface $stream Stream to read from 243 * @param int|null $maxLength Maximum buffer length 244 * 245 * @return string 246 */ 247 public static function readLine(StreamInterface $stream, $maxLength = null) 248 { 249 $buffer = ''; 250 $size = 0; 251 252 while (!$stream->eof()) { 253 // Using a loose equality here to match on '' and false. 254 if (null == ($byte = $stream->read(1))) { 255 return $buffer; 256 } 257 $buffer .= $byte; 258 // Break when a new line is found or the max length - 1 is reached 259 if ($byte === "\n" || ++$size === $maxLength - 1) { 260 break; 261 } 262 } 263 264 return $buffer; 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 $options Additional options 298 * 299 * @return StreamInterface 300 * 301 * @throws \InvalidArgumentException if the $resource arg is not valid. 302 */ 303 public static function streamFor($resource = '', array $options = []) 304 { 305 if (is_scalar($resource)) { 306 $stream = self::tryFopen('php://temp', 'r+'); 307 if ($resource !== '') { 308 fwrite($stream, $resource); 309 fseek($stream, 0); 310 } 311 return new Stream($stream, $options); 312 } 313 314 switch (gettype($resource)) { 315 case 'resource': 316 /* 317 * The 'php://input' is a special stream with quirks and inconsistencies. 318 * We avoid using that stream by reading it into php://temp 319 */ 320 $metaData = \stream_get_meta_data($resource); 321 if (isset($metaData['uri']) && $metaData['uri'] === 'php://input') { 322 $stream = self::tryFopen('php://temp', 'w+'); 323 fwrite($stream, stream_get_contents($resource)); 324 fseek($stream, 0); 325 $resource = $stream; 326 } 327 return new Stream($resource, $options); 328 case 'object': 329 if ($resource instanceof StreamInterface) { 330 return $resource; 331 } elseif ($resource instanceof \Iterator) { 332 return new PumpStream(function () use ($resource) { 333 if (!$resource->valid()) { 334 return false; 335 } 336 $result = $resource->current(); 337 $resource->next(); 338 return $result; 339 }, $options); 340 } elseif (method_exists($resource, '__toString')) { 341 return Utils::streamFor((string) $resource, $options); 342 } 343 break; 344 case 'NULL': 345 return new Stream(self::tryFopen('php://temp', 'r+'), $options); 346 } 347 348 if (is_callable($resource)) { 349 return new PumpStream($resource, $options); 350 } 351 352 throw new \InvalidArgumentException('Invalid resource type: ' . gettype($resource)); 353 } 354 355 /** 356 * Safely opens a PHP stream resource using a filename. 357 * 358 * When fopen fails, PHP normally raises a warning. This function adds an 359 * error handler that checks for errors and throws an exception instead. 360 * 361 * @param string $filename File to open 362 * @param string $mode Mode used to open the file 363 * 364 * @return resource 365 * 366 * @throws \RuntimeException if the file cannot be opened 367 */ 368 public static function tryFopen($filename, $mode) 369 { 370 $ex = null; 371 set_error_handler(function () use ($filename, $mode, &$ex) { 372 $ex = new \RuntimeException(sprintf( 373 'Unable to open "%s" using mode "%s": %s', 374 $filename, 375 $mode, 376 func_get_args()[1] 377 )); 378 379 return true; 380 }); 381 382 try { 383 $handle = fopen($filename, $mode); 384 } catch (\Throwable $e) { 385 $ex = new \RuntimeException(sprintf( 386 'Unable to open "%s" using mode "%s": %s', 387 $filename, 388 $mode, 389 $e->getMessage() 390 ), 0, $e); 391 } 392 393 restore_error_handler(); 394 395 if ($ex) { 396 /** @var $ex \RuntimeException */ 397 throw $ex; 398 } 399 400 return $handle; 401 } 402 403 /** 404 * Returns a UriInterface for the given value. 405 * 406 * This function accepts a string or UriInterface and returns a 407 * UriInterface for the given value. If the value is already a 408 * UriInterface, it is returned as-is. 409 * 410 * @param string|UriInterface $uri 411 * 412 * @return UriInterface 413 * 414 * @throws \InvalidArgumentException 415 */ 416 public static function uriFor($uri) 417 { 418 if ($uri instanceof UriInterface) { 419 return $uri; 420 } 421 422 if (is_string($uri)) { 423 return new Uri($uri); 424 } 425 426 throw new \InvalidArgumentException('URI must be a string or UriInterface'); 427 } 428} 429