Passed
Push — gh-pages ( 22b0fe...eb2d91 )
by
unknown
12:27 queued 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
 * @created      20.12.2018
8
 * @author       smiley <[email protected]>
9
 * @copyright    2018 smiley
10
 * @license      MIT
11
 */
12
13
namespace chillerlan\HTTP\Psr7;
14
15
use InvalidArgumentException, RuntimeException;
16
17
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 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_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...
18
use function array_merge, basename, is_string, pathinfo, random_bytes, sha1, strlen, strtolower, substr;
19
20
use const PATHINFO_EXTENSION;
21
22
/**
23
 * @property \chillerlan\HTTP\Psr7\Stream|null $stream
24
 */
25
final class MultipartStream extends StreamAbstract{
26
27
	protected string $boundary;
28
29
	protected bool $built = false;
30
31
	/**
32
	 * MultipartStream constructor.
33
	 *
34
	 * @param array|null   $elements [
35
	 *                                   name     => string,
36
	 *                                   contents => StreamInterface/resource/string,
37
	 *                                   headers  => array,
38
	 *                                   filename => string
39
	 *                               ]
40
	 * @param string|null  $boundary
41
	 */
42
	public function __construct(array $elements = null, string $boundary = null){
43
		$this->boundary = $boundary ?? sha1(random_bytes(1024));
44
		$this->stream   = create_stream();
45
46
		foreach($elements ?? [] as $element){
47
			$this->addElement($element);
48
		}
49
50
	}
51
52
	/**
53
	 *
54
	 */
55
	public function getBoundary():string{
56
		return $this->boundary;
57
	}
58
59
	/**
60
	 *
61
	 */
62
	public function build():MultipartStream{
63
64
		if(!$this->built){
65
			$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

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

206
		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...
207
	}
208
209
}
210