Passed
Push — master ( 157485...b7615a )
by Nikolaos
09:23
created

UploadedFile::moveTo()   C

Complexity

Conditions 12
Paths 6

Size

Total Lines 46
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 156

Importance

Changes 0
Metric Value
eloc 24
dl 0
loc 46
ccs 0
cts 10
cp 0
rs 6.9666
c 0
b 0
f 0
cc 12
nc 6
nop 1
crap 156

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
 * @link    https://github.com/zendframework/zend-diactoros
13
 * @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md
14
 */
15
16
declare(strict_types=1);
17
18
namespace Phalcon\Http\Message;
19
20
use Phalcon\Helper\Number;
21
use Phalcon\Helper\Arr;
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
    protected $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
    protected $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
    protected $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
    protected $error = 0;
95
96
    /**
97
     * If the stream is a string (file name) we store it here
98
     *
99
     * @var string
100
     */
101
    protected $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
    protected $size = null;
113
114
    /**
115
     * Holds the stream/string for the uploaded file
116
     *
117
     * @var StreamInterface|string|null
118
     */
119
    protected $stream;
120
121
    /**
122
     * UploadedFile constructor.
123
     *
124
     * @param StreamInterface|string|null $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
     * @return string|null
155
     */
156
    public function getClientFilename(): ?string
157
    {
158
        return $this->clientFilename;
159
    }
160
161
    /**
162
     * @return string|null
163
     */
164
    public function getClientMediaType(): ?string
165
    {
166
        return $this->clientMediaType;
167
    }
168
169
    /**
170
     * @return int
171
     */
172
    public function getError(): int
173
    {
174
        return $this->error;
175
    }
176
177
    /**
178
     * @return int|null
179
     */
180
    public function getSize(): ?int
181
    {
182
        return $this->size;
183
    }
184
185
    /**
186
     * Retrieve a stream representing the uploaded file.
187
     *
188
     * This method MUST return a StreamInterface instance, representing the
189
     * uploaded file. The purpose of this method is to allow utilizing native
190
     * PHP stream functionality to manipulate the file upload, such as
191
     * stream_copy_to_stream() (though the result will need to be decorated in
192
     * a native PHP stream wrapper to work with such functions).
193
     *
194
     * If the moveTo() method has been called previously, this method MUST
195
     * raise an exception.
196
     *
197
     * @return StreamInterface Stream representation of the uploaded file.
198
     * @throws RuntimeException in cases when no stream is available or can be created.
199
     */
200
    public function getStream()
201
    {
202
        if (0 !== $this->error) {
203
            throw new InvalidArgumentException(
204
                $this->getErrorDescription($this->error)
205
            );
206
        }
207
208
        if ($this->alreadyMoved) {
209
            throw new InvalidArgumentException(
210
                "The file has already been moved to the target location"
211
            );
212
        }
213
214
        if (!($this->stream instanceof StreamInterface)) {
215
            $this->stream = new Stream($this->fileName);
216
        }
217
218
        return $this->stream;
219
    }
220
221
    /**
222
     * Move the uploaded file to a new location.
223
     *
224
     * Use this method as an alternative to move_uploaded_file(). This method is
225
     * guaranteed to work in both SAPI and non-SAPI environments.
226
     * Implementations must determine which environment they are in, and use the
227
     * appropriate method (move_uploaded_file(), rename(), or a stream
228
     * operation) to perform the operation.
229
     *
230
     * $targetPath may be an absolute path, or a relative path. If it is a
231
     * relative path, resolution should be the same as used by PHP's rename()
232
     * function.
233
     *
234
     * The original file or stream MUST be removed on completion.
235
     *
236
     * If this method is called more than once, any subsequent calls MUST raise
237
     * an exception.
238
     *
239
     * When used in an SAPI environment where $_FILES is populated, when writing
240
     * files via moveTo(), is_uploaded_file() and move_uploaded_file() SHOULD be
241
     * used to ensure permissions and upload status are verified correctly.
242
     *
243
     * If you wish to move to a stream, use getStream(), as SAPI operations
244
     * cannot guarantee writing to stream destinations.
245
     *
246
     * @see http://php.net/is_uploaded_file
247
     * @see http://php.net/move_uploaded_file
248
     *
249
     * @param string $targetPath Path to which to move the uploaded file.
250
     *
251
     * @throws InvalidArgumentException if the $targetPath specified is invalid.
252
     * @throws RuntimeException on any error during the move operation, or on
253
     *     the second or subsequent call to the method.
254
     */
