UploadedFile::moveTo()   B
last analyzed

Complexity

Conditions 9
Paths 7

Size

Total Lines 44
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 17
dl 0
loc 44
rs 8.0555
c 0
b 0
f 0
cc 9
nc 7
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Valkyrja Framework package.
7
 *
8
 * (c) Melech Mizrachi <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Valkyrja\Http\Message\File;
15
16
use Override;
0 ignored issues
show
Bug introduced by
The type Override was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
17
use Valkyrja\Http\Message\Exception\InvalidArgumentException;
18
use Valkyrja\Http\Message\File\Contract\UploadedFile as Contract;
19
use Valkyrja\Http\Message\File\Enum\UploadError;
20
use Valkyrja\Http\Message\File\Exception\AlreadyMovedException;
21
use Valkyrja\Http\Message\File\Exception\InvalidDirectoryException;
22
use Valkyrja\Http\Message\File\Exception\InvalidUploadedFileException;
23
use Valkyrja\Http\Message\File\Exception\MoveFailureException;
24
use Valkyrja\Http\Message\File\Exception\UnableToWriteFileException;
25
use Valkyrja\Http\Message\File\Exception\UploadErrorException;
0 ignored issues
show
Bug introduced by
The type Valkyrja\Http\Message\Fi...on\UploadErrorException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
26
use Valkyrja\Http\Message\Stream\Contract\Stream;
27
use Valkyrja\Http\Message\Stream\Exception\InvalidStreamException;
28
use Valkyrja\Http\Message\Stream\Stream as HttpStream;
29
30
use function dirname;
31
use function fclose;
32
use function fopen;
33
use function fwrite;
34
use function is_dir;
35
use function is_file;
36
use function is_writable;
37
use function move_uploaded_file;
38
use function str_starts_with;
39
use function unlink;
40
41
use const PHP_SAPI;
42
43
/**
44
 * Class UploadedFile.
45
 *
46
 * @author Melech Mizrachi
47
 */
