Completed
Push — master ( 370ea5...ea9d80 )
by smiley
02:49
created

MultipartStream::addElement()   B

Complexity

Conditions 8
Paths 26

Size

Total Lines 39

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
nc 26
nop 1
dl 0
loc 39
rs 8.0515
c 0
b 0
f 0
1
<?php
2
/**
3
 * Class MultipartStream
4
 *
5
 * @link https://github.com/guzzle/psr7/blob/master/src/MultipartStream.php
6
 *
7
 * @filesource   MultipartStream.php
8
 * @created      20.12.2018
9
 * @package      chillerlan\HTTP\Psr7
10
 * @author       smiley <[email protected]>
11
 * @copyright    2018 smiley
12
 * @license      MIT
13
 */
14
15
namespace chillerlan\HTTP\Psr7;
16
17
use InvalidArgumentException, RuntimeException;
18
19
use function chillerlan\HTTP\Psr17\{create_stream, create_stream_from_input};
20
use function array_merge, basename, is_string, pathinfo, random_bytes, sha1, strlen, strtolower, substr;
21
22
use const PATHINFO_EXTENSION;
23
24
/**
25
 * @property \chillerlan\HTTP\Psr7\Stream $stream
26
 */
27
final class MultipartStream extends StreamAbstract{
28
29
	/**
30
	 * @var string
31
	 */
32
	protected $boundary;
33
34
	/**
35
	 * @var bool
36
	 */
37
	protected $built = false;
38
39
	/**
40
	 * MultipartStream constructor.
41
	 *
42
	 * @param array        $elements [
0 ignored issues
show
Documentation introduced by
Should the type for parameter $elements not be null|array? Also, consider making the array more specific, something like array<String>, or String[].

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive. In addition it looks for parameters that have the generic type array and suggests a stricter type like array<String>.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
43
	 *                                   name     => string,
44
	 *                                   contents => StreamInterface/resource/string,
45
	 *                                   headers  => array,
46
	 *                                   filename => string
47
	 *                               ]
48
	 * @param string|null  $boundary
49
	 */
50
	public function __construct(array $elements = null, string $boundary = null){
51
		$this->boundary = $boundary ?? sha1(random_bytes(1024));
52
		$this->stream   = create_stream();
53
54
		foreach($elements ?? [] as $element){
55
			$this->addElement($element);
56
		}
57
58
	}
59
60
	/**
61
	 * @return string
62
	 */
63
	public function getBoundary():string{
64
		return $this->boundary;
65
	}
66
67
	/**
68
	 * @return \chillerlan\HTTP\Psr7\MultipartStream
69
	 */
70
	public function build():MultipartStream{
71
72
		if(!$this->built){
73
			$this->stream->write('--'.$this->getBoundary()."--\r\n");
74
75
			$this->built = true;
76
		}
77
78
		$this->stream->rewind();
79
80
		return $this;
81
	}
82
83
	/**
84
	 * @param array $e
85
	 *
86
	 * @return \chillerlan\HTTP\Psr7\MultipartStream
87
	 */
88
	public function addElement(array $e):MultipartStream{
89
90
		if($this->built){
91
			throw new RuntimeException('Stream already built');
92
		}
93
94
		$e = array_merge(['filename' => null, 'headers' => []], $e);
95
96
		foreach(['contents', 'name'] as $key){
97
			if(!isset($e[$key])){
98
				throw new InvalidArgumentException('A "'.$key.'" element is required');
99
			}
100
		}
101
102
		// at this point we assume the string is already the file content and don't guess anymore
103
		$e['contents'] = is_string($e['contents'])
104
			? create_stream($e['contents'])
105
			: create_stream_from_input($e['contents']);
106
107
		if(empty($e['filename'])){
108
			$uri = $e['contents']->getMetadata('uri');
109
110
			if(substr($uri, 0, 6) !== 'php://'){
111
				$e['filename'] = $uri;
112
			}
113
		}
114
115
		$e = $this->setElementHeaders($e);
116
117
		$this->stream->write('--'.$this->boundary."\r\n");
118
119
		foreach(normalize_request_headers($e['headers']) as $key => $value){
120
			$this->stream->write($key.': '.$value."\r\n");
121
		}
122
123
		$this->stream->write("\r\n".$e['contents']->getContents()."\r\n");
124
125
		return $this;
126
	}
127
128
	/**
129
	 * @param array $e
130
	 *
131
	 * @return array
132
	 */
133
	protected function setElementHeaders(array $e):array{
134
		$hasFilename = $e['filename'] === '0' || $e['filename'];
135
136
		// Set a default content-disposition header if none was provided
137
		if(!$this->hasHeader($e['headers'], 'content-disposition')){
138
			$filename = $hasFilename ? '; filename="'.basename($e['filename']).'"' : '';
139
140
			$e['headers']['Content-Disposition'] = 'form-data; name="'.$e['name'].'"'.$filename;
141
		}
142
143
		// Set a default content-length header if none was provided
144
		if(!$this->hasHeader($e['headers'], 'content-length')){
145
			$length = $e['contents']->getSize();
146
147
			if($length){
148
				$e['headers']['Content-Length'] = $length;
149
			}
150
		}
151
152
		// Set a default Content-Type if none was supplied
153
		if(!$this->hasHeader($e['headers'], 'content-type') && $hasFilename){
154
			$type = MIMETYPES[pathinfo($e['filename'], PATHINFO_EXTENSION)] ?? null;
155
156
			if($type){
157
				$e['headers']['Content-Type'] = $type;
158
			}
159
		}
160
161
		return $e;
162
	}
163
164
	/**
165
	 * @param array  $headers
166
	 * @param string $key
167
	 *
168
	 * @return bool
169
	 */
170
	protected function hasHeader(array $headers, string $key):bool{
171
		$lowercaseHeader = strtolower($key);
172
173
		foreach($headers as $k => $v){
174
			if(strtolower($k) === $lowercaseHeader){
175
				return true;
176
			}
177
		}
178
179
		return false;
180
	}
181
182
	/**
183
	 * @inheritDoc
184
	 */
185
	public function __toString(){
186
		return $this->getContents();
187
	}
188
189
	/**
190
	 * @inheritDoc
191
	 */
192
	public function getSize():?int{
193
		return $this->stream->getSize() + strlen($this->boundary) + 6;
194
	}
195
196
	/**
197
	 * @inheritDoc
198
	 */
199
	public function isWritable():bool{
200
		return false;
201
	}
202
203
	/**
204
	 * @inheritDoc
205
	 */
206
	public function write($string):int{
207
		throw new RuntimeException('Cannot write to a MultipartStream, use MultipartStream::addElement() instead.');
208
	}
209
210
	/**
211
	 * @inheritDoc
212
	 */
213
	public function isReadable():bool{
214
		return true;
215
	}
216
217
	/**
218
	 * @inheritDoc
219
	 */
220
	public function getContents():string{
221
		return $this->build()->stream->getContents();
222
	}
223
224
}
225