Completed
Push — v3 ( 4ecdc9...abfb2c )
by Mihail
06:27
created

UploadedFile.php (1 issue)

Labels
Severity
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
    public function __construct(array $uploadedFile)
44
    {
45
        $this->size  = $uploadedFile['size'] ?? null;
46
        $this->file  = $uploadedFile['tmp_name'] ?? null;
47
        $this->name  = $uploadedFile['name'] ?? randomstring(9);
48
        $this->error = (int)($uploadedFile['error'] ?? \UPLOAD_ERR_OK);
49
50
        // Create a file out of the stream
51
        if ($this->file instanceof StreamInterface) {
52
            $file = sys_get_temp_dir() . '/' . $this->name;
53
            file_put_contents($file, $this->file->getContents());
54
            $this->file = $file;
55
        } elseif (false === is_string($this->file)) {
56
            throw UploadedFileException::fileNotSupported();
57
        } elseif (0 === strlen($this->file)) {
0 ignored issues
show
It seems like $this->file can also be of type null; however, parameter $string of strlen() 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

57
        } elseif (0 === strlen(/** @scrutinizer ignore-type */ $this->file)) {
Loading history...
58
            throw UploadedFileException::filenameCannotBeEmpty();
59
        }
60
        // Never trust the provided mime type
61
        $this->type = $this->getClientMediaType();
62
    }
63
64
    public function getStream(): StreamInterface
65
    {
66
        if ($this->moved) {
67
            throw UploadedFileException::streamNotAvailable();
68
        }
69
        return new FileStream($this->file, 'w+b');
70
    }
71
72
    public function moveTo($targetPath)
73
    {
74
        $this->assertUploadError();
75
        $this->assertTargetPath($targetPath);
76
        // @codeCoverageIgnoreStart
77
        try {
78
            $this->moved = ('cli' === php_sapi_name())
79
                ? rename($this->file, $targetPath)
80
                : move_uploaded_file($this->file, $targetPath);
81
82
            @unlink($this->file);
83
        } catch (Throwable $e) {
84
            throw new RuntimeException($e->getMessage());
85
        }
86
        // @codeCoverageIgnoreEnd
87
    }
88
89
    public function getSize(): ?int
90
    {
91
        return $this->size;
92
    }
93
94
    public function getError(): int
95
    {
96
        return $this->error;
97
    }
98
99
    public function getClientFilename(): ?string
100
    {
101
        return $this->name;
102
    }
103
104
    public function getClientMediaType(): ?string
105
    {
106
        try {
107
            return (new \finfo(\FILEINFO_MIME_TYPE))->file($this->file);
108
        } catch (Throwable $e) {
109
            return $this->type;
110
        }
111
    }
112
113
    private function assertUploadError(): void
114
    {
115
        if ($this->error !== \UPLOAD_ERR_OK) {
116
            throw new UploadedFileException($this->error);
117
        }
118
    }
119
120
    private function assertTargetPath($targetPath): void
121
    {
122
        if ($this->moved) {
123
            throw UploadedFileException::fileAlreadyMoved();
124
        }
125
        if (false === is_string($targetPath) || 0 === strlen($targetPath)) {
126
            throw UploadedFileException::targetPathIsInvalid();
127
        }
128
        if (false === is_dir($dirname = dirname($targetPath))) {
129
            @mkdir($dirname, 0777, true);
130
        }
131
    }
132
}
133
134
135
class UploadedFileException extends KodedException
136
{
137
    protected $messages = [
138
        \UPLOAD_ERR_INI_SIZE   => 'The uploaded file exceeds the "upload_max_filesize" directive in php.ini',
139
        \UPLOAD_ERR_FORM_SIZE  => 'The uploaded file exceeds the "MAX_FILE_SIZE" directive that was specified in the HTML form',
140
        \UPLOAD_ERR_PARTIAL    => 'The uploaded file was only partially uploaded',
141
        \UPLOAD_ERR_NO_FILE    => 'No file was uploaded',
142
        \UPLOAD_ERR_NO_TMP_DIR => 'The temporary directory to write to is missing',
143
        \UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
144
        \UPLOAD_ERR_EXTENSION  => 'A PHP extension stopped the file upload',
145
    ];
146
147
    public static function streamNotAvailable()
148
    {
149
        return new RuntimeException('Stream is not available, because the file was previously moved');
150
    }
151
152
    public static function targetPathIsInvalid()
153
    {
154
        return new InvalidArgumentException('The provided path for moveTo operation is not valid');
155
    }
156
157
    public static function fileAlreadyMoved()
158
    {
159
        return new RuntimeException('File is not available, because it was previously moved');
160
    }
161
162
    public static function fileNotSupported()
163
    {
164
        return new InvalidArgumentException('The uploaded file is not supported');
165
    }
166
167
    public static function filenameCannotBeEmpty()
168
    {
169
        return new InvalidArgumentException('Filename cannot be empty');
170
    }
171
}
172