255
    public function moveTo($targetPath): void
256
    {
257
        if ($this->alreadyMoved) {
258
            throw new InvalidArgumentException("File has already been moved");
259
        }
260
261
        if (0 !== $this->error) {
262
            throw new InvalidArgumentException(
263
                $this->getErrorDescription($this->error)
264
            );
265
        }
266
267
        /**
268
         * All together for early failure
269
         */
270
        if (
271
                !(
272
                    is_string($targetPath) &&
273
                    !empty($targetPath) &&
274
                    is_dir(dirname($targetPath)) &&
275
                    is_writable(dirname($targetPath))
276
                )
277
        ) {
278
            throw new InvalidArgumentException(
279
                "Target folder is empty string, not a folder or not writable"
280
            );
281
        }
282
283
        $sapi = constant("PHP_SAPI");
284
285
        if (
286
            empty($sapi) ||
287
            !empty($this->fileName) ||
288
            Str::startsWith($sapi, "cli") ||
289
            Str::startsWith($sapi, "phpdbg")
290
        ) {
291
            $this->storeFile($targetPath);
292
        } else {
293
            if (true !== move_uploaded_file($this->fileName, $targetPath)) {
294
                throw new InvalidArgumentException(
295
                    "The file cannot be moved to the target folder"
296
                );
297
            }
298
        }
299
300
        $this->alreadyMoved = true;
301
    }
302
303
    /**
304
     * Checks the passed error code and if not in the range throws an exception
305
     *
306
     * @param int $error
307
     */
308
    private function checkError(int $error): void
309
    {
310
        if (true !== Number::between($error, 0, 8)) {
311
            throw new InvalidArgumentException(
312
                "Invalid error. Must be one of the UPLOAD_ERR_* constants"
313
            );
314
        }
315
316
        $this->error = $error;
317
    }
318
319
    /**
320
     * Checks the passed error code and if not in the range throws an exception
321
     *
322
     * @param StreamInterface|resource|string $stream
323
     * @param int                             $error
324
     */
325
    private function checkStream($stream, int $error): void
326
    {
327
        if (0 === $error) {
328
            switch (true) {
329
                case (is_string($stream)):
330
                    $this->fileName = $stream;
331
                    break;
332
                case (is_resource($stream)):
333
                    $this->stream = new Stream($stream);
334
                    break;
335
                case ($stream instanceof StreamInterface):
336
                    $this->stream = $stream;
337
                    break;
338
                default:
339
                    throw new InvalidArgumentException(
340
                        "Invalid stream or file passed"
341
                    );
342
            }
343
        }
344
    }
345
346
    /**
347
     * Returns a description string depending on the upload error code passed
348
     *
349
     * @param int $error
350
     *
351
     * @return string
352
     */
353
    private function getErrorDescription(int $error): string
354
    {
355
        $errors = [
356
            0 => "There is no error, the file uploaded with success.",
357
            1 => "The uploaded file exceeds the upload_max_filesize directive in php.ini.",
358
            2 => "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.",
359
            3 => "The uploaded file was only partially uploaded.",
360
            4 => "No file was uploaded.",
361
            6 => "Missing a temporary folder.",
362
            7 => "Failed to write file to disk.",
363
            8 => "A PHP extension stopped the file upload."
364
        ];
365
366
        return Arr::get($errors, $error, "Unknown upload error");
367
    }
368
369
    /**
370
     * Store a file in the new location (stream)
371
     *
372
     * @param string $targetPath
373
     */
374
    private function storeFile(string $targetPath): void
375
    {
376
        $handle = fopen($targetPath, "w+b");
377
        if (false === $handle) {
378
            throw new InvalidArgumentException("Cannot write to file.");
379
        }
380
381
        $stream = $this->getStream();
382
383
        $stream->rewind();
384
385
        while (true !== $stream->eof()) {
386
            $data = $stream->read(2048);
387
388
            fwrite($handle, $data);
389
        }
390
391
        fclose($handle);
392
    }
393
}
394