Issues (16)

src/UploadedFile.php (6 issues)

1
<?php
2
3
/**
4
 * Platine HTTP
5
 *
6
 * Platine HTTP Message is the implementation of PSR 7
7
 *
8
 * This content is released under the MIT License (MIT)
9
 *
10
 * Copyright (c) 2020 Platine HTTP
11
 * Copyright (c) 2019 Dion Chaika
12
 *
13
 * Permission is hereby granted, free of charge, to any person obtaining a copy
14
 * of this software and associated documentation files (the "Software"), to deal
15
 * in the Software without restriction, including without limitation the rights
16
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
 * copies of the Software, and to permit persons to whom the Software is
18
 * furnished to do so, subject to the following conditions:
19
 *
20
 * The above copyright notice and this permission notice shall be included in all
21
 * copies or substantial portions of the Software.
22
 *
23
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
 * SOFTWARE.
30
 */
31
32
/**
33
 *  @file UploadedFile.php
34
 *
35
 *  The UploadedFile class that represent the data for file upload
36
 *
37
 *  @package    Platine\Http
38
 *  @author Platine Developers Team
39
 *  @copyright  Copyright (c) 2020
40
 *  @license    http://opensource.org/licenses/MIT  MIT License
41
 *  @link   https://www.platine-php.com
42
 *  @version 1.0.0
43
 *  @filesource
44
 */
45
46
declare(strict_types=1);
47
48
namespace Platine\Http;
49
50
use InvalidArgumentException;
51
use RuntimeException;
52
53
/**
54
 * @class UploadedFile
55
 * @package Platine\Http
56
 */
