UploadedFile::isDir()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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