48
class UploadedFile implements Contract
49
{
50
    /**
51
     * Whether the file has been moved yet.
52
     *
53
     * @var bool
54
     */
55
    protected bool $hasBeenMoved = false;
56
57
    /**
58
     * UploadedFile constructor.
59
     *
60
     * @param string|null $file        [optional] The file if not passed stream is required
61
     * @param Stream|null $stream      [optional] The stream if not passed file is required
62
     * @param UploadError $uploadError [optional] The upload error
63
     * @param int|null    $size        [optional] The file size
64
     * @param string|null $fileName    [optional] The file name
65
     * @param string|null $mediaType   [optional] The file media type
66
     *
67
     * @throws InvalidArgumentException
68
     */
69
    public function __construct(
70
        protected string|null $file = null,
71
        protected Stream|null $stream = null,
72
        protected UploadError $uploadError = UploadError::OK,
73
        protected int|null $size = null,
74
        protected string|null $fileName = null,
75
        protected string|null $mediaType = null
76
    ) {
77
        // If the file is not set and the stream is not set
78
        if ($uploadError === UploadError::OK && $file === null && $stream === null) {
79
            // Throw an invalid argument exception as on or the other is required
80
            throw new InvalidUploadedFileException('One of file or stream are required');
81
        }
82
    }
83
84
    /**
85
     * @inheritDoc
86
     */
87
    #[Override]
88
    public function getStream(): Stream
89
    {
90
        // If the error status is not OK
91
        if ($this->uploadError !== UploadError::OK) {
92
            // Throw a runtime exception as there's been an uploaded file error
93
            throw new UploadErrorException($this->uploadError);
94
        }
95
96
        // If the file has already been moved
97
        if ($this->hasBeenMoved) {
98
            // Throw a runtime exception as subsequent moves are not allowed in PSR-7
99
            throw new AlreadyMovedException('Cannot retrieve stream after it has already been moved');
100
        }
101
102
        // If the stream has been set
103
        if ($this->stream !== null) {
104
            // Return the stream
105
            return $this->stream;
106
        }
107
108
        // This should be impossible, but here just in case __construct is overridden
109
        if ($this->file === null) {
110
            throw new InvalidUploadedFileException('One of file or stream are required');
111
        }
112
113
        // Set the stream as a new native stream
114
        $this->stream = new HttpStream($this->file);
115
116
        return $this->stream;
117
    }
118
119
    /**
120
     * @inheritDoc
121
     */
122
    #[Override]
123
    public function moveTo(string $targetPath): void
124
    {
125
        // If the error status is not OK
126
        if ($this->uploadError !== UploadError::OK) {
127
            // Throw a runtime exception as there's been an uploaded file error
128
            throw new UploadErrorException($this->uploadError);
129
        }
130
131
        // If the file has already been moved
132
        if ($this->hasBeenMoved) {
133
            // Throw a runtime exception as subsequent moves are not allowed
134
            // in PSR-7
135
            throw new AlreadyMovedException('Cannot move file after it has already been moved');
136
        }
137
138
        $targetDirectory = $this->getDirectoryName($targetPath);
139
140
        // If the target directory is not a directory
141
        // or the target directory is not writable
142
        if (! $this->isDir($targetDirectory) || ! $this->isWritable($targetDirectory)) {
143
            // Throw a runtime exception
144
            throw new InvalidDirectoryException(
145
                "The target directory `$targetDirectory` does not exists or is not writable"
146
            );
147
        }
148
149
        if ($this->shouldWriteStream()) {
150
            // Non-SAPI environment, or no filename present
151
            $this->writeStream($targetPath);
152
153
            $this->stream?->close();
154
155
            if ($this->file !== null && is_file($this->file)) {
156
                $this->deleteFile($this->file);
157
            }
158
        } elseif (! $this->moveUploadedFile($this->file ?? '', $targetPath)) {
159
            // Otherwise try to use the move_uploaded_file function
160
            // and if the move_uploaded_file function call failed
161
            // Throw a runtime exception
162
            throw new MoveFailureException('Error occurred while moving uploaded file');
163
        }
164
165
        $this->hasBeenMoved = true;
166
    }
167
168
    /**
169
     * @inheritDoc
170
     */
171
    #[Override]
172
    public function getSize(): int|null
173
    {
174
        return $this->size;
175
    }
176
177
    /**
178
     * @inheritDoc
179
     */
180
    #[Override]
181
    public function getError(): UploadError
182
    {
183
        return $this->uploadError;
184
    }
185
186
    /**
187
     * @inheritDoc
188
     */
189
    #[Override]
190
    public function getClientFilename(): string|null
191
    {
192
        return $this->fileName;
193
    }
194
195
    /**
196
     * @inheritDoc
197
     */
198
    #[Override]
199
    public function getClientMediaType(): string|null
200
    {
201
        return $this->mediaType;
202
    }
203
204
    /**
205
     * Write the stream to a path.
206
     *
207
     * @param string $path The path to write the stream to
208
     *
209
     * @throws InvalidStreamException
210
     *
211
     * @return void
212
     */
213
    protected function writeStream(string $path): void
214
    {
215
        // Attempt to open the path specified
216
        $handle = $this->openStream($path);
217
218
        // If the handler failed to open
219
        if ($handle === false) {
220
            // Throw a runtime exception
221
            throw new UnableToWriteFileException('Unable to write to designated path');
222
        }
223
224
        // Get the stream
225
        $stream = $this->getStream();
226
        // Rewind the stream
227
        $stream->rewind();
228
229
        // While the end of the stream hasn't been reached
230
        while (! $stream->eof()) {
231
            // Write the stream's contents to the handler
232
            $this->writeToStream($handle, $stream->read(4096));
233
        }
234
235
        // Close the path
236
        $this->closeStream($handle);
237
    }
238
239
    /**
240
     * Get the PHP_SAPI value.
241
     *
242
     * @return string
243
     */
244
    protected function getPhpSapi(): string
245
    {
246
        return PHP_SAPI;
247
    }
248
249
    /**
250
     * Determine if a new stream should be opened to move the file.
251
     *
252
     * @return bool
253
     */
254
    protected function shouldWriteStream(): bool
255
    {
256
        $sapi = $this->getPhpSapi();
257
258
        // If the PHP_SAPI value is empty
259
        // or there is no file
260
        // or the PHP_SAPI value is set to a CLI environment
261
        return empty($sapi)
262
            || $this->file === null
263
            || $this->file === ''
264
            || str_starts_with($sapi, 'cli')
265
            || str_starts_with($sapi, 'phpdbg');
266
    }
267
268
    /**
269
     * Get the directory name for a given path.
270
     *
271
     * @param string $path The path
272
     *
273
     * @return string
274
     */
275
    protected function getDirectoryName(string $path): string
276
    {
277
        return dirname($path);
278
    }
279
280
    /**
281
     * Open a stream.
282
     *
283
     * @return resource|false
284
     */
285
    protected function openStream(string $filename)
286
    {
287
        return fopen($filename, 'wb+');
288
    }
289
290
    /**
291
     * Write a stream.
292
     *
293
     * @param resource $stream The stream
294
     */
295
    protected function writeToStream($stream, string $data): int|false
296
    {
297
        return fwrite($stream, $data);
298
    }
299
300
    /**
301
     * Close a stream.
302
     *
303
     * @param resource $stream The stream
304
     *
305
     * @return bool
306
     */
307
    protected function closeStream($stream): bool
308
    {
309
        return fclose($stream);
310
    }
311
312
    /**
313
     * Determine if a filename is a directory.
314
     *
315
     * @param string $filename The file
316
     *
317
     * @return bool
318
     */
319
    protected function isDir(string $filename): bool
320
    {
321
        return is_dir($filename);
322
    }
323
324
    /**
325
     * Determine if a file is writable.
326
     *
327
     * @param string $filename The file
328
     *
329
     * @return bool
330
     */
331
    protected function isWritable(string $filename): bool
332
    {
333
        return is_writable($filename);
334
    }
335
336
    /**
337
     * Move an uploaded file.
338
     *
339
     * @param string $from Path to move from
340
     * @param string $to   Path to move to
341
     *
342
     * @return bool
343
     */
344
    protected function moveUploadedFile(string $from, string $to): bool
345
    {
346
        return move_uploaded_file($from, $to);
347
    }
348
349
    /**
350
     * Delete a file.
351
     *
352
     * @param string $filename
353
     *
354
     * @return bool
355
     */
356
    protected function deleteFile(string $filename): bool
357
    {
358
        return unlink($filename);
359
    }
360
}
361