Passed
Pull Request — master (#15)
by Evgeniy
02:21
created

UploadedFile::getClientFilename()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace HttpSoft\Message;
6
7
use InvalidArgumentException;
8
use Psr\Http\Message\StreamInterface;
9
use Psr\Http\Message\UploadedFileInterface;
10
use RuntimeException;
11
12
use function array_key_exists;
13
use function array_keys;
14
use function dirname;
15
use function fclose;
16
use function fopen;
17
use function fwrite;
18
use function gettype;
19
use function get_class;
20
use function implode;
21
use function is_dir;
22
use function is_object;
23
use function is_resource;
24
use function is_string;
25
use function is_writable;
26
use function move_uploaded_file;
27
use function rename;
28
use function sprintf;
29
use function strpos;
30
31
use const PHP_SAPI;
32
use const UPLOAD_ERR_OK;
33
use const UPLOAD_ERR_INI_SIZE;
34
use const UPLOAD_ERR_FORM_SIZE;
35
use const UPLOAD_ERR_PARTIAL;
36
use const UPLOAD_ERR_NO_FILE;
37
use const UPLOAD_ERR_NO_TMP_DIR;
38
use const UPLOAD_ERR_CANT_WRITE;
39
use const UPLOAD_ERR_EXTENSION;
40
41
final class UploadedFile implements UploadedFileInterface
42
{
43
    /**
44
     * @const array
45
     * @link https://www.php.net/manual/en/features.file-upload.errors.php
46
     */
47
    private const ERRORS = [
48
        UPLOAD_ERR_OK => 'There is no error, the file uploaded with success.',
49
        UPLOAD_ERR_INI_SIZE => 'The uploaded file exceeds the upload_max_filesize directive in php.ini.',
50
        UPLOAD_ERR_FORM_SIZE => 'The uploaded file exceeds the MAX_FILE_SIZE directive'
51
            . ' that was specified in the HTML form.',
52
        UPLOAD_ERR_PARTIAL => 'The uploaded file was only partially uploaded.',
53
        UPLOAD_ERR_NO_FILE => 'No file was uploaded.',
54
        UPLOAD_ERR_NO_TMP_DIR => 'Missing a temporary folder.',
55
        UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk.',
56
        UPLOAD_ERR_EXTENSION => 'A PHP extension stopped the file upload.',
57
    ];
58
59
    /**
60
     * @var StreamInterface|null
61
     */
62
    private ?StreamInterface $stream = null;
63
64
    /**
65
     * @var string|null
66
     */
67
    private ?string $file = null;
68
69
    /**
70
     * @var int
71
     */
72
    private int $size;
73
74
    /**
75
     * @var int
76
     */
77
    private int $error;
78
79
    /**
80
     * @var string|null
81
     */
82
    private ?string $clientFilename;
83
84
    /**
85
     * @var string|null
86
     */
87
    private ?string $clientMediaType;
88
89
    /**
90
     * @var bool
91
     */
92
    private bool $isMoved = false;
93
94
    /**
95
     * @param StreamInterface|string|resource $streamOrFile
96
     * @param int $size
97
     * @param int $error
98
     * @param string|null $clientFilename
99
     * @param string|null $clientMediaType
100
     * @psalm-suppress DocblockTypeContradiction
101
     * @psalm-suppress RedundantConditionGivenDocblockType
102
     */
103 59
    public function __construct(
104
        $streamOrFile,
105
        int $size,
106
        int $error,
107
        string $clientFilename = null,
108
        string $clientMediaType = null
109
    ) {
110 59
        if (!array_key_exists($error, self::ERRORS)) {
111 4
            throw new InvalidArgumentException(sprintf(
112 4
                '"%s" is not valid error status for "UploadedFile". It must be one of "UPLOAD_ERR_*" constants:  "%s".',
113 4
                $error,
114 4
                implode('", "', array_keys(self::ERRORS))
115 4
            ));
116
        }
117
118 55
        $this->size = $size;
119 55
        $this->error = $error;
120 55
        $this->clientFilename = $clientFilename;
121 55
        $this->clientMediaType = $clientMediaType;
122
123 55
        if ($error !== UPLOAD_ERR_OK) {
124 14
            return;
125
        }
126
127 41
        if (is_string($streamOrFile)) {
128 3
            $this->file = $streamOrFile;
129 3
            return;
130
        }
131
132 39
        if (is_resource($streamOrFile)) {
133 1
            $this->stream = new Stream($streamOrFile);
134 1
            return;
135
        }
136
137 39
        if ($streamOrFile instanceof StreamInterface) {
0 ignored issues
show
introduced by
$streamOrFile is always a sub-type of Psr\Http\Message\StreamInterface.
Loading history...
138 30
            $this->stream = $streamOrFile;
139 30
            return;
140
        }
141
142 9
        throw new InvalidArgumentException(sprintf(
143 9
            '"%s" is not valid stream or file provided for "UploadedFile".',
144 9
            (is_object($streamOrFile) ? get_class($streamOrFile) : gettype($streamOrFile))
145 9
        ));
146
    }
147
148
    /**
149
     * {@inheritdoc}
150
     *
151
     * @psalm-suppress PossiblyNullArgument
152
     */
153 14
    public function getStream(): StreamInterface
154
    {
155 14
        if ($this->error !== UPLOAD_ERR_OK) {
156 7
            throw new RuntimeException(self::ERRORS[$this->error]);
157
        }
158
159 7
        if ($this->isMoved) {
160 2
            throw new RuntimeException('The stream is not available because it has been moved.');
161
        }
162
163 5
        if ($this->stream === null) {
164 2
            $this->stream = new Stream($this->file, 'r+');
165
        }
166
167 5
        return $this->stream;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->stream could return the type null which is incompatible with the type-hinted return Psr\Http\Message\StreamInterface. Consider adding an additional type-check to rule them out.
Loading history...
168
    }
169
170
    /**
171
     * {@inheritdoc}
172
     *
173
     * @psalm-suppress DocblockTypeContradiction
174
     */
175 25
    public function moveTo($targetPath): void
176
    {
177 25
        if ($this->error !== UPLOAD_ERR_OK) {
178 7
            throw new RuntimeException(self::ERRORS[$this->error]);
179
        }
180
181 18
        if ($this->isMoved) {
182 2
            throw new RuntimeException('The file cannot be moved because it has already been moved.');
183
        }
184
185 18
        if (!is_string($targetPath)) {
186 9
            throw new InvalidArgumentException(sprintf(
187 9
                '"%s" is not valid target path for move. It must be a string type.',
188 9
                (is_object($targetPath) ? get_class($targetPath) : gettype($targetPath))
189 9
            ));
190
        }
191
192 9
        if (empty($targetPath)) {
193 1
            throw new InvalidArgumentException('Target path is not valid for move. It must be a non-empty string.');
194
        }
195
196 8
        $targetDirectory = dirname($targetPath);
197
198 8
        if (!is_dir($targetDirectory) || !is_writable($targetDirectory)) {
199 1
            throw new RuntimeException(sprintf(
200 1
                'The target directory "%s" does not exist or is not writable.',
201 1
                $targetDirectory
202 1
            ));
203
        }
204
205 7
        $this->moveOrWriteFile($targetPath);
206 7
        $this->isMoved = true;
207
    }
208
209
    /**
210
     * {@inheritdoc}
211
     */
212 4
    public function getSize(): ?int
213
    {
214 4
        return $this->size;
215
    }
216
217
    /**
218
     * {@inheritdoc}
219
     */
220 3
    public function getError(): int
221
    {
222 3
        return $this->error;
223
    }
224
225
    /**
226
     * {@inheritdoc}
227
     */
228 4
    public function getClientFilename(): ?string
229
    {
230 4
        return $this->clientFilename;
231
    }
232
233
    /**
234
     * {@inheritdoc}
235
     */
236 4
    public function getClientMediaType(): ?string
237
    {
238 4
        return $this->clientMediaType;
239
    }
240
241
    /**
242
     * Moves if used in an SAPI environment where $_FILES is populated, when writing
243
     * files via is_uploaded_file() and move_uploaded_file() or writes If SAPI is not used.
244
     *
245
     * @param string $targetPath
246
     * @psalm-suppress PossiblyNullReference
247
     */
248 7
    private function moveOrWriteFile(string $targetPath): void
249
    {
250 7
        if ($this->file !== null) {
251
            $isCliEnv = (!PHP_SAPI || strpos(PHP_SAPI, 'cli') === 0 || strpos(PHP_SAPI, 'phpdbg') === 0);
252
253
            if (!($isCliEnv ? rename($this->file, $targetPath) : move_uploaded_file($this->file, $targetPath))) {
254
                throw new RuntimeException(sprintf('Uploaded file could not be moved to "%s".', $targetPath));
255
            }
256
257
            return;
258
        }
259
260 7
        if (!$file = fopen($targetPath, 'wb+')) {
261
            throw new RuntimeException(sprintf('Unable to write to "%s".', $targetPath));
262
        }
263
264 7
        $this->stream->rewind();
0 ignored issues
show
Bug introduced by
The method rewind() does not exist on null. ( Ignorable by Annotation )

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

264
        $this->stream->/** @scrutinizer ignore-call */ 
265
                       rewind();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
265
266 7
        while (!$this->stream->eof()) {
267 7
            fwrite($file, $this->stream->read(512000));
268
        }
269
270 7
        fclose($file);
271
    }
272
}
273