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\StreamInterface;
   8  
   9  /**
  10   * Stream that when read returns bytes for a streaming multipart or
  11   * multipart/form-data stream.
  12   */
  13  final class MultipartStream implements StreamInterface
  14  {
  15      use StreamDecoratorTrait;
  16  
  17      /** @var string */
  18      private $boundary;
  19  
  20      /** @var StreamInterface */
  21      private $stream;
  22  
  23      /**
  24       * @param array  $elements Array of associative arrays, each containing a
  25       *                         required "name" key mapping to the form field,
  26       *                         name, a required "contents" key mapping to a
  27       *                         StreamInterface/resource/string, an optional
  28       *                         "headers" associative array of custom headers,
  29       *                         and an optional "filename" key mapping to a
  30       *                         string to send as the filename in the part.
  31       * @param string $boundary You can optionally provide a specific boundary
  32       *
  33       * @throws \InvalidArgumentException
  34       */
  35      public function __construct(array $elements = [], string $boundary = null)
  36      {
  37          $this->boundary = $boundary ?: bin2hex(random_bytes(20));
  38          $this->stream = $this->createStream($elements);
  39      }
  40  
  41      public function getBoundary(): string
  42      {
  43          return $this->boundary;
  44      }
  45  
  46      public function isWritable(): bool
  47      {
  48          return false;
  49      }
  50  
  51      /**
  52       * Get the headers needed before transferring the content of a POST file
  53       *
  54       * @param array<string, string> $headers
  55       */
  56      private function getHeaders(array $headers): string
  57      {
  58          $str = '';
  59          foreach ($headers as $key => $value) {
  60              $str .= "{$key}: {$value}\r\n";
  61          }
  62  
  63          return "--{$this->boundary}\r\n" . trim($str) . "\r\n\r\n";
  64      }
  65  
  66      /**
  67       * Create the aggregate stream that will be used to upload the POST data
  68       */
  69      protected function createStream(array $elements = []): StreamInterface
  70      {
  71          $stream = new AppendStream();
  72  
  73          foreach ($elements as $element) {
  74              if (!is_array($element)) {
  75                  throw new \UnexpectedValueException("An array is expected");
  76              }
  77              $this->addElement($stream, $element);
  78          }
  79  
  80          // Add the trailing boundary with CRLF
  81          $stream->addStream(Utils::streamFor("--{$this->boundary}--\r\n"));
  82  
  83          return $stream;
  84      }
  85  
  86      private function addElement(AppendStream $stream, array $element): void
  87      {
  88          foreach (['contents', 'name'] as $key) {
  89              if (!array_key_exists($key, $element)) {
  90                  throw new \InvalidArgumentException("A '{$key}' key is required");
  91              }
  92          }
  93  
  94          $element['contents'] = Utils::streamFor($element['contents']);
  95  
  96          if (empty($element['filename'])) {
  97              $uri = $element['contents']->getMetadata('uri');
  98              if ($uri && \is_string($uri) && \substr($uri, 0, 6) !== 'php://' && \substr($uri, 0, 7) !== 'data://') {
  99                  $element['filename'] = $uri;
 100              }
 101          }
 102  
 103          [$body, $headers] = $this->createElement(
 104              $element['name'],
 105              $element['contents'],
 106              $element['filename'] ?? null,
 107              $element['headers'] ?? []
 108          );
 109  
 110          $stream->addStream(Utils::streamFor($this->getHeaders($headers)));
 111          $stream->addStream($body);
 112          $stream->addStream(Utils::streamFor("\r\n"));
 113      }
 114  
 115      private function createElement(string $name, StreamInterface $stream, ?string $filename, array $headers): array
 116      {
 117          // Set a default content-disposition header if one was no provided
 118          $disposition = $this->getHeader($headers, 'content-disposition');
 119          if (!$disposition) {
 120              $headers['Content-Disposition'] = ($filename === '0' || $filename)
 121                  ? sprintf(
 122                      'form-data; name="%s"; filename="%s"',
 123                      $name,
 124                      basename($filename)
 125                  )
 126                  : "form-data; name=\"{$name}\"";
 127          }
 128  
 129          // Set a default content-length header if one was no provided
 130          $length = $this->getHeader($headers, 'content-length');
 131          if (!$length) {
 132              if ($length = $stream->getSize()) {
 133                  $headers['Content-Length'] = (string) $length;
 134              }
 135          }
 136  
 137          // Set a default Content-Type if one was not supplied
 138          $type = $this->getHeader($headers, 'content-type');
 139          if (!$type && ($filename === '0' || $filename)) {
 140              if ($type = MimeType::fromFilename($filename)) {
 141                  $headers['Content-Type'] = $type;
 142              }
 143          }
 144  
 145          return [$stream, $headers];
 146      }
 147  
 148      private function getHeader(array $headers, string $key)
 149      {
 150          $lowercaseHeader = strtolower($key);
 151          foreach ($headers as $k => $v) {
 152              if (strtolower($k) === $lowercaseHeader) {
 153                  return $v;
 154              }
 155          }
 156  
 157          return null;
 158      }
 159  }