1 | <?php |
||||
2 | /** |
||||
3 | * This file is part of the Shieldon package. |
||||
4 | * |
||||
5 | * (c) Terry L. <[email protected]> |
||||
6 | * |
||||
7 | * For the full copyright and license information, please view the LICENSE |
||||
8 | * file that was distributed with this source code. |
||||
9 | */ |
||||
10 | |||||
11 | declare(strict_types=1); |
||||
12 | |||||
13 | namespace Shieldon\Psr7; |
||||
14 | |||||
15 | use Psr\Http\Message\StreamInterface; |
||||
16 | use Psr\Http\Message\UploadedFileInterface; |
||||
17 | use Shieldon\Psr7\Stream; |
||||
18 | use InvalidArgumentException; |
||||
19 | use RuntimeException; |
||||
20 | |||||
21 | use function file_exists; |
||||
22 | use function file_put_contents; |
||||
23 | use function is_string; |
||||
24 | use function is_uploaded_file; |
||||
25 | use function is_writable; |
||||
26 | use function move_uploaded_file; |
||||
27 | use function php_sapi_name; |
||||
28 | use function rename; |
||||
29 | |||||
30 | use const UPLOAD_ERR_CANT_WRITE; |
||||
31 | use const UPLOAD_ERR_EXTENSION; |
||||
32 | use const UPLOAD_ERR_FORM_SIZE; |
||||
33 | use const UPLOAD_ERR_INI_SIZE; |
||||
34 | use const UPLOAD_ERR_NO_FILE; |
||||
35 | use const UPLOAD_ERR_NO_TMP_DIR; |
||||
36 | use const UPLOAD_ERR_OK; |
||||
37 | use const UPLOAD_ERR_PARTIAL; |
||||
38 | use const LOCK_EX; |
||||
39 | |||||
40 | /* |
||||
41 | * Describes a data stream. |
||||
42 | */ |
||||
43 | class UploadedFile implements UploadedFileInterface |
||||
44 | { |
||||
45 | /** |
||||
46 | * The full path of the file provided by client. |
||||
47 | * |
||||
48 | * @var string|null |
||||
49 | */ |
||||
50 | protected $file; |
||||
51 | |||||
52 | /** |
||||
53 | * A stream representing the uploaded file. |
||||
54 | * |
||||
55 | * @var StreamInterface|null |
||||
56 | */ |
||||
57 | protected $stream; |
||||
58 | |||||
59 | /** |
||||
60 | * Is file copy to stream when first time calling getStream(). |
||||
61 | * |
||||
62 | * @var bool |
||||
63 | */ |
||||
64 | protected $isFileToStream = false; |
||||
65 | |||||
66 | /** |
||||
67 | * The file size based on the "size" key in the $_FILES array. |
||||
68 | * |
||||
69 | * @var int|null |
||||
70 | */ |
||||
71 | protected $size; |
||||
72 | |||||
73 | /** |
||||
74 | * The filename based on the "name" key in the $_FILES array. |
||||
75 | * |
||||
76 | * @var string|null |
||||
77 | */ |
||||
78 | protected $name; |
||||
79 | |||||
80 | /** |
||||
81 | * The type of a file. This value is based on the "type" key in the $_FILES array. |
||||
82 | * |
||||
83 | * @var string|null |
||||
84 | */ |
||||
85 | protected $type; |
||||
86 | |||||
87 | /** |
||||
88 | * The error code associated with the uploaded file. |
||||
89 | * |
||||
90 | * @var int |
||||
91 | */ |
||||
92 | protected $error; |
||||
93 | |||||
94 | /** |
||||
95 | * Check if the uploaded file has been moved or not. |
||||
96 | * |
||||
97 | * @var bool |
||||
98 | */ |
||||
99 | protected $isMoved = false; |
||||
100 | |||||
101 | /** |
||||
102 | * The type of interface between web server and PHP. |
||||
103 | * This value is typically from `php_sapi_name`, might be changed ony for |
||||
104 | * unit testing purpose. |
||||
105 | * |
||||
106 | * @var string |
||||
107 | */ |
||||
108 | private $sapi; |
||||
109 | |||||
110 | /** |
||||
111 | * UploadedFile constructor. |
||||
112 | * |
||||
113 | * @param string|StreamInterface $source The full path of a file or stream. |
||||
114 | * @param string|null $name The file name. |
||||
115 | * @param string|null $type The file media type. |
||||
116 | * @param int|null $size The file size in bytes. |
||||
117 | * @param int $error The status code of the upload. |
||||
118 | * @param string|null $sapi Only assign for unit testing purpose. |
||||
119 | */ |
||||
120 | 18 | public function __construct( |
|||
121 | $source , |
||||
122 | ?string $name = null, |
||||
123 | ?string $type = null, |
||||
124 | ?int $size = null, |
||||
125 | int $error = 0 , |
||||
126 | ?string $sapi = null |
||||
127 | ) { |
||||
128 | |||||
129 | 18 | if (is_string($source)) { |
|||
130 | 15 | $this->file = $source; |
|||
131 | |||||
132 | 4 | } elseif ($source instanceof StreamInterface) { |
|||
0 ignored issues
–
show
introduced
by
![]() |
|||||
133 | 3 | $this->stream = $source; |
|||
134 | |||||
135 | } else { |
||||
136 | 1 | throw new InvalidArgumentException( |
|||
137 | 1 | 'First argument accepts only a string or StreamInterface instance.' |
|||
138 | 1 | ); |
|||
139 | } |
||||
140 | |||||
141 | 17 | $this->name = $name; |
|||
142 | 17 | $this->type = $type; |
|||
143 | 17 | $this->size = $size; |
|||
144 | 17 | $this->error = $error; |
|||
145 | 17 | $this->sapi = php_sapi_name(); |
|||
146 | |||||
147 | 17 | if ($sapi) { |
|||
148 | 2 | $this->sapi = $sapi; |
|||
149 | } |
||||
150 | } |
||||
151 | |||||
152 | /** |
||||
153 | * {@inheritdoc} |
||||
154 | */ |
||||
155 | 4 | public function getStream(): StreamInterface |
|||
156 | { |
||||
157 | 4 | if ($this->isMoved) { |
|||
158 | 1 | throw new RuntimeException( |
|||
159 | 1 | 'The stream has been moved.' |
|||
160 | 1 | ); |
|||
161 | } |
||||
162 | |||||
163 | 3 | if (!$this->isFileToStream && !$this->stream) { |
|||
164 | 2 | $resource = @fopen($this->file, 'r'); |
|||
0 ignored issues
–
show
It seems like
$this->file can also be of type null ; however, parameter $filename of fopen() 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
![]() |
|||||
165 | 2 | if (is_resource($resource)) { |
|||
166 | 1 | $this->stream = new Stream($resource); |
|||
167 | } |
||||
168 | 2 | $this->isFileToStream = true; |
|||
169 | } |
||||
170 | |||||
171 | 3 | if (!$this->stream) { |
|||
172 | 1 | throw new RuntimeException( |
|||
173 | 1 | 'No stream is available or can be created.' |
|||
174 | 1 | ); |
|||
175 | } |
||||
176 | |||||
177 | 2 | return $this->stream; |
|||
178 | } |
||||
179 | |||||
180 | /** |
||||
181 | * {@inheritdoc} |
||||
182 | */ |
||||
183 | 7 | public function moveTo($targetPath): void |
|||
184 | { |
||||
185 | 7 | if ($this->isMoved) { |
|||
186 | // Throw exception on the second or subsequent call to the method. |
||||
187 | 1 | throw new RuntimeException( |
|||
188 | 1 | 'Uploaded file already moved' |
|||
189 | 1 | ); |
|||
190 | } |
||||
191 | |||||
192 | 6 | if (!is_writable(dirname($targetPath))) { |
|||
193 | // Throw exception if the $targetPath specified is invalid. |
||||
194 | 1 | throw new RuntimeException( |
|||
195 | 1 | sprintf( |
|||
196 | 1 | 'The target path "%s" is not writable.', |
|||
197 | 1 | $targetPath |
|||
198 | 1 | ) |
|||
199 | 1 | ); |
|||
200 | } |
||||
201 | |||||
202 | // Is a file.. |
||||
203 | 5 | if (is_string($this->file) && ! empty($this->file)) { |
|||
204 | |||||
205 | 3 | if ($this->sapi === 'cli') { |
|||
206 | |||||
207 | 1 | if (!rename($this->file, $targetPath)) { |
|||
208 | |||||
209 | // @codeCoverageIgnoreStart |
||||
210 | |||||
211 | // Throw exception on any error during the move operation. |
||||
212 | throw new RuntimeException( |
||||
213 | sprintf( |
||||
214 | 'Could not rename the file to the target path "%s".', |
||||
215 | $targetPath |
||||
216 | ) |
||||
217 | ); |
||||
218 | |||||
219 | // @codeCoverageIgnoreEnd |
||||
220 | } |
||||
221 | } else { |
||||
222 | |||||
223 | if ( |
||||
224 | 2 | ! is_uploaded_file($this->file) || |
|||
225 | 2 | ! move_uploaded_file($this->file, $targetPath) |
|||
226 | ) { |
||||
227 | // Throw exception on any error during the move operation. |
||||
228 | 3 | throw new RuntimeException( |
|||
229 | 3 | sprintf( |
|||
230 | 3 | 'Could not move the file to the target path "%s".', |
|||
231 | 3 | $targetPath |
|||
232 | 3 | ) |
|||
233 | 3 | ); |
|||
234 | } |
||||
235 | } |
||||
236 | |||||
237 | 2 | } elseif ($this->stream instanceof StreamInterface) { |
|||
238 | 2 | $content = $this->stream->getContents(); |
|||
239 | |||||
240 | 2 | file_put_contents($targetPath, $content, LOCK_EX); |
|||
241 | |||||
242 | // @codeCoverageIgnoreStart |
||||
243 | |||||
244 | if (!file_exists($targetPath)) { |
||||
245 | // Throw exception on any error during the move operation. |
||||
246 | throw new RuntimeException( |
||||
247 | sprintf( |
||||
248 | 'Could not move the stream to the target path "%s".', |
||||
249 | $targetPath |
||||
250 | ) |
||||
251 | ); |
||||
252 | } |
||||
253 | |||||
254 | // @codeCoverageIgnoreEnd |
||||
255 | |||||
256 | 2 | unset($content, $this->stream); |
|||
257 | } |
||||
258 | |||||
259 | 3 | $this->isMoved = true; |
|||
260 | } |
||||
261 | |||||
262 | /** |
||||
263 | * {@inheritdoc} |
||||
264 | */ |
||||
265 | 1 | public function getSize(): ?int |
|||
266 | { |
||||
267 | 1 | return $this->size; |
|||
268 | } |
||||
269 | |||||
270 | /** |
||||
271 | * {@inheritdoc} |
||||
272 | */ |
||||
273 | 1 | public function getError(): int |
|||
274 | { |
||||
275 | 1 | return $this->error; |
|||
276 | } |
||||
277 | |||||
278 | /** |
||||
279 | * {@inheritdoc} |
||||
280 | */ |
||||
281 | 2 | public function getClientFilename(): ?string |
|||
282 | { |
||||
283 | 2 | return $this->name; |
|||
284 | } |
||||
285 | |||||
286 | /** |
||||
287 | * {@inheritdoc} |
||||
288 | */ |
||||
289 | 1 | public function getClientMediaType(): ?string |
|||
290 | { |
||||
291 | 1 | return $this->type; |
|||
292 | } |
||||
293 | |||||
294 | /* |
||||
295 | |-------------------------------------------------------------------------- |
||||
296 | | Non-PSR-7 Methods. |
||||
297 | |-------------------------------------------------------------------------- |
||||
298 | */ |
||||
299 | |||||
300 | /** |
||||
301 | * Get error message when uploading files. |
||||
302 | * |
||||
303 | * @return string |
||||
304 | */ |
||||
305 | 2 | public function getErrorMessage(): string |
|||
306 | { |
||||
307 | 2 | $message = [ |
|||
308 | 2 | UPLOAD_ERR_INI_SIZE => 'The uploaded file exceeds the upload_max_filesize directive in php.ini', |
|||
309 | 2 | UPLOAD_ERR_FORM_SIZE => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.', |
|||
310 | 2 | UPLOAD_ERR_PARTIAL => 'The uploaded file was only partially uploaded.', |
|||
311 | 2 | UPLOAD_ERR_NO_FILE => 'No file was uploaded.', |
|||
312 | 2 | UPLOAD_ERR_NO_TMP_DIR => 'Missing a temporary folder.', |
|||
313 | 2 | UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk.', |
|||
314 | 2 | UPLOAD_ERR_EXTENSION => 'File upload stopped by extension.', |
|||
315 | 2 | UPLOAD_ERR_OK => 'There is no error, the file uploaded with success.', |
|||
316 | 2 | ]; |
|||
317 | |||||
318 | 2 | return $message[$this->error] ?? 'Unknown upload error.'; |
|||
319 | } |
||||
320 | } |
||||
321 |