Search moodle.org's
Developer Documentation

See Release Notes

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