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