Passed
Push — master ( 52ecf5...abf332 )
by Nikolaos
02:34
created

UploadedFile::getErrorDescription()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 16
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1.512

Importance

Changes 0
Metric Value
eloc 12
dl 0
loc 16
ccs 3
cts 15
cp 0.2
rs 9.8666
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1.512
1
<?php
2
3
/**
4
 * This file is part of the Phalcon Framework.
5
 *
6
 * (c) Phalcon Team <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE.txt
9
 * file that was distributed with this source code.
10
 *
11
 * Implementation of this file has been influenced by Zend Diactoros
12
 *
13
 * @link    https://github.com/zendframework/zend-diactoros
14
 * @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md
15
 */
16
17
declare(strict_types=1);
18
19
namespace Phalcon\Http\Message;
20
21
use Phalcon\Helper\Number;
22
use Phalcon\Helper\Str;
23
use Phalcon\Http\Message\Exception\InvalidArgumentException;
24
use Psr\Http\Message\StreamInterface;
25
use Psr\Http\Message\UploadedFileInterface;
26
use RuntimeException;
27
28
use function dirname;
29
use function fclose;
30
use function fopen;
31
use function fwrite;
32
use function is_dir;
33
use function is_resource;
34
use function is_string;
35
use function is_writable;
36
use function move_uploaded_file;
37
38
use const PHP_SAPI;
39
use const UPLOAD_ERR_CANT_WRITE;
40
use const UPLOAD_ERR_EXTENSION;
41
use const UPLOAD_ERR_FORM_SIZE;
42
use const UPLOAD_ERR_INI_SIZE;
43
use const UPLOAD_ERR_NO_FILE;
44
use const UPLOAD_ERR_NO_TMP_DIR;
45
use const UPLOAD_ERR_OK;
46
use const UPLOAD_ERR_PARTIAL;
47
48
/**
49
 * PSR-7 UploadedFile
50
 *
51
 * @property bool                        $alreadyMoved
52
 * @property string|null                 $clientFilename
53
 * @property string|null                 $clientMediaType
54
 * @property int                         $error
55
 * @property string                      $fileName
56
 * @property int|null                    $size
57
 * @property StreamInterface|string|null $stream;
58
 */
