-
-
Notifications
You must be signed in to change notification settings - Fork 89
/
FileUpload.php
277 lines (227 loc) 路 7.14 KB
/
FileUpload.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Http;
use Nette;
use Nette\Utils\Image;
/**
* Provides access to individual files that have been uploaded by a client.
*
* @property-read string $name
* @property-read string $sanitizedName
* @property-read string $untrustedFullPath
* @property-read string|null $contentType
* @property-read int $size
* @property-read string $temporaryFile
* @property-read int $error
* @property-read bool $ok
* @property-read string|null $contents
*/
final class FileUpload
{
use Nette\SmartObject;
/** @deprecated */
public const IMAGE_MIME_TYPES = ['image/gif', 'image/png', 'image/jpeg', 'image/webp'];
private readonly string $name;
private readonly string|null $fullPath;
private string|false|null $type = null;
private string|false|null $extension = null;
private readonly int $size;
private string $tmpName;
private readonly int $error;
public function __construct(?array $value)
{
foreach (['name', 'size', 'tmp_name', 'error'] as $key) {
if (!isset($value[$key]) || !is_scalar($value[$key])) {
$this->error = UPLOAD_ERR_NO_FILE;
return; // or throw exception?
}
}
$this->name = $value['name'];
$this->fullPath = $value['full_path'] ?? null;
$this->size = $value['size'];
$this->tmpName = $value['tmp_name'];
$this->error = $value['error'];
}
/**
* @deprecated use getUntrustedName()
*/
public function getName(): string
{
return $this->name;
}
/**
* Returns the original file name as submitted by the browser. Do not trust the value returned by this method.
* A client could send a malicious filename with the intention to corrupt or hack your application.
*/
public function getUntrustedName(): string
{
return $this->name;
}
/**
* Returns the sanitized file name. The resulting name contains only ASCII characters [a-zA-Z0-9.-].
* If the name does not contain such characters, it returns 'unknown'. If the file is JPEG, PNG, GIF, or WebP image,
* it returns the correct file extension. Do not blindly trust the value returned by this method.
*/
public function getSanitizedName(): string
{
$name = Nette\Utils\Strings::webalize($this->name, '.', lower: false);
$name = str_replace(['-.', '.-'], '.', $name);
$name = trim($name, '.-');
$name = $name === '' ? 'unknown' : $name;
if ($ext = $this->getSuggestedExtension()) {
$name = preg_replace('#\.[^.]+$#D', '', $name);
$name .= '.' . $ext;
}
return $name;
}
/**
* Returns the original full path as submitted by the browser during directory upload. Do not trust the value
* returned by this method. A client could send a malicious directory structure with the intention to corrupt
* or hack your application.
*
* The full path is only available in PHP 8.1 and above. In previous versions, this method returns the file name.
*/
public function getUntrustedFullPath(): string
{
return $this->fullPath ?? $this->name;
}
/**
* Detects the MIME content type of the uploaded file based on its signature. Requires PHP extension fileinfo.
* If the upload was not successful or the detection failed, it returns null.
*/
public function getContentType(): ?string
{
if ($this->isOk()) {
$this->type ??= finfo_file(finfo_open(FILEINFO_MIME_TYPE), $this->tmpName);
}
return $this->type ?: null;
}
/**
* Returns the appropriate file extension (without the period) corresponding to the detected MIME type. Requires the PHP extension fileinfo.
*/
public function getSuggestedExtension(): ?string
{
if ($this->isOk() && $this->extension === null) {
$exts = finfo_file(finfo_open(FILEINFO_EXTENSION), $this->tmpName);
if ($exts && $exts !== '???') {
return $this->extension = preg_replace('~[/,].*~', '', $exts);
}
[, , $type] = @getimagesize($this->tmpName); // @ - files smaller than 12 bytes causes read error
if ($type) {
return $this->extension = image_type_to_extension($type, false);
}
$this->extension = false;
}
return $this->extension ?: null;
}
/**
* Returns the size of the uploaded file in bytes.
*/
public function getSize(): int
{
return $this->size;
}
/**
* Returns the path of the temporary location of the uploaded file.
*/
public function getTemporaryFile(): string
{
return $this->tmpName;
}
/**
* Returns the path of the temporary location of the uploaded file.
*/
public function __toString(): string
{
return $this->tmpName;
}
/**
* Returns the error code. It is be one of UPLOAD_ERR_XXX constants.
* @see http://php.net/manual/en/features.file-upload.errors.php
*/
public function getError(): int
{
return $this->error;
}
/**
* Returns true if the file was uploaded successfully.
*/
public function isOk(): bool
{
return $this->error === UPLOAD_ERR_OK;
}
/**
* Returns true if the user has uploaded a file.
*/
public function hasFile(): bool
{
return $this->error !== UPLOAD_ERR_NO_FILE;
}
/**
* Moves an uploaded file to a new location. If the destination file already exists, it will be overwritten.
*/
public function move(string $dest): static
{
$dir = dirname($dest);
Nette\Utils\FileSystem::createDir($dir);
@unlink($dest); // @ - file may not exists
Nette\Utils\Callback::invokeSafe(
is_uploaded_file($this->tmpName) ? 'move_uploaded_file' : 'rename',
[$this->tmpName, $dest],
function (string $message) use ($dest): void {
throw new Nette\InvalidStateException("Unable to move uploaded file '$this->tmpName' to '$dest'. $message");
},
);
@chmod($dest, 0o666); // @ - possible low permission to chmod
$this->tmpName = $dest;
return $this;
}
/**
* Returns true if the uploaded file is an image and the format is supported by PHP, so it can be loaded using the toImage() method.
* Detection is based on its signature, the integrity of the file is not checked. Requires PHP extensions fileinfo & gd.
*/
public function isImage(): bool
{
$types = array_map(fn($type) => Image::typeToMimeType($type), Image::getSupportedTypes());
return in_array($this->getContentType(), $types, strict: true);
}
/**
* Converts uploaded image to Nette\Utils\Image object.
* @throws Nette\Utils\ImageException If the upload was not successful or is not a valid image
*/
public function toImage(): Image
{
return Image::fromFile($this->tmpName);
}
/**
* Returns a pair of [width, height] with dimensions of the uploaded image.
*/
public function getImageSize(): ?array
{
return $this->isImage()
? array_intersect_key(getimagesize($this->tmpName), [0, 1])
: null;
}
/**
* Returns image file extension based on detected content type (without dot).
* @deprecated use getSuggestedExtension()
*/
public function getImageFileExtension(): ?string
{
return $this->getSuggestedExtension();
}
/**
* Returns the contents of the uploaded file. If the upload was not successful, it returns null.
*/
public function getContents(): ?string
{
// future implementation can try to work around safe_mode and open_basedir limitations
return $this->isOk()
? file_get_contents($this->tmpName)
: null;
}
}