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

UploadedFile::moveTo()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 14
nc 8
nop 1
dl 0
loc 24
rs 8.8333
c 0
b 0
f 0
1
<?php
2
/**
3
 * Class UploadedFile
4
 *
5
 * @filesource   UploadedFile.php
6
 * @created      11.08.2018
7
 * @package      chillerlan\HTTP\Psr7
8
 * @author       smiley <[email protected]>
9
 * @copyright    2018 smiley
10
 * @license      MIT
11
 */
12
13
namespace chillerlan\HTTP\Psr7;
14
15
use chillerlan\HTTP\Psr17\StreamFactory;
16
use Psr\Http\Message\{StreamInterface, UploadedFileInterface};
17
use InvalidArgumentException, RuntimeException;
18
19
use function chillerlan\HTTP\Psr17\create_stream_from_input;
20
use function in_array, is_file, is_string, is_writable, move_uploaded_file, php_sapi_name,rename;
21
22
use const UPLOAD_ERR_CANT_WRITE, UPLOAD_ERR_EXTENSION, UPLOAD_ERR_FORM_SIZE, UPLOAD_ERR_INI_SIZE,
23
	UPLOAD_ERR_NO_FILE, UPLOAD_ERR_NO_TMP_DIR, UPLOAD_ERR_OK, UPLOAD_ERR_PARTIAL;
24
25
final class UploadedFile implements UploadedFileInterface{
26
27
	/** @var int[] */
28
	public const UPLOAD_ERRORS = [
29
		UPLOAD_ERR_OK,
30
		UPLOAD_ERR_INI_SIZE,
31
		UPLOAD_ERR_FORM_SIZE,
32
		UPLOAD_ERR_PARTIAL,
33
		UPLOAD_ERR_NO_FILE,
34
		UPLOAD_ERR_NO_TMP_DIR,
35
		UPLOAD_ERR_CANT_WRITE,
36
		UPLOAD_ERR_EXTENSION,
37
	];
38
39
	private int $error;
40
41
	private int $size;
42
43
	private ?string $clientFilename;
44
45
	private ?string $clientMediaType;
46
47
	private ?string $file = null;
48
49
	private ?StreamInterface $stream;
50
51
	private bool $moved = false;
52
53
	protected StreamFactory $streamFactory;
54
55
	/**
56
	 * @param \Psr\Http\Message\StreamInterface|string|resource $file
57
	 * @param int                                               $size
58
	 * @param int                                               $error
59
	 * @param string|null                                       $filename
60
	 * @param string|null                                       $mediaType
61
	 *
62
	 * @throws \InvalidArgumentException
63
	 */
64
	public function __construct($file, int $size, int $error = UPLOAD_ERR_OK, string $filename = null, string $mediaType = null){
65
66
		if(!in_array($error, $this::UPLOAD_ERRORS, true)){
67
			throw new InvalidArgumentException('Invalid error status for UploadedFile');
68
		}
69
70
		$this->size            = (int)$size; // int type hint also accepts float...
71
		$this->error           = $error;
72
		$this->clientFilename  = $filename;
73
		$this->clientMediaType = $mediaType;
74
		$this->streamFactory   = new StreamFactory;
75
76
		if($this->error === UPLOAD_ERR_OK){
77
78
			is_string($file)
79
				? $this->file = $file
80
				: $this->stream = create_stream_from_input($file);
81
82
		}
83
84
	}
85
86
	/**
87
	 * @inheritDoc
88
	 */
89
	public function getStream():StreamInterface{
90
91
		$this->validateActive();
92
93
		if($this->stream instanceof StreamInterface){
94
			return $this->stream;
95
		}
96
97
		if(is_file($this->file)){
0 ignored issues
show
Bug introduced by
It seems like $this->file can also be of type null; however, parameter $filename of is_file() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

97
		if(is_file(/** @scrutinizer ignore-type */ $this->file)){
Loading history...
98
			return $this->streamFactory->createStreamFromFile($this->file, 'r+');
0 ignored issues
show
Bug introduced by
It seems like $this->file can also be of type null; however, parameter $filename of chillerlan\HTTP\Psr17\St...:createStreamFromFile() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

98
			return $this->streamFactory->createStreamFromFile(/** @scrutinizer ignore-type */ $this->file, 'r+');
Loading history...
99
		}
100
101
		return $this->streamFactory->createStream($this->file);
0 ignored issues
show
Bug introduced by
It seems like $this->file can also be of type null; however, parameter $content of chillerlan\HTTP\Psr17\St...Factory::createStream() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

101
		return $this->streamFactory->createStream(/** @scrutinizer ignore-type */ $this->file);
Loading history...
102
	}
103
104
	/**
105
	 * @inheritDoc
106
	 */
107
	public function moveTo($targetPath):void{
108
109
		$this->validateActive();
110
111
		if(is_string($targetPath) && empty($targetPath)){
112
			throw new InvalidArgumentException('Invalid path provided for move operation; must be a non-empty string');
113
		}
114
115
		if(!is_writable($targetPath)){
116
			throw new RuntimeException('Directory is not writable: '.$targetPath);
117
		}
118
119
		if($this->file !== null){
120
			$this->moved = php_sapi_name() === 'cli'
121
				? rename($this->file, $targetPath)
122
				: move_uploaded_file($this->file, $targetPath);
123
		}
124
		else{
125
			$this->copyToStream($this->streamFactory->createStreamFromFile($targetPath, 'r+'));
126
			$this->moved = true;
127
		}
128
129
		if($this->moved === false){
130
			throw new RuntimeException('Uploaded file could not be moved to '.$targetPath); // @codeCoverageIgnore
131
		}
132
133
	}
134
135
	/**
136
	 * @inheritDoc
137
	 */
138
	public function getSize():?int{
139
		return $this->size;
140
	}
141
142
	/**
143
	 * @inheritDoc
144
	 */
145
	public function getError():int{
146
		return $this->error;
147
	}
148
149
	/**
150
	 * @inheritDoc
151
	 */
152
	public function getClientFilename():?string{
153
		return $this->clientFilename;
154
	}
155
156
	/**
157
	 * @inheritDoc
158
	 */
159
	public function getClientMediaType():?string{
160
		return $this->clientMediaType;
161
	}
162
163
	/**
164
	 * @throws RuntimeException if is moved or not ok
165
	 */
166
	private function validateActive():void{
167
168
		if($this->error !== UPLOAD_ERR_OK){
169
			throw new RuntimeException('Cannot retrieve stream due to upload error');
170
		}
171
172
		if($this->moved){
173
			throw new RuntimeException('Cannot retrieve stream after it has already been moved');
174
		}
175
176
	}
177
178
	/**
179
	 * Copy the contents of a stream into another stream until the given number
180
	 * of bytes have been read.
181
	 *
182
	 * @author Michael Dowling and contributors to guzzlehttp/psr7
183
	 *
184
	 * @param StreamInterface $dest   Stream to write to
185
	 *
186
	 * @throws \RuntimeException on error
187
	 */
188
	private function copyToStream(StreamInterface $dest){
189
		$source = $this->getStream();
190
191
		if($source->isSeekable()){
192
			$source->rewind();
193
		}
194
195
		while(!$source->eof()){
196
197
			if(!$dest->write($source->read(1048576))){
198
				break; // @codeCoverageIgnore
199
			}
200
201
		}
202
203
	}
204
205
}
206