59
final class UploadedFile implements UploadedFileInterface
60
{
61
    /**
62
     * If the file has already been moved, we hold that status here
63
     *
64
     * @var bool
65
     */
66
    private $alreadyMoved = false;
67
68
    /**
69
     * Retrieve the filename sent by the client.
70
     *
71
     * Do not trust the value returned by this method. A client could send
72
     * a malicious filename with the intention to corrupt or hack your
73
     * application.
74
     *
75
     * Implementations SHOULD return the value stored in the 'name' key of
76
     * the file in the $_FILES array.
77
     *
78
     * @var string | null
79
     */
80
    private $clientFilename = null;
81
82
    /**
83
     * Retrieve the media type sent by the client.
84
     *
85
     * Do not trust the value returned by this method. A client could send
86
     * a malicious media type with the intention to corrupt or hack your
87
     * application.
88
     *
89
     * Implementations SHOULD return the value stored in the 'type' key of
90
     * the file in the $_FILES array.
91
     *
92
     * @var string | null
93
     */
94
    private $clientMediaType = null;
95
96
    /**
97
     * Retrieve the error associated with the uploaded file.
98
     *
99
     * The return value MUST be one of PHP's UPLOAD_ERR_XXX constants.
100
     *
101
     * If the file was uploaded successfully, this method MUST return
102
     * UPLOAD_ERR_OK.
103
     *
104
     * Implementations SHOULD return the value stored in the 'error' key of
105
     * the file in the $_FILES array.
106
     *
107
     * @see http://php.net/manual/en/features.file-upload.errors.php
108
     *
109
     * @var int
110
     */
111
    private $error = 0;
112
113
    /**
114
     * If the stream is a string (file name) we store it here
115
     *
116
     * @var string
117
     */
118
    private $fileName = '';
119
120
    /**
121
     * Retrieve the file size.
122
     *
123
     * Implementations SHOULD return the value stored in the 'size' key of
124
     * the file in the $_FILES array if available, as PHP calculates this based
125
     * on the actual size transmitted.
126
     *
127
     * @var int | null
128
     */
129
    private $size = null;
130
131
    /**
132
     * Holds the stream/string for the uploaded file
133
     *
134
     * @var StreamInterface|string|null
135
     */
136
    private $stream;
137
138
    /**
139
     * UploadedFile constructor.
140
     *
141
     * @param StreamInterface|resource|string $stream
142
     * @param int|null                        $size
143
     * @param int                             $error
144
     * @param string|null                     $clientFilename
145
     * @param string|null                     $clientMediaType
146
     */
147 20
    public function __construct(
148
        $stream,
149
        int $size = null,
150
        int $error = 0,
151
        string $clientFilename = null,
152
        string $clientMediaType = null
153
    ) {
154
        /**
155
         * Check the stream passed. It can be a string representing a file or
156
         * a StreamInterface
157
         */
158 20
        $this->checkStream($stream, $error);
159
160
        /**
161
         * Check the error
162
         */
163 19
        $this->checkError($error);
164
165 18
        $this->size            = $size;
166 18
        $this->clientFilename  = $clientFilename;
167 18
        $this->clientMediaType = $clientMediaType;
168 18
    }
169
170
    /**
171
     * Retrieve the filename sent by the client.
172
     *
173
     * Do not trust the value returned by this method. A client could send
174
     * a malicious filename with the intention to corrupt or hack your
175
     * application.
176
     *
177
     * Implementations SHOULD return the value stored in the 'name' key of
178
     * the file in the $_FILES array.
179
     *
180
     * @return string|null The filename sent by the client or null if none
181
     *     was provided.
182
     */
183 2
    public function getClientFilename(): ?string
184
    {
185 2
        return $this->clientFilename;
186
    }
187
188
    /**
189
     * Retrieve the media type sent by the client.
190
     *
191
     * Do not trust the value returned by this method. A client could send
192
     * a malicious media type with the intention to corrupt or hack your
193
     * application.
194
     *
195
     * Implementations SHOULD return the value stored in the 'type' key of
196
     * the file in the $_FILES array.
197
     *
198
     * @return string|null The media type sent by the client or null if none
199
     *     was provided.
200
     */
201 2
    public function getClientMediaType(): ?string
202
    {
203 2
        return $this->clientMediaType;
204
    }
205
206
    /**
207
     * Retrieve the error associated with the uploaded file.
208
     *
209
     * The return value MUST be one of PHP's UPLOAD_ERR_XXX constants.
210
     *
211
     * If the file was uploaded successfully, this method MUST return
212
     * UPLOAD_ERR_OK.
213
     *
214
     * Implementations SHOULD return the value stored in the 'error' key of
215
     * the file in the $_FILES array.
216
     *
217
     * @see http://php.net/manual/en/features.file-upload.errors.php
218
     * @return int One of PHP's UPLOAD_ERR_XXX constants.
219
     */
220 1
    public function getError(): int
221
    {
222 1
        return $this->error;
223
    }
224
225
    /**
226
     * Retrieve the file size.
227
     *
228
     * Implementations SHOULD return the value stored in the 'size' key of
229
     * the file in the $_FILES array if available, as PHP calculates this based
230
     * on the actual size transmitted.
231
     *
232
     * @return int|null The file size in bytes or null if unknown.
233
     */
234 1
    public function getSize(): ?int
235
    {
236 1
        return $this->size;
237
    }
238
239
    /**
240
     * Retrieve a stream representing the uploaded file.
241
     *
242
     * This method MUST return a StreamInterface instance, representing the
243
     * uploaded file. The purpose of this method is to allow utilizing native
244
     * PHP stream functionality to manipulate the file upload, such as
245
     * stream_copy_to_stream() (though the result will need to be decorated in
246
     * a native PHP stream wrapper to work with such functions).
247
     *
248
     * If the moveTo() method has been called previously, this method MUST
249
     * raise an exception.
250
     *
251
     * @return StreamInterface Stream representation of the uploaded file.
252
     * @throws RuntimeException in cases when no stream is available or can be
253
     *     created.
254
     */
255 6
    public function getStream()
256
    {
257 6
        if (UPLOAD_ERR_OK !== $this->error) {
258 1
            throw new InvalidArgumentException(
259 1
                $this->getErrorDescription($this->error)
260
            );
261
        }
262
263 5
        if ($this->alreadyMoved) {
264 1
            throw new InvalidArgumentException(
265 1
                "The file has already been moved to the target location"
266
            );
267
        }
268
269 5
        if (!($this->stream instanceof StreamInterface)) {
270 1
            $this->stream = new Stream($this->fileName);
271
        }
272
273 5
        return $this->stream;
274
    }
275
276
    /**
277
     * Move the uploaded file to a new location.
278
     *
279
     * Use this method as an alternative to move_uploaded_file(). This method is
280
     * guaranteed to work in both SAPI and non-SAPI environments.
281
     * Implementations must determine which environment they are in, and use the
282
     * appropriate method (move_uploaded_file(), rename(), or a stream
283
     * operation) to perform the operation.
284
     *
285
     * $targetPath may be an absolute path, or a relative path. If it is a
286
     * relative path, resolution should be the same as used by PHP's rename()
287
     * function.
288
     *
289
     * The original file or stream MUST be removed on completion.
290
     *
291
     * If this method is called more than once, any subsequent calls MUST raise
292
     * an exception.
293
     *
294
     * When used in an SAPI environment where $_FILES is populated, when writing
295
     * files via moveTo(), is_uploaded_file() and move_uploaded_file() SHOULD be
296
     * used to ensure permissions and upload status are verified correctly.
297
     *
298
     * If you wish to move to a stream, use getStream(), as SAPI operations
299
     * cannot guarantee writing to stream destinations.
300
     *
301
     * @see http://php.net/is_uploaded_file
302
     * @see http://php.net/move_uploaded_file
303
     *
304
     * @param string $targetPath Path to which to move the uploaded file.
305
     *
306
     * @throws InvalidArgumentException if the $targetPath specified is invalid.
307
     * @throws RuntimeException on any error during the move operation, or on
308
     *     the second or subsequent call to the method.
309
     */
310 5
    public function moveTo($targetPath): void
311
    {
312
        $this
313 5
            ->checkMoveIfMoved()
314 5
            ->checkMoveError()
315 4
            ->checkMoveDirectory($targetPath)
316
        ;
317
318
        if (
319
            empty(PHP_SAPI) ||
320 3
            !empty($this->fileName) ||
321 3
            Str::startsWith(PHP_SAPI, 'cli')
322
        ) {
323 3
            $this->storeFile($targetPath);
324
        } else {
325
            if (true !== move_uploaded_file($this->fileName, $targetPath)) {
326
                throw new InvalidArgumentException(
327
                    "The file cannot be moved to the target folder"
328
                );
329
            }
330
        }
331
332 3
        $this->alreadyMoved = true;
333 3
    }
334
335
    /**
336
     * Checks the passed error code and if not in the range throws an exception
337
     *
338
     * @param int $error
339
     */
340 19
    private function checkError(int $error): void
341
    {
342 19
        if (true !== Number::between($error, UPLOAD_ERR_OK, UPLOAD_ERR_EXTENSION)) {
343 1
            throw new InvalidArgumentException(
344 1
                "Invalid error. Must be one of the UPLOAD_ERR_* constants"
345
            );
346
        }
347
348 18
        $this->error = $error;
349 18
    }
350
351
    /**
352
     * @return UploadedFile
353
     */
354 5
    private function checkMoveIfMoved(): UploadedFile
355
    {
356 5
        if ($this->alreadyMoved) {
357 1
            throw new InvalidArgumentException('File has already been moved');
358
        }
359
360 5
        return $this;
361
    }
362
363
    /**
364
     * @return UploadedFile
365
     */
366 5
    private function checkMoveError(): UploadedFile
367
    {
368 5
        if (UPLOAD_ERR_OK !== $this->error) {
369 1
            throw new InvalidArgumentException(
370 1
                $this->getErrorDescription($this->error)
371
            );
372
        }
373
374 4
        return $this;
375
    }
376
377
    /**
378
     * @param mixed $targetPath
379
     *
380
     * @return UploadedFile
381
     */
382 4
    private function checkMoveDirectory($targetPath): UploadedFile
383
    {
384
        /**
385
         * All together for early failure
386
         */
387
        if (
388 4
            !(is_string($targetPath) &&
389 4
            !empty($targetPath) &&
390 4
            is_dir(dirname($targetPath)) &&
391 4
            is_writable(dirname($targetPath)))
392
        ) {
393 1
            throw new InvalidArgumentException(
394 1
                'Target folder is empty string, not a folder or not writable'
395
            );
396
        }
397
398
399 3
        return $this;
400
    }
401
402
    /**
403
     * Checks the passed error code and if not in the range throws an exception
404
     *
405
     * @param StreamInterface|resource|string $stream
406
     * @param int                             $error
407
     */
408 20
    private function checkStream($stream, int $error): void
409
    {
410 20
        if (UPLOAD_ERR_OK === $error) {
411
            switch (true) {
412 17
                case (is_string($stream)):
413 9
                    $this->fileName = $stream;
414 9
                    break;
415 8
                case (is_resource($stream)):
416 1
                    $this->stream = new Stream($stream);
417 1
                    break;
418 7
                case ($stream instanceof StreamInterface):
419 6
                    $this->stream = $stream;
420 6
                    break;
421
                default:
422 1
                    throw new InvalidArgumentException(
423 1
                        "Invalid stream or file passed"
424
                    );
425
            }
426
        }
427 19
    }
428
429
    /**
430
     * Returns a description string depending on the upload error code passed
431
     *
432
     * @param int $error
433
     *
434
     * @return string
435
     */
436 2
    private function getErrorDescription(int $error): string
437
    {
438
        $errors = [
439 2
            UPLOAD_ERR_OK         => 'There is no error, the file uploaded with success.',
440
            UPLOAD_ERR_INI_SIZE   => 'The uploaded file exceeds the upload_max_' .
441
                'filesize directive in php.ini.',
442
            UPLOAD_ERR_FORM_SIZE  => 'The uploaded file exceeds the MAX_FILE_SIZE ' .
443
                'directive that was specified in the HTML form.',
444
            UPLOAD_ERR_PARTIAL    => 'The uploaded file was only partially uploaded.',
445
            UPLOAD_ERR_NO_FILE    => 'No file was uploaded.',
446
            UPLOAD_ERR_NO_TMP_DIR => 'Missing a temporary folder.',
447
            UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk.',
448
            UPLOAD_ERR_EXTENSION  => 'A PHP extension stopped the file upload.',
449
        ];
450
451 2
        return $errors[$error] ?? 'Unknown upload error';
452
    }
453
454
    /**
455
     * Store a file in the new location (stream)
456
     *
457
     * @param string $targetPath
458
     */
459 3
    private function storeFile(string $targetPath): void
460
    {
461 3
        $handle = fopen($targetPath, "w+b");
462 3
        if (false === $handle) {
463
            throw new InvalidArgumentException("Cannot write to file.");
464
        }
465
466 3
        $stream = $this->getStream();
467
468 3
        $stream->rewind();
469
470 3
        while (true !== $stream->eof()) {
471 3
            $data = $stream->read(2048);
472
473 3
            fwrite($handle, $data);
474
        }
475
476 3
        fclose($handle);
477 3
    }
478
}
479