Passed
Push — master ( bf6fa9...817f88 )
by Nikolaos
06:58
created

UploadedFile::checkMoveDirectory()   A

Complexity

Conditions 5
Paths 2

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

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