57
class UploadedFile implements UploadedFileInterface
58
{
59
    /**
60
     * The uploaded file name
61
     * @var string
62
     */
63
    protected ?string $filename = null;
64
65
    /**
66
     * Whether the uploaded file is moved
67
     * @var bool
68
     */
69
    protected bool $moved = false;
70
71
    /**
72
     * The uploaded file stream
73
     * @var StreamInterface
74
     */
75
    protected ?StreamInterface $stream = null;
76
77
    /**
78
     *  The uploaded file size
79
     * @var int|null
80
     */
81
    protected ?int $size;
82
83
    /**
84
     *  The uploaded file error
85
     * @var int
86
     */
87
    protected int $error = UPLOAD_ERR_OK;
88
89
    /**
90
     * The uploaded file client name
91
     * @var string|null
92
     */
93
    protected ?string $clientFilename;
94
95
    /**
96
     * The uploaded file client media type
97
     * @var string|null
98
     */
99
    protected ?string $clientMediaType;
100
101
    /**
102
     * Create new uploaded file instance
103
     *
104
     * @param string|StreamInterface $filenameOrStream the filename or stream
105
     * @param int|null $size the upload file size
106
     * @param int $error the upload error code
107
     * @param string|null $clientFilename
108
     * @param string|null $clientMediaType
109
     */
110
    public function __construct(
111
        string|StreamInterface $filenameOrStream,
112
        ?int $size = null,
113
        int $error = UPLOAD_ERR_OK,
114
        ?string $clientFilename = null,
115
        ?string $clientMediaType = null
116
    ) {
117
        if ($filenameOrStream instanceof StreamInterface) {
118
            if ($filenameOrStream->isReadable() === false) {
119
                throw new InvalidArgumentException('Stream is not readable');
120
            }
121
            $this->stream = $filenameOrStream;
122
            $this->size = $size ? $size : $filenameOrStream->getSize();
123
        } else {
124
            $this->filename = $filenameOrStream;
125
            $this->size = $size;
126
        }
127
128
        $this->error = $this->filterError($error);
129
        $this->clientFilename = $clientFilename;
130
        $this->clientMediaType = $clientMediaType;
131
    }
132
133
    /**
134
    * Create uploaded file from global variable $_FILES
135
    * @return array<mixed>
136
    */
137
    public static function createFromGlobals(): array
138
    {
139
        return static::normalize($_FILES);
140
    }
141
142
    /**
143
     * {@inheritdoc}
144
     */
145
    public function getStream(): StreamInterface
146
    {
147
        if ($this->moved) {
148
            throw new RuntimeException('Stream is not avaliable! Uploaded file is moved');
149
        }
150
151
        if ($this->stream === null && $this->filename !== null) {
152
            $this->stream = new Stream($this->filename);
153
        }
154
155
        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 Platine\Http\StreamInterface. Consider adding an additional type-check to rule them out.
Loading history...
156
    }
157
158
    /**
159
     * {@inheritdoc}
160
     */
161
    public function moveTo(string $targetPath): void
162
    {
163
        if ($this->moved) {
164
            throw new RuntimeException('Uploaded file is already moved');
165
        }
166
167
        $cleanTargetPath = $this->filterTargetPath($targetPath);
168
        if ($this->filename !== null) {
169
            if (php_sapi_name() === 'cli') {
170
                if (rename($this->filename, $cleanTargetPath) === false) {
171
                    throw new RuntimeException('Unable to rename the uploaded file');
172
                }
173
            } else {
174
                if (
175
                    is_uploaded_file($this->filename) === false || move_uploaded_file(
176
                        $this->filename,
177
                        $cleanTargetPath
178
                    ) === false
179
                ) {
180
                    throw new RuntimeException('Unable to move the uploaded file');
181
                }
182
            }
183
        } else {
184
            $stream = $this->getStream();
185
            if ($stream->isSeekable()) {
186
                $stream->rewind();
187
            }
188
            $dest = new Stream($cleanTargetPath);
189
            $bufferSize = 8192;
190
            while (!$stream->eof()) {
191
                if (!$dest->write($stream->read($bufferSize))) {
192
                    break;
193
                }
194
            }
195
            $stream->close();
196
        }
197
        $this->moved = true;
198
    }
199
200
    /**
201
     * {@inheritdoc}
202
     */
203
    public function getSize(): ?int
204
    {
205
        return $this->size;
206
    }
207
208
    /**
209
     * {@inheritdoc}
210
     */
211
    public function getError(): int
212
    {
213
        return $this->error;
214
    }
215
216
    /**
217
     * {@inheritdoc}
218
     */
219
    public function getClientFilename(): ?string
220
    {
221
        return $this->clientFilename;
222
    }
223
224
    /**
225
     * {@inheritdoc}
226
     */
227
    public function getClientMediaType(): ?string
228
    {
229
        return $this->clientMediaType;
230
    }
231
232
    /**
233
     * Normalize files according to standard
234
     * @param  array<string, array<string, mixed|UploadedFileInterface>>  $files
235
     * @return array<string, UploadedFileInterface|UploadedFileInterface[]>
236
     */
237
    public static function normalize(array $files): array
238
    {
239
        $normalized = [];
240
        foreach ($files as $name => $info) {
241
            if ($info instanceof UploadedFileInterface) {
242
                $normalized[$name] = $info;
243
                continue;
244
            }
245
246
            if (!isset($info['error'])) {
247
                if (is_array($info)) {
248
                    $normalized[$name] = static::normalize($info);
249
                }
250
                continue;
251
            }
252
253
            $normalized[$name] = [];
254
            if (!is_array($info['error'])) {
255
                $normalized[$name] = new static(
256
                    isset($info['tmp_name']) ? $info['tmp_name'] : '',
0 ignored issues
show
It seems like IssetNode ? $info['tmp_name'] : '' can also be of type Platine\Http\UploadedFileInterface; however, parameter $filenameOrStream of Platine\Http\UploadedFile::__construct() does only seem to accept Platine\Http\StreamInterface|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

256
                    /** @scrutinizer ignore-type */ isset($info['tmp_name']) ? $info['tmp_name'] : '',
Loading history...
257
                    !empty($info['size']) ? $info['size'] : null,
0 ignored issues
show
It seems like ! empty($info['size']) ? $info['size'] : null can also be of type Platine\Http\UploadedFileInterface; however, parameter $size of Platine\Http\UploadedFile::__construct() does only seem to accept integer|null, 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

257
                    /** @scrutinizer ignore-type */ !empty($info['size']) ? $info['size'] : null,
Loading history...
258
                    $info['error'],
0 ignored issues
show
It seems like $info['error'] can also be of type Platine\Http\UploadedFileInterface; however, parameter $error of Platine\Http\UploadedFile::__construct() does only seem to accept integer, 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

258
                    /** @scrutinizer ignore-type */ $info['error'],
Loading history...
259
                    !empty($info['name']) ? $info['name'] : null,
0 ignored issues
show
It seems like ! empty($info['name']) ? $info['name'] : null can also be of type Platine\Http\UploadedFileInterface; however, parameter $clientFilename of Platine\Http\UploadedFile::__construct() does only seem to accept null|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

259
                    /** @scrutinizer ignore-type */ !empty($info['name']) ? $info['name'] : null,
Loading history...
260
                    !empty($info['type']) ? $info['type'] : null,
0 ignored issues
show
It seems like ! empty($info['type']) ? $info['type'] : null can also be of type Platine\Http\UploadedFileInterface; however, parameter $clientMediaType of Platine\Http\UploadedFile::__construct() does only seem to accept null|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

260
                    /** @scrutinizer ignore-type */ !empty($info['type']) ? $info['type'] : null,
Loading history...
261
                );
262
            } else {
263
                $nestedInfo = [];
264
                foreach (array_keys($info['error']) as $key) {
265
                    $nestedInfo[$key]['tmp_name'] = isset($info['tmp_name'][$key]) ? $info['tmp_name'][$key] : '';
266
                    $nestedInfo[$key]['name'] = isset($info['name'][$key]) ? $info['name'][$key] : '';
267
                    $nestedInfo[$key]['size'] = isset($info['size'][$key]) ? $info['size'][$key] : null;
268
                    $nestedInfo[$key]['error'] = isset($info['error'][$key]) ? $info['error'][$key] : 0;
269
                    $nestedInfo[$key]['type'] = isset($info['type'][$key]) ? $info['type'][$key] : '';
270
271
                    $normalized[$name] = static::normalize($nestedInfo);
272
                }
273
            }
274
        }
275
276
        return $normalized;
277
    }
278
279
    /**
280
     * Filter the uploaded file error
281
     * @param  int  $error
282
     * @return int
283
     */
284
    protected function filterError(int $error): int
285
    {
286
        $validErrors = [
287
            UPLOAD_ERR_OK,
288
            UPLOAD_ERR_INI_SIZE,
289
            UPLOAD_ERR_FORM_SIZE,
290
            UPLOAD_ERR_PARTIAL,
291
            UPLOAD_ERR_NO_FILE,
292
            UPLOAD_ERR_NO_TMP_DIR,
293
            UPLOAD_ERR_CANT_WRITE,
294
            UPLOAD_ERR_EXTENSION
295
        ];
296
297
        if (!in_array($error, $validErrors)) {
298
            throw new InvalidArgumentException('Upload error code must be a PHP file upload error.');
299
        }
300
301
        return $error;
302
    }
303
304
    /**
305
     * Filter the uploaded file target path
306
     * @param  string    $targetPath
307
     * @return string
308
     */
309
    protected function filterTargetPath(string $targetPath): string
310
    {
311
        if ($targetPath === '') {
312
            throw new InvalidArgumentException('Target path can not be empty.');
313
        }
314
        return $targetPath;
315
    }
316
}
317