MultipartStream.php 4.7 KB

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