Passed
Push — master ( 4ac306...d2aa89 )
by Mihail
05:30
created

UploadedFile   A

Complexity

Total Complexity 21

Size/Duplication

Total Lines 120
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 4
Bugs 0 Features 1
Metric Value
eloc 46
c 4
b 0
f 1
dl 0
loc 120
ccs 44
cts 44
cp 1
rs 10
wmc 21

9 Methods

Rating   Name   Duplication   Size   Complexity  
A getClientFilename() 0 3 1
A __construct() 0 20 4
A moveTo() 0 14 3
A getError() 0 3 1
A assertUploadError() 0 4 2
A getClientMediaType() 0 6 2
A getStream() 0 7 2
A assertTargetPath() 0 12 5
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 InvalidArgumentException;
16
use Koded\Exceptions\KodedException;
17
use Psr\Http\Message\{StreamInterface, UploadedFileInterface};
18
use RuntimeException;
19
use Throwable;
20
use function Koded\Stdlib\randomstring;
21
22
23
class UploadedFile implements UploadedFileInterface
24
{
25
    /** @var string|null */
26
    private $file;
27
28
    /** @var string|null */
29
    private $name;
30
31
    /** @var string|null */
32
    private $type;
33
34
    /** @var int|null */
35
    private $size;
36
37
    /** @var int See UPLOAD_ERR_* constants */
38
    private $error = \UPLOAD_ERR_OK;
39
40
    /** @var bool */
41
    private $moved = false;
42
43 36
    public function __construct(array $uploadedFile)
44
    {
45 36
        $this->size  = $uploadedFile['size'] ?? null;
46 36
        $this->file  = $uploadedFile['tmp_name'] ?? null;
47 36
        $this->name  = $uploadedFile['name'] ?? randomstring(9);
48 36
        $this->error = (int)($uploadedFile['error'] ?? \UPLOAD_ERR_OK);
49
50
        // Create a file out of the stream
51 36
        if ($this->file instanceof StreamInterface) {
52 1
            $file = sys_get_temp_dir() . '/' . $this->name;
53 1
            file_put_contents($file, $this->file->getContents());
54 1
            $this->file = $file;
55 35
        } elseif (false === is_string($this->file)) {
56 8
            throw UploadedFileException::fileNotSupported();
57 34
        } elseif (0 === strlen($this->file)) {
58 1
            throw UploadedFileException::filenameCannotBeEmpty();
59
        }
60
61
        // Never trust the provided mime type
62 35
        $this->type = $this->getClientMediaType();
63 35
    }
64
65
66 4
    public function getStream(): StreamInterface
67
    {
68 4
        if ($this->moved) {
69 2
            throw UploadedFileException::streamNotAvailable();
70
        }
71
72 2
        return new FileStream($this->file, 'w+b');
0 ignored issues
show
Bug introduced by
It seems like $this->file can also be of type null; however, parameter $filename of Koded\Http\FileStream::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

72
        return new FileStream(/** @scrutinizer ignore-type */ $this->file, 'w+b');
Loading history...
73
    }
74
75
76 14
    public function moveTo($targetPath)
77
    {
78 14
        $this->assertUploadError();
79 13
        $this->assertTargetPath($targetPath);
80
81
        // @codeCoverageIgnoreStart
82
        try {
83
            $this->moved = ('cli' === php_sapi_name())
84
                ? rename($this->file, $targetPath)
85
                : move_uploaded_file($this->file, $targetPath);
86
87
            @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

87
            /** @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...
88
        } catch (Throwable $e) {
89
            throw new RuntimeException($e->getMessage());
90
        }
91
        // @codeCoverageIgnoreEnd
92 6
    }
93
94
95 2
    public function getSize(): ?int
96
    {
97 2
        return $this->size;
98
    }
99
100
101 2
    public function getError(): int
102
    {
103 2
        return $this->error;
104
    }
105
106
107 2
    public function getClientFilename(): ?string
108
    {
109 2
        return $this->name;
110
    }
111
112
113 35
    public function getClientMediaType(): ?string
114
    {
115
        try {
116 35
            return (new \finfo(\FILEINFO_MIME_TYPE))->file($this->file);
117 1
        } catch (Throwable $e) {
118 1
            return $this->type;
119
        }
120
    }
121
122
123 14
    private function assertUploadError(): void
124
    {
125 14
        if ($this->error !== \UPLOAD_ERR_OK) {
126 1
            throw new UploadedFileException($this->error);
127
        }
128 13
    }
129
130
131 13
    private function assertTargetPath($targetPath): void
132
    {
133 13
        if ($this->moved) {
134 2
            throw UploadedFileException::fileAlreadyMoved();
135
        }
136
137 13
        if (false === is_string($targetPath) || 0 === strlen($targetPath)) {
138 7
            throw UploadedFileException::targetPathIsInvalid();
139
        }
140
141 6
        if (false === is_dir($dirname = dirname($targetPath))) {
142 1
            @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

142
            /** @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...
143
        }
144 6
    }
145
}
146
147
148
class UploadedFileException extends KodedException
149
{
150
    protected $messages = [
151
        \UPLOAD_ERR_INI_SIZE   => 'The uploaded file exceeds the "upload_max_filesize" directive in php.ini',
152
        \UPLOAD_ERR_FORM_SIZE  => 'The uploaded file exceeds the "MAX_FILE_SIZE" directive that was specified in the HTML form',
153
        \UPLOAD_ERR_PARTIAL    => 'The uploaded file was only partially uploaded',
154
        \UPLOAD_ERR_NO_FILE    => 'No file was uploaded',
155
        \UPLOAD_ERR_NO_TMP_DIR => 'The temporary directory to write to is missing',
156
        \UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
157
        \UPLOAD_ERR_EXTENSION  => 'A PHP extension stopped the file upload',
158
    ];
159
160 2
    public static function streamNotAvailable()
161
    {
162 2
        return new RuntimeException('Stream is not available, because the file was previously moved');
163
    }
164
165 7
    public static function targetPathIsInvalid()
166
    {
167 7
        return new InvalidArgumentException('The provided path for moveTo operation is not valid');
168
    }
169
170 2
    public static function fileAlreadyMoved()
171
    {
172 2
        return new RuntimeException('File is not available, because it was previously moved');
173
    }
174
175 8
    public static function fileNotSupported()
176
    {
177 8
        return new InvalidArgumentException('The uploaded file is not supported');
178
    }
179
180 1
    public static function filenameCannotBeEmpty()
181
    {
182 1
        return new InvalidArgumentException('Filename cannot be empty');
183
    }
184
}
185