Passed
Pull Request — master (#17)
by Mihail
15:10
created

UploadedFile   A

Complexity

Total Complexity 21

Size/Duplication

Total Lines 103
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 5
Bugs 0 Features 2
Metric Value
eloc 49
c 5
b 0
f 2
dl 0
loc 103
rs 10
ccs 44
cts 44
cp 1
wmc 21

10 Methods

Rating   Name   Duplication   Size   Complexity  
A getClientFilename() 0 3 1
A __construct() 0 8 1
A moveTo() 0 13 3
A getError() 0 3 1
A prepareFile() 0 15 4
A assertUploadError() 0 4 2
A getClientMediaType() 0 6 2
A getStream() 0 6 2
A assertTargetPath() 0 10 4
A getSize() 0 3 1
1
<?php
2
3
/*
4
 * This file is part of the Koded package.
5
 *
6
 * (c) Mihail Binev <[email protected]>
7
 *
8
 * Please view the LICENSE distributed with this source code
9
 * for the full copyright and license information.
10
 *
11
 */
12
13
namespace Koded\Http;
14
15
use finfo;
16
use InvalidArgumentException;
17
use Koded\Exceptions\KodedException;
18
use RuntimeException;
19
use Psr\Http\Message\{StreamInterface, UploadedFileInterface};
20
use Throwable;
21
use function dirname;
22
use function file_put_contents;
23
use function get_debug_type;
24
use function is_dir;
25
use function is_string;
26
use function Koded\Stdlib\randomstring;
27
use function mb_strlen;
28
use function mkdir;
29
use function move_uploaded_file;
30
use function php_sapi_name;
31
use function rename;
32
use function sys_get_temp_dir;
33
use function unlink;
34
use const FILEINFO_MIME_TYPE;
35
use const UPLOAD_ERR_CANT_WRITE;
36
use const UPLOAD_ERR_EXTENSION;
37
use const UPLOAD_ERR_FORM_SIZE;
38
use const UPLOAD_ERR_INI_SIZE;
39
use const UPLOAD_ERR_NO_FILE;
40
use const UPLOAD_ERR_NO_TMP_DIR;
41
use const UPLOAD_ERR_OK;
42
use const UPLOAD_ERR_PARTIAL;
43 36
44
class UploadedFile implements UploadedFileInterface
45 36
{
46 36
    private mixed $file;
47 36
    private mixed $name;
48 36
    private ?string $type;
49
    private ?int $size;
50
    private int  $error;
51 36
    private bool $moved = false;
52 1
53 1
    public function __construct(array $uploadedFile)
54 1
    {
55 35
        $this->file  = $uploadedFile['tmp_name'] ?? null;
56 8
        $this->name  = $uploadedFile['name'] ?? randomstring(9);
57 34
        $this->size  = $uploadedFile['size'] ?? null;
58 1
        $this->prepareFile();
59
        $this->type = $this->getClientMediaType();
60
        $this->error = (int)($uploadedFile['error'] ?? UPLOAD_ERR_OK);
61
    }
62 35
63 35
    public function getStream(): StreamInterface
64
    {
65
        if ($this->moved) {
66 4
            throw UploadedFileException::streamNotAvailable();
67
        }
68 4
        return new FileStream($this->file, 'w+b');
69 2
    }
70
71
    public function moveTo(string $targetPath): void
72 2
    {
73
        $this->assertUploadError();
74
        $this->assertTargetPath($targetPath);
75
        // @codeCoverageIgnoreStart
76 14
        try {
77
            $this->moved = ('cli' === php_sapi_name())
78 14
                ? rename($this->file, $targetPath)
79 13
                : move_uploaded_file($this->file, $targetPath);
80
81
            @unlink($this->file);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

81
            /** @scrutinizer ignore-unhandled */ @unlink($this->file);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
82
        } catch (Throwable $e) {
83
            throw new RuntimeException($e->getMessage());
84
        }
85
        // @codeCoverageIgnoreEnd
86
    }
87
88
    public function getSize(): ?int
89
    {
90
        return $this->size;
91
    }
92 6
93
    public function getError(): int
94
    {
95 2
        return $this->error;
96
    }
97 2
98
    public function getClientFilename(): ?string
99
    {
100
        return $this->name;
101 2
    }
102
103 2
    public function getClientMediaType(): ?string
104
    {
105
        try {
106
            return @(new finfo(FILEINFO_MIME_TYPE))->file($this->file);
107 2
        } catch (Throwable) {
108
            return $this->type;
109 2
        }
110
    }
111
112
    private function assertUploadError(): void
113 35
    {
114
        if ($this->error !== UPLOAD_ERR_OK) {
115
            throw new UploadedFileException($this->error);
116 35
        }
117 1
    }
118 1
119
    private function assertTargetPath(string $targetPath): void
120
    {
121
        if ($this->moved) {
122
            throw UploadedFileException::fileAlreadyMoved();
123 14
        }
124
        if (empty($targetPath)) {
125 14
            throw UploadedFileException::targetPathIsInvalid();
126 1
        }
127
        if (false === is_dir($dirname = dirname($targetPath))) {
128 13
            @mkdir($dirname, 0777, true);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for mkdir(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

128
            /** @scrutinizer ignore-unhandled */ @mkdir($dirname, 0777, true);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
129
        }
130
    }
131 13
132
    private function prepareFile(): void
133 13
    {
134 2
        if ($this->file instanceof StreamInterface) {
135
            // Create a temporary file out of the stream object
136
            $this->size = $this->file->getSize();
137 13
            $file = sys_get_temp_dir() . '/' . $this->name;
138 7
            file_put_contents($file, $this->file->getContents());
139
            $this->file = $file;
140
            return;
141 6
        }
142 1
        if (false === is_string($this->file)) {
143
            throw UploadedFileException::fileNotSupported($this->file);
144 6
        }
145
        if (0 === mb_strlen($this->file)) {
146
            throw UploadedFileException::filenameCannotBeEmpty();
147
        }
148
    }
149
}
150
151
152
class UploadedFileException extends KodedException
153
{
154
    protected array $messages = [
155
        UPLOAD_ERR_INI_SIZE   => 'The uploaded file exceeds the "upload_max_filesize" directive in php.ini',
156
        UPLOAD_ERR_FORM_SIZE  => 'The uploaded file exceeds the "MAX_FILE_SIZE" directive that was specified in the HTML form',
157
        UPLOAD_ERR_PARTIAL    => 'The uploaded file was only partially uploaded',
158
        UPLOAD_ERR_NO_FILE    => 'No file was uploaded',
159
        UPLOAD_ERR_NO_TMP_DIR => 'The temporary directory to write to is missing',
160 2
        UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
161
        UPLOAD_ERR_EXTENSION  => 'A PHP extension stopped the file upload',
162 2
    ];
163
164
    public static function streamNotAvailable(): RuntimeException
165 7
    {
166
        return new RuntimeException('Stream is not available, because the file was previously moved');
167 7
    }
168
169
    public static function targetPathIsInvalid(): InvalidArgumentException
170 2
    {
171
        return new InvalidArgumentException('The provided path for moveTo operation is not valid');
172 2
    }
173
174
    public static function fileAlreadyMoved(): RuntimeException
175 8
    {
176
        return new RuntimeException('File is not available, because it was previously moved');
177 8
    }
178
179
    public static function fileNotSupported(mixed $file): InvalidArgumentException
180 1
    {
181
        return new InvalidArgumentException(sprintf(
182 1
            'The uploaded file is not supported, expected string, %s given',
183
            get_debug_type($file)
184
        ));
185
    }
186
187
    public static function filenameCannotBeEmpty(): InvalidArgumentException
188
    {
189
        return new InvalidArgumentException('Filename cannot be empty');
190
    }
191
}
192