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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body