UploadedFile::getSize()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
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
        }
159
        // Otherwise try to use the move_uploaded_file function
160
        // and if the move_uploaded_file function call failed
161
        elseif (! $this->moveUploadedFile($this->file ?? '', $targetPath)) {
162
            // Throw a runtime exception
163
            throw new MoveFailureException('Error occurred while moving uploaded file');
164
        }
165
166
        $this->hasBeenMoved = true;
167
    }
168
169
    /**
170
     * @inheritDoc
171
     */
172
    #[Override]
173
    public function getSize(): int|null
174
    {
175
        return $this->size;
176
    }
177
178
    /**
179
     * @inheritDoc
180
     */
181
    #[Override]
182
    public function getError(): UploadError
183
    {
184
        return $this->uploadError;
185
    }
186
187
    /**
188
     * @inheritDoc
189
     */
190
    #[Override]
191
    public function getClientFilename(): string|null
192
    {
193
        return $this->fileName;
194
    }
195
196
    /**
197
     * @inheritDoc
198
     */
199
    #[Override]
200
    public function getClientMediaType(): string|null
201
    {
202
        return $this->mediaType;
203
    }
204
205
    /**
206
     * Write the stream to a path.
207
     *
208
     * @param string $path The path to write the stream to
209
     *
210
     * @throws InvalidStreamException
211
     *
212
     * @return void
213
     */
214
    protected function writeStream(string $path): void
215
    {
216
        // Attempt to open the path specified
217
        $handle = $this->openStream($path);
218
219
        // If the handler failed to open
220
        if ($handle === false) {
221
            // Throw a runtime exception
222
            throw new UnableToWriteFileException('Unable to write to designated path');
223
        }
224
225
        // Get the stream
226
        $stream = $this->getStream();
227
        // Rewind the stream
228
        $stream->rewind();
229
230
        // While the end of the stream hasn't been reached
231
        while (! $stream->eof()) {
232
            // Write the stream's contents to the handler
233
            $this->writeToStream($handle, $stream->read(4096));
234
        }
235
236
        // Close the path
237
        $this->closeStream($handle);
238
    }
239
240
    /**
241
     * Get the PHP_SAPI value.
242
     *
243
     * @return string
244
     */
245
    protected function getPhpSapi(): string
246
    {
247
        return PHP_SAPI;
248
    }
249
250
    /**
251
     * Determine if a new stream should be opened to move the file.
252
     *
253
     * @return bool
254
     */
255
    protected function shouldWriteStream(): bool
256
    {
257
        $sapi = $this->getPhpSapi();
258
259
        // If the PHP_SAPI value is empty
260
        // or there is no file
261
        // or the PHP_SAPI value is set to a CLI environment
262
        return empty($sapi)
263
            || $this->file === null
264
            || $this->file === ''
265
            || str_starts_with($sapi, 'cli')
266
            || str_starts_with($sapi, 'phpdbg');
267
    }
268
269
    /**
270
     * Get the directory name for a given path.
271
     *
272
     * @param string $path The path
273
     *
274
     * @return string
275
     */
276
    protected function getDirectoryName(string $path): string
277
    {
278
        return dirname($path);
279
    }
280
281
    /**
282
     * Open a stream.
283
     *
284
     * @return resource|false
285
     */
286
    protected function openStream(string $filename)
287
    {
288
        return fopen($filename, 'wb+');
289
    }
290
291
    /**
292
     * Write a stream.
293
     *
294
     * @param resource $stream The stream
295
     */
296
    protected function writeToStream($stream, string $data): int|false
297
    {
298
        return fwrite($stream, $data);
299
    }
300
301
    /**
302
     * Close a stream.
303
     *
304
     * @param resource $stream The stream
305
     *
306
     * @return bool
307
     */
308
    protected function closeStream($stream): bool
309
    {
310
        return fclose($stream);
311
    }
312
313
    /**
314
     * Determine if a filename is a directory.
315
     *
316
     * @param string $filename The file
317
     *
318
     * @return bool
319
     */
320
    protected function isDir(string $filename): bool
321
    {
322
        return is_dir($filename);
323
    }
324
325
    /**
326
     * Determine if a file is writable.
327
     *
328
     * @param string $filename The file
329
     *
330
     * @return bool
331
     */
332
    protected function isWritable(string $filename): bool
333
    {
334
        return is_writable($filename);
335
    }
336
337
    /**
338
     * Move an uploaded file.
339
     *
340
     * @param string $from Path to move from
341
     * @param string $to   Path to move to
342
     *
343
     * @return bool
344
     */
345
    protected function moveUploadedFile(string $from, string $to): bool
346
    {
347
        return move_uploaded_file($from, $to);
348
    }
349
350
    /**
351
     * Delete a file.
352
     *
353
     * @param string $filename
354
     *
355
     * @return bool
356
     */
357
    protected function deleteFile(string $filename): bool
358
    {
359
        return unlink($filename);
360
    }
361
}
362