Passed
Push — main ( f2efe3...53dc0a )
by smiley
10:15
created

MultipartStream::setElementHeaders()   B

Complexity

Conditions 9
Paths 54

Size

Total Lines 29
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
eloc 13
nc 54
nop 1
dl 0
loc 29
rs 8.0555
c 1
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};
0 ignored issues
show
Bug introduced by
The type chillerlan\HTTP\Psr17\create_stream_from_input was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
Bug introduced by
The type chillerlan\HTTP\Psr17\create_stream was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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|null $stream
26
 */
27
final class MultipartStream extends StreamAbstract{
28
29
	protected string $boundary;
30
31
	protected bool $built = false;
32
33
	/**
34
	 * MultipartStream constructor.
35
	 *
36
	 * @param array|null   $elements [
37
	 *                                   name     => string,
38
	 *                                   contents => StreamInterface/resource/string,
39
	 *                                   headers  => array,
40
	 *                                   filename => string
41
	 *                               ]
42
	 * @param string|null  $boundary
43
	 */
44
	public function __construct(array $elements = null, string $boundary = null){
45
		$this->boundary = $boundary ?? sha1(random_bytes(1024));
46
		$this->stream   = create_stream();
47
48
		foreach($elements ?? [] as $element){
49
			$this->addElement($element);
50
		}
51
52
	}
53
54
	/**
55
	 *
56
	 */
57
	public function getBoundary():string{
58
		return $this->boundary;
59
	}
60
61
	/**
62
	 *
63
	 */
64
	public function build():MultipartStream{
65
66
		if(!$this->built){
67
			$this->stream->write('--'.$this->getBoundary()."--\r\n");
0 ignored issues
show
Bug introduced by
The method write() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

67
			$this->stream->/** @scrutinizer ignore-call */ 
68
                  write('--'.$this->getBoundary()."--\r\n");

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
68
69
			$this->built = true;
70
		}
71
72
		$this->stream->rewind();
73
74
		return $this;
75
	}
76
77
	/**
78
	 *
79
	 */
80
	public function addElement(array $e):MultipartStream{
81
82
		if($this->built){
83
			throw new RuntimeException('Stream already built');
84
		}
85
86
		$e = array_merge(['filename' => null, 'headers' => []], $e);
87
88
		foreach(['contents', 'name'] as $key){
89
			if(!isset($e[$key])){
90
				throw new InvalidArgumentException('A "'.$key.'" element is required');
91
			}
92
		}
93
94
		// at this point we assume the string is already the file content and don't guess anymore
95
		$e['contents'] = is_string($e['contents'])
96
			? create_stream($e['contents'])
97
			: create_stream_from_input($e['contents']);
98
99
		if(empty($e['filename'])){
100
			$uri = $e['contents']->getMetadata('uri');
101
102
			if(substr($uri, 0, 6) !== 'php://'){
103
				$e['filename'] = $uri;
104
			}
105
		}
106
107
		$e = $this->setElementHeaders($e);
108
109
		$this->stream->write('--'.$this->boundary."\r\n");
110
111
		foreach(normalize_message_headers($e['headers']) as $key => $value){
112
			$this->stream->write($key.': '.$value."\r\n");
113
		}
114
115
		$this->stream->write("\r\n".$e['contents']->getContents()."\r\n");
116
117
		return $this;
118
	}
119
120
	/**
121
	 *
122
	 */
123
	protected function setElementHeaders(array $e):array{
124
		$hasFilename = $e['filename'] === '0' || $e['filename'];
125
126
		// Set a default content-disposition header if none was provided
127
		if(!$this->hasHeader($e['headers'], 'content-disposition')){
128
			$filename = $hasFilename ? '; filename="'.basename($e['filename']).'"' : '';
129
130
			$e['headers']['Content-Disposition'] = 'form-data; name="'.$e['name'].'"'.$filename;
131
		}
132
133
		// Set a default content-length header if none was provided
134
		if(!$this->hasHeader($e['headers'], 'content-length')){
135
			$length = $e['contents']->getSize();
136
137
			if($length){
138
				$e['headers']['Content-Length'] = $length;
139
			}
140
		}
141
142
		// Set a default Content-Type if none was supplied
143
		if(!$this->hasHeader($e['headers'], 'content-type') && $hasFilename){
144
			$type = MIMETYPES[pathinfo($e['filename'], PATHINFO_EXTENSION)] ?? null;
145
146
			if($type){
147
				$e['headers']['Content-Type'] = $type;
148
			}
149
		}
150
151
		return $e;
152
	}
153
154
	/**
155
	 *
156
	 */
157
	protected function hasHeader(array $headers, string $key):bool{
158
		$lowercaseHeader = strtolower($key);
159
160
		foreach($headers as $k => $v){
161
			if(strtolower($k) === $lowercaseHeader){
162
				return true;
163
			}
164
		}
165
166
		return false;
167
	}
168
169
	/**
170
	 * @inheritDoc
171
	 */
172
	public function __toString(){
173
		return $this->getContents();
174
	}
175
176
	/**
177
	 * @inheritDoc
178
	 */
179
	public function getSize():?int{
180
		return $this->stream->getSize() + strlen($this->boundary) + 6;
181
	}
182
183
	/**
184
	 * @inheritDoc
185
	 */
186
	public function isWritable():bool{
187
		return false;
188
	}
189
190
	/**
191
	 * @inheritDoc
192
	 */
193
	public function write($string):int{
194
		throw new RuntimeException('Cannot write to a MultipartStream, use MultipartStream::addElement() instead.');
195
	}
196
197
	/**
198
	 * @inheritDoc
199
	 */
200
	public function isReadable():bool{
201
		return true;
202
	}
203
204
	/**
205
	 * @inheritDoc
206
	 */
207
	public function getContents():string{
208
		return $this->build()->stream->getContents();
0 ignored issues
show
Bug introduced by
The method getContents() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

208
		return $this->build()->stream->/** @scrutinizer ignore-call */ getContents();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
209
	}
210
211
}
212