1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Charcoal\Property; |
4
|
|
|
|
5
|
|
|
use finfo; |
6
|
|
|
use PDO; |
7
|
|
|
use Exception; |
8
|
|
|
use InvalidArgumentException; |
9
|
|
|
use UnexpectedValueException; |
10
|
|
|
|
11
|
|
|
// From Pimple |
12
|
|
|
use Pimple\Container; |
13
|
|
|
|
14
|
|
|
// From 'charcoal-translator' |
15
|
|
|
use Charcoal\Translator\Translation; |
16
|
|
|
|
17
|
|
|
// From 'charcoal-property' |
18
|
|
|
use Charcoal\Property\AbstractProperty; |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* File Property |
22
|
|
|
*/ |
23
|
|
|
class FileProperty extends AbstractProperty |
24
|
|
|
{ |
25
|
|
|
const DEFAULT_PUBLIC_ACCESS = false; |
26
|
|
|
const DEFAULT_UPLOAD_PATH = 'uploads/'; |
27
|
|
|
const DEFAULT_FILESYSTEM = 'public'; |
28
|
|
|
const DEFAULT_OVERWRITE = false; |
29
|
|
|
const ERROR_MESSAGES = [ |
30
|
|
|
UPLOAD_ERR_OK => 'There is no error, the file uploaded with success', |
31
|
|
|
UPLOAD_ERR_INI_SIZE => 'The uploaded file exceeds the upload_max_filesize directive in php.ini', |
32
|
|
|
UPLOAD_ERR_FORM_SIZE => 'The uploaded file exceeds the MAX_FILE_SIZE directive'. |
33
|
|
|
'that was specified in the HTML form', |
34
|
|
|
UPLOAD_ERR_PARTIAL => 'The uploaded file was only partially uploaded', |
35
|
|
|
UPLOAD_ERR_NO_FILE => 'No file was uploaded', |
36
|
|
|
UPLOAD_ERR_NO_TMP_DIR => 'Missing a temporary folder', |
37
|
|
|
UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk', |
38
|
|
|
UPLOAD_ERR_EXTENSION => 'A PHP extension stopped the file upload.', |
39
|
|
|
]; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* Whether uploaded files should be accessible from the web root. |
43
|
|
|
* |
44
|
|
|
* @var boolean |
45
|
|
|
*/ |
46
|
|
|
private $publicAccess = self::DEFAULT_PUBLIC_ACCESS; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* The relative path to the storage directory. |
50
|
|
|
* |
51
|
|
|
* @var string |
52
|
|
|
*/ |
53
|
|
|
private $uploadPath = self::DEFAULT_UPLOAD_PATH; |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* The base path for the Charcoal installation. |
57
|
|
|
* |
58
|
|
|
* @var string |
59
|
|
|
*/ |
60
|
|
|
private $basePath; |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* The path to the public / web directory. |
64
|
|
|
* |
65
|
|
|
* @var string |
66
|
|
|
*/ |
67
|
|
|
private $publicPath; |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* Whether existing destinations should be overwritten. |
71
|
|
|
* |
72
|
|
|
* @var boolean |
73
|
|
|
*/ |
74
|
|
|
private $overwrite = self::DEFAULT_OVERWRITE; |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* Collection of accepted MIME types. |
78
|
|
|
* |
79
|
|
|
* @var string[] |
80
|
|
|
*/ |
81
|
|
|
private $acceptedMimetypes; |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* Current file mimetype |
85
|
|
|
* |
86
|
|
|
* @var string |
87
|
|
|
*/ |
88
|
|
|
private $mimetype; |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* Maximum allowed file size, in bytes. |
92
|
|
|
* |
93
|
|
|
* @var integer |
94
|
|
|
*/ |
95
|
|
|
private $maxFilesize; |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* Current file size, in bytes. |
99
|
|
|
* |
100
|
|
|
* @var integer |
101
|
|
|
*/ |
102
|
|
|
private $filesize; |
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* @var string |
106
|
|
|
*/ |
107
|
|
|
private $fallbackFilename; |
108
|
|
|
|
109
|
|
|
/** |
110
|
|
|
* The filesystem to use while uploading a file. |
111
|
|
|
* |
112
|
|
|
* @var string |
113
|
|
|
*/ |
114
|
|
|
private $filesystem = self::DEFAULT_FILESYSTEM; |
115
|
|
|
|
116
|
|
|
/** |
117
|
|
|
* Holds a list of all normalized paths. |
118
|
|
|
* |
119
|
|
|
* @var string[] |
120
|
|
|
*/ |
121
|
|
|
protected static $normalizePathCache = []; |
122
|
|
|
|
123
|
|
|
/** |
124
|
|
|
* @return string |
125
|
|
|
*/ |
126
|
|
|
public function type() |
127
|
|
|
{ |
128
|
|
|
return 'file'; |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
/** |
132
|
|
|
* Set whether uploaded files should be publicly available. |
133
|
|
|
* |
134
|
|
|
* @param boolean $public Whether uploaded files should be accessible (TRUE) or not (FALSE) from the web root. |
135
|
|
|
* @return self |
136
|
|
|
*/ |
137
|
|
|
public function setPublicAccess($public) |
138
|
|
|
{ |
139
|
|
|
$this->publicAccess = !!$public; |
140
|
|
|
|
141
|
|
|
return $this; |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* Determine if uploaded files should be publicly available. |
146
|
|
|
* |
147
|
|
|
* @return boolean |
148
|
|
|
*/ |
149
|
|
|
public function getPublicAccess() |
150
|
|
|
{ |
151
|
|
|
return $this->publicAccess; |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
/** |
155
|
|
|
* Set the destination (directory) where uploaded files are stored. |
156
|
|
|
* |
157
|
|
|
* The path must be relative to the {@see self::basePath()}, |
158
|
|
|
* |
159
|
|
|
* @param string $path The destination directory, relative to project's root. |
160
|
|
|
* @throws InvalidArgumentException If the path is not a string. |
161
|
|
|
* @return self |
162
|
|
|
*/ |
163
|
|
|
public function setUploadPath($path) |
164
|
|
|
{ |
165
|
|
|
if (!is_string($path)) { |
166
|
|
|
throw new InvalidArgumentException( |
167
|
|
|
'Upload path must be a string' |
168
|
|
|
); |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
// Sanitize upload path (force trailing slash) |
172
|
|
|
$this->uploadPath = rtrim($path, '/').'/'; |
173
|
|
|
|
174
|
|
|
return $this; |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
/** |
178
|
|
|
* Retrieve the destination for the uploaded file(s). |
179
|
|
|
* |
180
|
|
|
* @return string |
181
|
|
|
*/ |
182
|
|
|
public function getUploadPath() |
183
|
|
|
{ |
184
|
|
|
return $this->uploadPath; |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
/** |
188
|
|
|
* Set whether existing destinations should be overwritten. |
189
|
|
|
* |
190
|
|
|
* @param boolean $overwrite Whether existing destinations should be overwritten (TRUE) or not (FALSE). |
191
|
|
|
* @return self |
192
|
|
|
*/ |
193
|
|
|
public function setOverwrite($overwrite) |
194
|
|
|
{ |
195
|
|
|
$this->overwrite = !!$overwrite; |
196
|
|
|
|
197
|
|
|
return $this; |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
/** |
201
|
|
|
* Determine if existing destinations should be overwritten. |
202
|
|
|
* |
203
|
|
|
* @return boolean |
204
|
|
|
*/ |
205
|
|
|
public function getOverwrite() |
206
|
|
|
{ |
207
|
|
|
return $this->overwrite; |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* Sets the acceptable MIME types for uploaded files. |
212
|
|
|
* |
213
|
|
|
* @param mixed $types One or many MIME types. |
214
|
|
|
* @throws InvalidArgumentException If the $types argument is not NULL or a list. |
215
|
|
|
* @return self |
216
|
|
|
*/ |
217
|
|
|
public function setAcceptedMimetypes($types) |
218
|
|
|
{ |
219
|
|
|
if (is_array($types)) { |
220
|
|
|
$types = array_filter($types); |
221
|
|
|
|
222
|
|
|
if (empty($types)) { |
223
|
|
|
$types = null; |
224
|
|
|
} |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
if ($types !== null && !is_array($types)) { |
228
|
|
|
throw new InvalidArgumentException( |
229
|
|
|
'Must be an array of acceptable MIME types or NULL' |
230
|
|
|
); |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
$this->acceptedMimetypes = $types; |
|
|
|
|
234
|
|
|
return $this; |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
/** |
238
|
|
|
* Determines if any acceptable MIME types are defined. |
239
|
|
|
* |
240
|
|
|
* @return boolean |
241
|
|
|
*/ |
242
|
|
|
public function hasAcceptedMimetypes() |
243
|
|
|
{ |
244
|
|
|
if (!empty($this->acceptedMimetypes)) { |
245
|
|
|
return true; |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
return !empty($this->getDefaultAcceptedMimetypes()); |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
/** |
252
|
|
|
* Retrieves a list of acceptable MIME types for uploaded files. |
253
|
|
|
* |
254
|
|
|
* @return string[] |
255
|
|
|
*/ |
256
|
|
|
public function getAcceptedMimetypes() |
257
|
|
|
{ |
258
|
|
|
if ($this->acceptedMimetypes === null) { |
259
|
|
|
return $this->getDefaultAcceptedMimetypes(); |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
return $this->acceptedMimetypes; |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
/** |
266
|
|
|
* Retrieves the default list of acceptable MIME types for uploaded files. |
267
|
|
|
* |
268
|
|
|
* This method should be overriden. |
269
|
|
|
* |
270
|
|
|
* @return string[] |
271
|
|
|
*/ |
272
|
|
|
public function getDefaultAcceptedMimetypes() |
273
|
|
|
{ |
274
|
|
|
return []; |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
/** |
278
|
|
|
* Set the MIME type. |
279
|
|
|
* |
280
|
|
|
* @param mixed $type The file MIME type. |
281
|
|
|
* @throws InvalidArgumentException If the MIME type argument is not a string. |
282
|
|
|
* @return FileProperty Chainable |
283
|
|
|
*/ |
284
|
|
|
public function setMimetype($type) |
285
|
|
|
{ |
286
|
|
|
if ($type === null || $type === false) { |
287
|
|
|
$this->mimetype = null; |
288
|
|
|
return $this; |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
if (!is_string($type)) { |
292
|
|
|
throw new InvalidArgumentException( |
293
|
|
|
'MIME type must be a string' |
294
|
|
|
); |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
$this->mimetype = $type; |
298
|
|
|
return $this; |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
/** |
302
|
|
|
* Retrieve the MIME type of the property value. |
303
|
|
|
* |
304
|
|
|
* @todo Refactor to support multilingual/multiple files. |
305
|
|
|
* |
306
|
|
|
* @return integer Returns the MIME type for the first value. |
307
|
|
|
*/ |
308
|
|
|
public function getMimetype() |
309
|
|
|
{ |
310
|
|
|
if ($this->mimetype === null) { |
311
|
|
|
$files = $this->parseValAsFileList($this->val()); |
|
|
|
|
312
|
|
|
if (empty($files)) { |
313
|
|
|
return null; |
314
|
|
|
} |
315
|
|
|
|
316
|
|
|
$file = reset($files); |
317
|
|
|
$type = $this->getMimetypeFor($file); |
|
|
|
|
318
|
|
|
if ($type === null) { |
319
|
|
|
return null; |
320
|
|
|
} |
321
|
|
|
|
322
|
|
|
$this->setMimetype($type); |
323
|
|
|
} |
324
|
|
|
|
325
|
|
|
return $this->mimetype; |
326
|
|
|
} |
327
|
|
|
|
328
|
|
|
/** |
329
|
|
|
* Extract the MIME type from the given file. |
330
|
|
|
* |
331
|
|
|
* @param string $file The file to check. |
332
|
|
|
* @return integer|null Returns the file's MIME type, |
333
|
|
|
* or NULL in case of an error or the file is missing. |
334
|
|
|
*/ |
335
|
|
|
public function getMimetypeFor($file) |
336
|
|
|
{ |
337
|
|
|
if (!$this->isAbsolutePath($file)) { |
338
|
|
|
$file = $this->pathFor($file); |
339
|
|
|
} |
340
|
|
|
|
341
|
|
|
if (!$this->fileExists($file)) { |
342
|
|
|
return null; |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
$info = new finfo(FILEINFO_MIME_TYPE); |
346
|
|
|
$type = $info->file($file); |
347
|
|
|
if (empty($type) || $type === 'inode/x-empty') { |
348
|
|
|
return null; |
349
|
|
|
} |
350
|
|
|
|
351
|
|
|
return $type; |
352
|
|
|
} |
353
|
|
|
|
354
|
|
|
/** |
355
|
|
|
* Set the maximium size accepted for an uploaded files. |
356
|
|
|
* |
357
|
|
|
* @param string|integer $size The maximum file size allowed, in bytes. |
358
|
|
|
* @throws InvalidArgumentException If the size argument is not an integer. |
359
|
|
|
* @return FileProperty Chainable |
360
|
|
|
*/ |
361
|
|
|
public function setMaxFilesize($size) |
362
|
|
|
{ |
363
|
|
|
$this->maxFilesize = $this->parseIniSize($size); |
|
|
|
|
364
|
|
|
|
365
|
|
|
return $this; |
366
|
|
|
} |
367
|
|
|
|
368
|
|
|
/** |
369
|
|
|
* Retrieve the maximum size accepted for uploaded files. |
370
|
|
|
* |
371
|
|
|
* If null or 0, then no limit. Defaults to 128 MB. |
372
|
|
|
* |
373
|
|
|
* @return integer |
374
|
|
|
*/ |
375
|
|
|
public function getMaxFilesize() |
376
|
|
|
{ |
377
|
|
|
if (!isset($this->maxFilesize)) { |
378
|
|
|
return $this->maxFilesizeAllowedByPhp(); |
379
|
|
|
} |
380
|
|
|
|
381
|
|
|
return $this->maxFilesize; |
382
|
|
|
} |
383
|
|
|
|
384
|
|
|
/** |
385
|
|
|
* Retrieve the maximum size (in bytes) allowed for an uploaded file |
386
|
|
|
* as configured in {@link http://php.net/manual/en/ini.php `php.ini`}. |
387
|
|
|
* |
388
|
|
|
* @param string|null $iniDirective If $iniDirective is provided, then it is filled with |
389
|
|
|
* the name of the PHP INI directive corresponding to the maximum size allowed. |
390
|
|
|
* @return integer |
391
|
|
|
*/ |
392
|
|
|
public function maxFilesizeAllowedByPhp(&$iniDirective = null) |
393
|
|
|
{ |
394
|
|
|
$postMaxSize = $this->parseIniSize(ini_get('post_max_size')); |
395
|
|
|
$uploadMaxFilesize = $this->parseIniSize(ini_get('upload_max_filesize')); |
396
|
|
|
|
397
|
|
|
if ($postMaxSize < $uploadMaxFilesize) { |
398
|
|
|
$iniDirective = 'post_max_size'; |
399
|
|
|
|
400
|
|
|
return $postMaxSize; |
401
|
|
|
} else { |
402
|
|
|
$iniDirective = 'upload_max_filesize'; |
403
|
|
|
|
404
|
|
|
return $uploadMaxFilesize; |
405
|
|
|
} |
406
|
|
|
} |
407
|
|
|
|
408
|
|
|
/** |
409
|
|
|
* @param integer $size The file size, in bytes. |
410
|
|
|
* @throws InvalidArgumentException If the size argument is not an integer. |
411
|
|
|
* @return FileProperty Chainable |
412
|
|
|
*/ |
413
|
|
|
public function setFilesize($size) |
414
|
|
|
{ |
415
|
|
|
if (!is_int($size) && $size !== null) { |
416
|
|
|
throw new InvalidArgumentException( |
417
|
|
|
'File size must be an integer in bytes' |
418
|
|
|
); |
419
|
|
|
} |
420
|
|
|
|
421
|
|
|
$this->filesize = $size; |
422
|
|
|
return $this; |
423
|
|
|
} |
424
|
|
|
|
425
|
|
|
/** |
426
|
|
|
* Retrieve the size of the property value. |
427
|
|
|
* |
428
|
|
|
* @todo Refactor to support multilingual/multiple files. |
429
|
|
|
* |
430
|
|
|
* @return integer Returns the size in bytes for the first value. |
431
|
|
|
*/ |
432
|
|
|
public function getFilesize() |
433
|
|
|
{ |
434
|
|
|
if ($this->filesize === null) { |
435
|
|
|
$files = $this->parseValAsFileList($this->val()); |
|
|
|
|
436
|
|
|
if (empty($files)) { |
437
|
|
|
return 0; |
438
|
|
|
} |
439
|
|
|
|
440
|
|
|
$file = reset($files); |
441
|
|
|
$size = $this->getFilesizeFor($file); |
|
|
|
|
442
|
|
|
if ($size === null) { |
443
|
|
|
return 0; |
444
|
|
|
} |
445
|
|
|
|
446
|
|
|
$this->setFilesize($size); |
447
|
|
|
} |
448
|
|
|
|
449
|
|
|
return $this->filesize; |
450
|
|
|
} |
451
|
|
|
|
452
|
|
|
/** |
453
|
|
|
* Extract the size of the given file. |
454
|
|
|
* |
455
|
|
|
* @param string $file The file to check. |
456
|
|
|
* @return integer|null Returns the file size in bytes, |
457
|
|
|
* or NULL in case of an error or the file is missing. |
458
|
|
|
*/ |
459
|
|
|
public function getFilesizeFor($file) |
460
|
|
|
{ |
461
|
|
|
if (!$this->isAbsolutePath($file)) { |
462
|
|
|
$file = $this->pathFor($file); |
463
|
|
|
} |
464
|
|
|
|
465
|
|
|
if (!$this->fileExists($file)) { |
466
|
|
|
return null; |
467
|
|
|
} |
468
|
|
|
|
469
|
|
|
$size = filesize($file); |
470
|
|
|
if ($size === false) { |
471
|
|
|
return null; |
472
|
|
|
} |
473
|
|
|
|
474
|
|
|
return $size; |
475
|
|
|
} |
476
|
|
|
|
477
|
|
|
/** |
478
|
|
|
* Convert number of bytes to largest human-readable unit. |
479
|
|
|
* |
480
|
|
|
* @param integer $bytes Number of bytes. |
481
|
|
|
* @param integer $decimals Precision of number of decimal places. Default 0. |
482
|
|
|
* @return string|null Returns the formatted number or NULL. |
483
|
|
|
*/ |
484
|
|
|
public function formatFilesize($bytes, $decimals = 2) |
485
|
|
|
{ |
486
|
|
|
if ($bytes === 0) { |
487
|
|
|
$factor = 0; |
|
|
|
|
488
|
|
|
} else { |
489
|
|
|
$factor = floor((strlen($bytes) - 1) / 3); |
|
|
|
|
490
|
|
|
} |
491
|
|
|
|
492
|
|
|
$unit = [ 'B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB' ]; |
493
|
|
|
|
494
|
|
|
$factor = floor((strlen($bytes) - 1) / 3); |
495
|
|
|
|
496
|
|
|
if (!isset($unit[$factor])) { |
497
|
|
|
$factor = 0; |
498
|
|
|
} |
499
|
|
|
|
500
|
|
|
return sprintf('%.'.$decimals.'f', ($bytes / pow(1024, $factor))).' '.$unit[$factor]; |
501
|
|
|
} |
502
|
|
|
|
503
|
|
|
/** |
504
|
|
|
* @return array |
505
|
|
|
*/ |
506
|
|
|
public function validationMethods() |
507
|
|
|
{ |
508
|
|
|
$parentMethods = parent::validationMethods(); |
509
|
|
|
|
510
|
|
|
return array_merge($parentMethods, [ |
511
|
|
|
'mimetypes', |
512
|
|
|
'filesizes', |
513
|
|
|
]); |
514
|
|
|
} |
515
|
|
|
|
516
|
|
|
/** |
517
|
|
|
* Validates the MIME types for the property's value(s). |
518
|
|
|
* |
519
|
|
|
* @return boolean Returns TRUE if all values are valid. |
520
|
|
|
* Otherwise, returns FALSE and reports issues. |
521
|
|
|
*/ |
522
|
|
View Code Duplication |
public function validateMimetypes() |
|
|
|
|
523
|
|
|
{ |
524
|
|
|
$acceptedMimetypes = $this['acceptedMimetypes']; |
525
|
|
|
if (empty($acceptedMimetypes)) { |
526
|
|
|
// No validation rules = always true |
527
|
|
|
return true; |
528
|
|
|
} |
529
|
|
|
|
530
|
|
|
$files = $this->parseValAsFileList($this->val()); |
|
|
|
|
531
|
|
|
|
532
|
|
|
if (empty($files)) { |
533
|
|
|
return true; |
534
|
|
|
} |
535
|
|
|
|
536
|
|
|
$valid = true; |
537
|
|
|
|
538
|
|
|
foreach ($files as $file) { |
539
|
|
|
$mime = $this->getMimetypeFor($file); |
540
|
|
|
|
541
|
|
|
if ($mime === null) { |
542
|
|
|
$valid = false; |
543
|
|
|
|
544
|
|
|
$this->validator()->error(sprintf( |
545
|
|
|
'File [%s] not found or MIME type unrecognizable', |
546
|
|
|
$file |
547
|
|
|
), 'acceptedMimetypes'); |
|
|
|
|
548
|
|
|
} elseif (!in_array($mime, $acceptedMimetypes)) { |
549
|
|
|
$valid = false; |
550
|
|
|
|
551
|
|
|
$this->validator()->error(sprintf( |
552
|
|
|
'File [%s] has unacceptable MIME type [%s]', |
553
|
|
|
$file, |
554
|
|
|
$mime |
555
|
|
|
), 'acceptedMimetypes'); |
|
|
|
|
556
|
|
|
} |
557
|
|
|
} |
558
|
|
|
|
559
|
|
|
return $valid; |
560
|
|
|
} |
561
|
|
|
|
562
|
|
|
/** |
563
|
|
|
* Validates the file sizes for the property's value(s). |
564
|
|
|
* |
565
|
|
|
* @return boolean Returns TRUE if all values are valid. |
566
|
|
|
* Otherwise, returns FALSE and reports issues. |
567
|
|
|
*/ |
568
|
|
View Code Duplication |
public function validateFilesizes() |
|
|
|
|
569
|
|
|
{ |
570
|
|
|
$maxFilesize = $this['maxFilesize']; |
571
|
|
|
if (empty($maxFilesize)) { |
572
|
|
|
// No max size rule = always true |
573
|
|
|
return true; |
574
|
|
|
} |
575
|
|
|
|
576
|
|
|
$files = $this->parseValAsFileList($this->val()); |
|
|
|
|
577
|
|
|
|
578
|
|
|
if (empty($files)) { |
579
|
|
|
return true; |
580
|
|
|
} |
581
|
|
|
|
582
|
|
|
$valid = true; |
583
|
|
|
|
584
|
|
|
foreach ($files as $file) { |
585
|
|
|
$filesize = $this->getFilesizeFor($file); |
586
|
|
|
|
587
|
|
|
if ($filesize === null) { |
588
|
|
|
$valid = false; |
589
|
|
|
|
590
|
|
|
$this->validator()->error(sprintf( |
591
|
|
|
'File [%s] not found or size unknown', |
592
|
|
|
$file |
593
|
|
|
), 'maxFilesize'); |
|
|
|
|
594
|
|
|
} elseif (($filesize > $maxFilesize)) { |
595
|
|
|
$valid = false; |
596
|
|
|
|
597
|
|
|
$this->validator()->error(sprintf( |
598
|
|
|
'File [%s] exceeds maximum file size [%s]', |
599
|
|
|
$file, |
600
|
|
|
$this->formatFilesize($maxFilesize) |
601
|
|
|
), 'maxFilesize'); |
|
|
|
|
602
|
|
|
} |
603
|
|
|
} |
604
|
|
|
|
605
|
|
|
return $valid; |
606
|
|
|
} |
607
|
|
|
|
608
|
|
|
/** |
609
|
|
|
* Parse a multi-dimensional array of value(s) into a single level. |
610
|
|
|
* |
611
|
|
|
* This method flattens a value object that is "l10n" or "multiple". |
612
|
|
|
* Empty or duplicate values are removed. |
613
|
|
|
* |
614
|
|
|
* @param mixed $value A multi-dimensional variable. |
615
|
|
|
* @return string[] The array of values. |
616
|
|
|
*/ |
617
|
|
|
public function parseValAsFileList($value) |
618
|
|
|
{ |
619
|
|
|
$files = []; |
620
|
|
|
|
621
|
|
|
if ($value instanceof Translation) { |
622
|
|
|
$value = $value->data(); |
623
|
|
|
} |
624
|
|
|
|
625
|
|
|
$array = $this->parseValAsMultiple($value); |
626
|
|
|
array_walk_recursive($array, function ($item) use (&$files) { |
627
|
|
|
$array = $this->parseValAsMultiple($item); |
628
|
|
|
$files = array_merge($files, $array); |
629
|
|
|
}); |
630
|
|
|
|
631
|
|
|
$files = array_filter($files, function ($file) { |
632
|
|
|
return is_string($file) && isset($file[0]); |
633
|
|
|
}); |
634
|
|
|
$files = array_unique($files); |
635
|
|
|
$files = array_values($files); |
636
|
|
|
|
637
|
|
|
return $files; |
638
|
|
|
} |
639
|
|
|
|
640
|
|
|
/** |
641
|
|
|
* Get the SQL type (Storage format) |
642
|
|
|
* |
643
|
|
|
* Stored as `VARCHAR` for max_length under 255 and `TEXT` for other, longer strings |
644
|
|
|
* |
645
|
|
|
* @see StorablePropertyTrait::sqlType() |
646
|
|
|
* @return string The SQL type |
647
|
|
|
*/ |
648
|
|
|
public function sqlType() |
649
|
|
|
{ |
650
|
|
|
// Multiple strings are always stored as TEXT because they can hold multiple values |
651
|
|
|
if ($this['multiple']) { |
652
|
|
|
return 'TEXT'; |
653
|
|
|
} else { |
654
|
|
|
return 'VARCHAR(255)'; |
655
|
|
|
} |
656
|
|
|
} |
657
|
|
|
|
658
|
|
|
/** |
659
|
|
|
* @see StorablePropertyTrait::sqlPdoType() |
660
|
|
|
* @return integer |
661
|
|
|
*/ |
662
|
|
|
public function sqlPdoType() |
663
|
|
|
{ |
664
|
|
|
return PDO::PARAM_STR; |
665
|
|
|
} |
666
|
|
|
|
667
|
|
|
/** |
668
|
|
|
* Process file uploads {@see AbstractProperty::save() parsing values}. |
669
|
|
|
* |
670
|
|
|
* @param mixed $val The value, at time of saving. |
671
|
|
|
* @return mixed |
672
|
|
|
*/ |
673
|
|
|
public function save($val) |
674
|
|
|
{ |
675
|
|
|
if ($val instanceof Translation) { |
676
|
|
|
$values = $val->data(); |
677
|
|
|
} else { |
678
|
|
|
$values = $val; |
679
|
|
|
} |
680
|
|
|
|
681
|
|
|
$uploadedFiles = $this->getUploadedFiles(); |
682
|
|
|
|
683
|
|
|
if ($this['l10n']) { |
684
|
|
|
foreach ($this->translator()->availableLocales() as $lang) { |
685
|
|
|
if (!isset($values[$lang])) { |
686
|
|
|
$values[$lang] = $this['multiple'] ? [] : ''; |
687
|
|
|
} |
688
|
|
|
|
689
|
|
|
$parsedFiles = []; |
690
|
|
|
|
691
|
|
|
if (isset($uploadedFiles[$lang])) { |
692
|
|
|
$parsedFiles = $this->saveFileUploads($uploadedFiles[$lang]); |
693
|
|
|
} |
694
|
|
|
|
695
|
|
|
if (empty($parsedFiles)) { |
696
|
|
|
$parsedFiles = $this->saveDataUploads($values[$lang]); |
697
|
|
|
} |
698
|
|
|
|
699
|
|
|
$values[$lang] = $this->parseSavedValues($parsedFiles, $values[$lang]); |
700
|
|
|
} |
701
|
|
|
} else { |
702
|
|
|
$parsedFiles = []; |
703
|
|
|
|
704
|
|
|
if (!empty($uploadedFiles)) { |
705
|
|
|
$parsedFiles = $this->saveFileUploads($uploadedFiles); |
706
|
|
|
} |
707
|
|
|
|
708
|
|
|
if (empty($parsedFiles)) { |
709
|
|
|
$parsedFiles = $this->saveDataUploads($values); |
710
|
|
|
} |
711
|
|
|
|
712
|
|
|
$values = $this->parseSavedValues($parsedFiles, $values); |
713
|
|
|
} |
714
|
|
|
|
715
|
|
|
return $values; |
716
|
|
|
} |
717
|
|
|
|
718
|
|
|
/** |
719
|
|
|
* Process and transfer any data URIs to the filesystem, |
720
|
|
|
* and carry over any pre-processed file paths. |
721
|
|
|
* |
722
|
|
|
* @param mixed $values One or more data URIs, data entries, or processed file paths. |
723
|
|
|
* @return string|string[] One or more paths to the processed uploaded files. |
724
|
|
|
*/ |
725
|
|
|
protected function saveDataUploads($values) |
726
|
|
|
{ |
727
|
|
|
// Bag value if singular |
728
|
|
|
if (!is_array($values) || isset($values['id'])) { |
729
|
|
|
$values = [ $values ]; |
730
|
|
|
} |
731
|
|
|
|
732
|
|
|
$parsed = []; |
733
|
|
|
foreach ($values as $value) { |
734
|
|
|
if ($this->isDataArr($value) || $this->isDataUri($value)) { |
735
|
|
|
try { |
736
|
|
|
$path = $this->dataUpload($value); |
737
|
|
View Code Duplication |
if ($path !== null) { |
|
|
|
|
738
|
|
|
$parsed[] = $path; |
739
|
|
|
|
740
|
|
|
$this->logger->notice(sprintf( |
741
|
|
|
'File [%s] uploaded succesfully', |
742
|
|
|
$path |
743
|
|
|
)); |
744
|
|
|
} |
745
|
|
|
} catch (Exception $e) { |
746
|
|
|
$this->logger->warning(sprintf( |
747
|
|
|
'Upload error on data URI: %s', |
748
|
|
|
$e->getMessage() |
749
|
|
|
)); |
750
|
|
|
} |
751
|
|
|
} elseif (is_string($value) && !empty($value)) { |
752
|
|
|
$parsed[] = $value; |
753
|
|
|
} |
754
|
|
|
} |
755
|
|
|
|
756
|
|
|
return $parsed; |
757
|
|
|
} |
758
|
|
|
|
759
|
|
|
/** |
760
|
|
|
* Process and transfer any uploaded files to the filesystem. |
761
|
|
|
* |
762
|
|
|
* @param mixed $files One or more normalized $_FILE entries. |
763
|
|
|
* @return string[] One or more paths to the processed uploaded files. |
764
|
|
|
*/ |
765
|
|
|
protected function saveFileUploads($files) |
766
|
|
|
{ |
767
|
|
|
// Bag value if singular |
768
|
|
|
if (isset($files['error'])) { |
769
|
|
|
$files = [ $files ]; |
770
|
|
|
} |
771
|
|
|
|
772
|
|
|
$parsed = []; |
773
|
|
|
foreach ($files as $file) { |
774
|
|
|
if (isset($file['error'])) { |
775
|
|
|
try { |
776
|
|
|
$path = $this->fileUpload($file); |
777
|
|
View Code Duplication |
if ($path !== null) { |
|
|
|
|
778
|
|
|
$parsed[] = $path; |
779
|
|
|
|
780
|
|
|
$this->logger->notice(sprintf( |
781
|
|
|
'File [%s] uploaded succesfully', |
782
|
|
|
$path |
783
|
|
|
)); |
784
|
|
|
} |
785
|
|
|
} catch (Exception $e) { |
786
|
|
|
$this->logger->warning(sprintf( |
787
|
|
|
'Upload error on file [%s]: %s', |
788
|
|
|
$file['name'], |
789
|
|
|
$e->getMessage() |
790
|
|
|
)); |
791
|
|
|
} |
792
|
|
|
} |
793
|
|
|
} |
794
|
|
|
|
795
|
|
|
return $parsed; |
796
|
|
|
} |
797
|
|
|
|
798
|
|
|
/** |
799
|
|
|
* Finalize any processed files. |
800
|
|
|
* |
801
|
|
|
* @param mixed $saved One or more values, at time of saving. |
802
|
|
|
* @param mixed $default The default value to return. |
803
|
|
|
* @return string|string[] One or more paths to the processed uploaded files. |
804
|
|
|
*/ |
805
|
|
|
protected function parseSavedValues($saved, $default = null) |
806
|
|
|
{ |
807
|
|
|
$values = empty($saved) ? $default : $saved; |
808
|
|
|
|
809
|
|
|
if ($this['multiple']) { |
810
|
|
|
if (!is_array($values)) { |
811
|
|
|
$values = empty($values) && !is_numeric($values) ? [] : [ $values ]; |
812
|
|
|
} |
813
|
|
|
} else { |
814
|
|
|
if (is_array($values)) { |
815
|
|
|
$values = reset($values); |
816
|
|
|
} |
817
|
|
|
} |
818
|
|
|
|
819
|
|
|
return $values; |
820
|
|
|
} |
821
|
|
|
|
822
|
|
|
/** |
823
|
|
|
* Upload to filesystem, from data URI. |
824
|
|
|
* |
825
|
|
|
* @param mixed $data A data URI. |
826
|
|
|
* @throws Exception If data content decoding fails. |
827
|
|
|
* @throws InvalidArgumentException If the input $data is invalid. |
828
|
|
|
* @throws Exception If the upload fails or the $data is bad. |
829
|
|
|
* @return string|null The file path to the uploaded data. |
830
|
|
|
*/ |
831
|
|
|
public function dataUpload($data) |
832
|
|
|
{ |
833
|
|
|
$filename = null; |
834
|
|
|
$contents = false; |
835
|
|
|
|
836
|
|
|
if (is_array($data)) { |
837
|
|
|
if (!isset($data['id'], $data['name'])) { |
838
|
|
|
throw new InvalidArgumentException( |
839
|
|
|
'$data as an array MUST contain each of the keys "id" and "name", '. |
840
|
|
|
'with each represented as a scalar value; one or more were missing or non-array values' |
841
|
|
|
); |
842
|
|
|
} |
843
|
|
|
// retrieve tmp file from temp dir |
844
|
|
|
$tmpDir = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR; |
845
|
|
|
$tmpFile = $tmpDir.$data['id']; |
846
|
|
|
if (!file_exists($tmpFile)) { |
847
|
|
|
throw new Exception(sprintf( |
848
|
|
|
'File %s does not exists', |
849
|
|
|
$data['id'] |
850
|
|
|
)); |
851
|
|
|
} |
852
|
|
|
|
853
|
|
|
$contents = file_get_contents($tmpFile); |
854
|
|
|
|
855
|
|
|
if (strlen($data['name']) > 0) { |
856
|
|
|
$filename = $data['name']; |
857
|
|
|
} |
858
|
|
|
|
859
|
|
|
// delete tmp file |
860
|
|
|
unlink($tmpFile); |
861
|
|
|
} elseif (is_string($data)) { |
862
|
|
|
$contents = file_get_contents($data); |
863
|
|
|
} |
864
|
|
|
|
865
|
|
|
if ($contents === false) { |
866
|
|
|
throw new Exception( |
867
|
|
|
'File content could not be decoded for data URI' |
868
|
|
|
); |
869
|
|
|
} |
870
|
|
|
|
871
|
|
|
$info = new finfo(FILEINFO_MIME_TYPE); |
872
|
|
|
$mime = $info->buffer($contents); |
873
|
|
|
if (!$this->isAcceptedMimeType($mime)) { |
874
|
|
|
throw new Exception(sprintf( |
875
|
|
|
'Unacceptable MIME type [%s]', |
876
|
|
|
$mime |
877
|
|
|
)); |
878
|
|
|
} |
879
|
|
|
|
880
|
|
|
$size = strlen($contents); |
881
|
|
View Code Duplication |
if (!$this->isAcceptedFilesize($size)) { |
|
|
|
|
882
|
|
|
throw new Exception(sprintf( |
883
|
|
|
'Maximum file size exceeded [%s]', |
884
|
|
|
$this->formatFilesize($this['maxFilesize']) |
885
|
|
|
)); |
886
|
|
|
} |
887
|
|
|
|
888
|
|
|
if ($filename === null) { |
889
|
|
|
$extension = $this->generateExtensionFromMimeType($mime); |
890
|
|
|
$filename = $this->generateFilename($extension); |
891
|
|
|
} |
892
|
|
|
|
893
|
|
|
$targetPath = $this->uploadTarget($filename); |
894
|
|
|
|
895
|
|
|
$result = file_put_contents($targetPath, $contents); |
896
|
|
|
if ($result === false) { |
897
|
|
|
throw new Exception(sprintf( |
898
|
|
|
'Failed to write file to %s', |
899
|
|
|
$targetPath |
900
|
|
|
)); |
901
|
|
|
} |
902
|
|
|
|
903
|
|
|
$basePath = $this->basePath(); |
904
|
|
|
$targetPath = str_replace($basePath, '', $targetPath); |
905
|
|
|
|
906
|
|
|
return $targetPath; |
907
|
|
|
} |
908
|
|
|
|
909
|
|
|
/** |
910
|
|
|
* Upload to filesystem. |
911
|
|
|
* |
912
|
|
|
* @link https://github.com/slimphp/Slim/blob/3.12.1/Slim/Http/UploadedFile.php |
913
|
|
|
* Adapted from slim/slim. |
914
|
|
|
* |
915
|
|
|
* @param array $file A single $_FILES entry. |
916
|
|
|
* @throws InvalidArgumentException If the input $file is invalid. |
917
|
|
|
* @throws Exception If the upload fails or the $file is bad. |
918
|
|
|
* @return string|null The file path to the uploaded file. |
919
|
|
|
*/ |
920
|
|
|
public function fileUpload(array $file) |
921
|
|
|
{ |
922
|
|
|
if (!isset($file['tmp_name'], $file['name'], $file['size'], $file['error'])) { |
923
|
|
|
throw new InvalidArgumentException( |
924
|
|
|
'$file MUST contain each of the keys "tmp_name", "name", "size", and "error", '. |
925
|
|
|
'with each represented as a scalar value; one or more were missing or non-array values' |
926
|
|
|
); |
927
|
|
|
} |
928
|
|
|
|
929
|
|
|
if ($file['error'] !== UPLOAD_ERR_OK) { |
930
|
|
|
$errorCode = $file['error']; |
931
|
|
|
throw new Exception( |
932
|
|
|
self::ERROR_MESSAGES[$errorCode] |
933
|
|
|
); |
934
|
|
|
} |
935
|
|
|
|
936
|
|
|
if (!file_exists($file['tmp_name'])) { |
937
|
|
|
throw new Exception( |
938
|
|
|
'File does not exist' |
939
|
|
|
); |
940
|
|
|
} |
941
|
|
|
|
942
|
|
|
if (!is_uploaded_file($file['tmp_name'])) { |
943
|
|
|
throw new Exception( |
944
|
|
|
'File was not uploaded' |
945
|
|
|
); |
946
|
|
|
} |
947
|
|
|
|
948
|
|
|
$info = new finfo(FILEINFO_MIME_TYPE); |
949
|
|
|
$mime = $info->file($file['tmp_name']); |
950
|
|
|
if (!$this->isAcceptedMimeType($mime)) { |
951
|
|
|
throw new Exception(sprintf( |
952
|
|
|
'Unacceptable MIME type [%s]', |
953
|
|
|
$mime |
954
|
|
|
)); |
955
|
|
|
} |
956
|
|
|
|
957
|
|
|
$size = filesize($file['tmp_name']); |
958
|
|
View Code Duplication |
if (!$this->isAcceptedFilesize($size)) { |
|
|
|
|
959
|
|
|
throw new Exception(sprintf( |
960
|
|
|
'Maximum file size exceeded [%s]', |
961
|
|
|
$this->formatFilesize($this['maxFilesize']) |
962
|
|
|
)); |
963
|
|
|
} |
964
|
|
|
|
965
|
|
|
$targetPath = $this->uploadTarget($file['name']); |
966
|
|
|
|
967
|
|
|
$result = move_uploaded_file($file['tmp_name'], $targetPath); |
968
|
|
|
if ($result === false) { |
969
|
|
|
throw new Exception(sprintf( |
970
|
|
|
'Failed to move uploaded file to %s', |
971
|
|
|
$targetPath |
972
|
|
|
)); |
973
|
|
|
} |
974
|
|
|
|
975
|
|
|
$basePath = $this->basePath(); |
976
|
|
|
$targetPath = str_replace($basePath, '', $targetPath); |
977
|
|
|
|
978
|
|
|
return $targetPath; |
979
|
|
|
} |
980
|
|
|
|
981
|
|
|
/** |
982
|
|
|
* Parse the uploaded file path. |
983
|
|
|
* |
984
|
|
|
* This method will create the file's directory path and will sanitize the file's name |
985
|
|
|
* or generate a unique name if none provided (such as data URIs). |
986
|
|
|
* |
987
|
|
|
* @param string|null $filename Optional. The filename to save as. |
988
|
|
|
* If NULL, a default filename will be generated. |
989
|
|
|
* @return string |
990
|
|
|
*/ |
991
|
|
|
public function uploadTarget($filename = null) |
992
|
|
|
{ |
993
|
|
|
$this->assertValidUploadPath(); |
994
|
|
|
|
995
|
|
|
$uploadPath = $this->pathFor($this['uploadPath']); |
996
|
|
|
|
997
|
|
|
if ($filename === null) { |
998
|
|
|
$filename = $this->generateFilename(); |
999
|
|
|
} else { |
1000
|
|
|
$filename = $this->sanitizeFilename($filename); |
1001
|
|
|
} |
1002
|
|
|
|
1003
|
|
|
$targetPath = $uploadPath.'/'.$filename; |
1004
|
|
|
|
1005
|
|
|
if ($this->fileExists($targetPath)) { |
1006
|
|
|
if ($this['overwrite'] === true) { |
1007
|
|
|
return $targetPath; |
1008
|
|
|
} |
1009
|
|
|
|
1010
|
|
|
do { |
1011
|
|
|
$targetPath = $uploadPath.'/'.$this->generateUniqueFilename($filename); |
1012
|
|
|
} while ($this->fileExists($targetPath)); |
1013
|
|
|
} |
1014
|
|
|
|
1015
|
|
|
return $targetPath; |
1016
|
|
|
} |
1017
|
|
|
|
1018
|
|
|
/** |
1019
|
|
|
* Checks whether a file or directory exists. |
1020
|
|
|
* |
1021
|
|
|
* PHP built-in's `file_exists` is only case-insensitive on |
1022
|
|
|
* a case-insensitive filesystem (such as Windows). This method allows |
1023
|
|
|
* to have the same validation across different platforms / filesystems. |
1024
|
|
|
* |
1025
|
|
|
* @param string $file The full file to check. |
1026
|
|
|
* @param boolean $caseInsensitive Case-insensitive by default. |
1027
|
|
|
* @return boolean |
1028
|
|
|
*/ |
1029
|
|
|
public function fileExists($file, $caseInsensitive = true) |
1030
|
|
|
{ |
1031
|
|
|
$file = (string)$file; |
1032
|
|
|
|
1033
|
|
|
if (!$this->isAbsolutePath($file)) { |
1034
|
|
|
$file = $this->pathFor($file); |
1035
|
|
|
} |
1036
|
|
|
|
1037
|
|
|
if (file_exists($file)) { |
1038
|
|
|
return true; |
1039
|
|
|
} |
1040
|
|
|
|
1041
|
|
|
if ($caseInsensitive === false) { |
1042
|
|
|
return false; |
1043
|
|
|
} |
1044
|
|
|
|
1045
|
|
|
$files = glob(dirname($file).DIRECTORY_SEPARATOR.'*', GLOB_NOSORT); |
1046
|
|
|
if ($files) { |
|
|
|
|
1047
|
|
|
$pattern = preg_quote($file, '#'); |
1048
|
|
|
foreach ($files as $f) { |
1049
|
|
|
if (preg_match("#{$pattern}#i", $f)) { |
1050
|
|
|
return true; |
1051
|
|
|
} |
1052
|
|
|
} |
1053
|
|
|
} |
1054
|
|
|
|
1055
|
|
|
return false; |
1056
|
|
|
} |
1057
|
|
|
|
1058
|
|
|
/** |
1059
|
|
|
* Sanitize a filename by removing characters from a blacklist and escaping dot. |
1060
|
|
|
* |
1061
|
|
|
* @param string $filename The filename to sanitize. |
1062
|
|
|
* @throws Exception If the filename is invalid. |
1063
|
|
|
* @return string The sanitized filename. |
1064
|
|
|
*/ |
1065
|
|
|
public function sanitizeFilename($filename) |
1066
|
|
|
{ |
1067
|
|
|
// Remove blacklisted caharacters |
1068
|
|
|
$blacklist = [ '/', '\\', '\0', '*', ':', '?', '"', '<', '>', '|', '#', '&', '!', '`', ' ' ]; |
1069
|
|
|
$filename = str_replace($blacklist, '_', (string)$filename); |
1070
|
|
|
|
1071
|
|
|
// Avoid hidden file or trailing dot |
1072
|
|
|
$filename = trim($filename, '.'); |
1073
|
|
|
|
1074
|
|
|
if (strlen($filename) === 0) { |
1075
|
|
|
throw new Exception( |
1076
|
|
|
'Bad file name after sanitization' |
1077
|
|
|
); |
1078
|
|
|
} |
1079
|
|
|
|
1080
|
|
|
return $filename; |
1081
|
|
|
} |
1082
|
|
|
|
1083
|
|
|
/** |
1084
|
|
|
* Render the given file to the given pattern. |
1085
|
|
|
* |
1086
|
|
|
* This method does not rename the given path. |
1087
|
|
|
* |
1088
|
|
|
* @uses strtr() To replace tokens in the form `{{foobar}}`. |
1089
|
|
|
* @param string $from The string being rendered. |
1090
|
|
|
* @param string $to The pattern replacing $from. |
1091
|
|
|
* @param array|callable $args Extra rename tokens. |
1092
|
|
|
* @throws InvalidArgumentException If the given arguments are invalid. |
1093
|
|
|
* @throws UnexpectedValueException If the renaming failed. |
1094
|
|
|
* @return string Returns the rendered target. |
1095
|
|
|
*/ |
1096
|
|
|
public function renderFileRenamePattern($from, $to, $args = null) |
1097
|
|
|
{ |
1098
|
|
|
if (!is_string($from)) { |
1099
|
|
|
throw new InvalidArgumentException(sprintf( |
1100
|
|
|
'The target to rename must be a string, received %s', |
1101
|
|
|
(is_object($from) ? get_class($from) : gettype($from)) |
1102
|
|
|
)); |
1103
|
|
|
} |
1104
|
|
|
|
1105
|
|
|
if (!is_string($to)) { |
1106
|
|
|
throw new InvalidArgumentException(sprintf( |
1107
|
|
|
'The rename pattern must be a string, received %s', |
1108
|
|
|
(is_object($to) ? get_class($to) : gettype($to)) |
1109
|
|
|
)); |
1110
|
|
|
} |
1111
|
|
|
|
1112
|
|
|
$info = pathinfo($from); |
1113
|
|
|
$args = $this->renamePatternArgs($info, $args); |
1114
|
|
|
|
1115
|
|
|
$to = strtr($to, $args); |
1116
|
|
|
if (strpos($to, '{{') !== false) { |
1117
|
|
|
preg_match_all('~\{\{\s*(.*?)\s*\}\}~i', $to, $matches); |
1118
|
|
|
|
1119
|
|
|
throw new UnexpectedValueException(sprintf( |
1120
|
|
|
'The rename pattern failed. Leftover tokens found: %s', |
1121
|
|
|
implode(', ', $matches[1]) |
1122
|
|
|
)); |
1123
|
|
|
} |
1124
|
|
|
|
1125
|
|
|
$to = str_replace($info['basename'], $to, $from); |
1126
|
|
|
|
1127
|
|
|
return $to; |
1128
|
|
|
} |
1129
|
|
|
|
1130
|
|
|
/** |
1131
|
|
|
* Generate a new filename from the property. |
1132
|
|
|
* |
1133
|
|
|
* @param string|null $extension An extension to append to the generated filename. |
1134
|
|
|
* @return string |
1135
|
|
|
*/ |
1136
|
|
|
public function generateFilename($extension = null) |
1137
|
|
|
{ |
1138
|
|
|
$filename = $this->sanitizeFilename($this['fallbackFilename']); |
1139
|
|
|
$filename = $filename.' '.date('Y-m-d\TH-i-s'); |
1140
|
|
|
|
1141
|
|
|
if ($extension !== null) { |
1142
|
|
|
return $filename.'.'.$extension; |
1143
|
|
|
} |
1144
|
|
|
|
1145
|
|
|
return $filename; |
1146
|
|
|
} |
1147
|
|
|
|
1148
|
|
|
/** |
1149
|
|
|
* Generate a unique filename. |
1150
|
|
|
* |
1151
|
|
|
* @param string|array $filename The filename to alter. |
1152
|
|
|
* @throws InvalidArgumentException If the given filename is invalid. |
1153
|
|
|
* @return string |
1154
|
|
|
*/ |
1155
|
|
|
public function generateUniqueFilename($filename) |
1156
|
|
|
{ |
1157
|
|
|
if (is_string($filename)) { |
1158
|
|
|
$info = pathinfo($filename); |
1159
|
|
|
} else { |
1160
|
|
|
$info = $filename; |
1161
|
|
|
} |
1162
|
|
|
|
1163
|
|
|
if (!isset($info['filename']) || strlen($info['filename']) === 0) { |
1164
|
|
|
throw new InvalidArgumentException(sprintf( |
1165
|
|
|
'File must be a string [file path] or an array [pathfino()], received %s', |
1166
|
|
|
(is_object($filename) ? get_class($filename) : gettype($filename)) |
1167
|
|
|
)); |
1168
|
|
|
} |
1169
|
|
|
|
1170
|
|
|
$filename = $info['filename'].'-'.uniqid(); |
1171
|
|
|
|
1172
|
|
|
if (isset($info['extension']) && strlen($info['extension']) > 0) { |
1173
|
|
|
$filename .= '.'.$info['extension']; |
1174
|
|
|
} |
1175
|
|
|
|
1176
|
|
|
return $filename; |
1177
|
|
|
} |
1178
|
|
|
|
1179
|
|
|
/** |
1180
|
|
|
* Generate the file extension from the property value. |
1181
|
|
|
* |
1182
|
|
|
* @todo Refactor to support multilingual/multiple files. |
1183
|
|
|
* |
1184
|
|
|
* @return string Returns the file extension based on the MIME type for the first value. |
1185
|
|
|
*/ |
1186
|
|
|
public function generateExtension() |
1187
|
|
|
{ |
1188
|
|
|
$type = $this->getMimetype(); |
1189
|
|
|
|
1190
|
|
|
return $this->resolveExtensionFromMimeType($type); |
1191
|
|
|
} |
1192
|
|
|
|
1193
|
|
|
/** |
1194
|
|
|
* Generate a file extension from the given file path. |
1195
|
|
|
* |
1196
|
|
|
* @param string $file The file to parse. |
1197
|
|
|
* @return string|null The extension based on the file's MIME type. |
1198
|
|
|
*/ |
1199
|
|
|
public function generateExtensionFromFile($file) |
1200
|
|
|
{ |
1201
|
|
|
if ($this->hasAcceptedMimetypes()) { |
1202
|
|
|
$type = $this->getMimetypeFor($file); |
1203
|
|
|
|
1204
|
|
|
return $this->resolveExtensionFromMimeType($type); |
1205
|
|
|
} |
1206
|
|
|
|
1207
|
|
|
if (!is_string($file) || !defined('FILEINFO_EXTENSION')) { |
1208
|
|
|
return null; |
1209
|
|
|
} |
1210
|
|
|
|
1211
|
|
|
// PHP 7.2 |
1212
|
|
|
$info = new finfo(FILEINFO_EXTENSION); |
1213
|
|
|
$ext = $info->file($file); |
1214
|
|
|
|
1215
|
|
|
if ($ext === '???') { |
1216
|
|
|
return null; |
1217
|
|
|
} |
1218
|
|
|
|
1219
|
|
|
if (strpos($ext, '/') !== false) { |
1220
|
|
|
$ext = explode('/', $ext); |
1221
|
|
|
$ext = reset($ext); |
1222
|
|
|
} |
1223
|
|
|
|
1224
|
|
|
return $ext; |
1225
|
|
|
} |
1226
|
|
|
|
1227
|
|
|
/** |
1228
|
|
|
* Generate a file extension from the given MIME type. |
1229
|
|
|
* |
1230
|
|
|
* @param string $type The MIME type to parse. |
1231
|
|
|
* @return string|null The extension based on the MIME type. |
1232
|
|
|
*/ |
1233
|
|
|
public function generateExtensionFromMimeType($type) |
1234
|
|
|
{ |
1235
|
|
|
if (in_array($type, $this->getAcceptedMimetypes())) { |
1236
|
|
|
return $this->resolveExtensionFromMimeType($type); |
1237
|
|
|
} |
1238
|
|
|
|
1239
|
|
|
return null; |
1240
|
|
|
} |
1241
|
|
|
|
1242
|
|
|
/** |
1243
|
|
|
* Resolve the file extension from the given MIME type. |
1244
|
|
|
* |
1245
|
|
|
* This method should be overriden to provide available extensions. |
1246
|
|
|
* |
1247
|
|
|
* @param string $type The MIME type to resolve. |
1248
|
|
|
* @return string|null The extension based on the MIME type. |
1249
|
|
|
*/ |
1250
|
|
|
protected function resolveExtensionFromMimeType($type) |
1251
|
|
|
{ |
1252
|
|
|
switch ($type) { |
1253
|
|
|
case 'text/plain': |
1254
|
|
|
return 'txt'; |
1255
|
|
|
} |
1256
|
|
|
|
1257
|
|
|
return null; |
1258
|
|
|
} |
1259
|
|
|
|
1260
|
|
|
/** |
1261
|
|
|
* @param mixed $fallback The fallback filename. |
1262
|
|
|
* @return self |
1263
|
|
|
*/ |
1264
|
|
|
public function setFallbackFilename($fallback) |
1265
|
|
|
{ |
1266
|
|
|
$this->fallbackFilename = $this->translator()->translation($fallback); |
|
|
|
|
1267
|
|
|
return $this; |
1268
|
|
|
} |
1269
|
|
|
|
1270
|
|
|
/** |
1271
|
|
|
* @return Translation|null |
1272
|
|
|
*/ |
1273
|
|
|
public function getFallbackFilename() |
1274
|
|
|
{ |
1275
|
|
|
if ($this->fallbackFilename === null) { |
1276
|
|
|
return $this['label']; |
1277
|
|
|
} |
1278
|
|
|
|
1279
|
|
|
return $this->fallbackFilename; |
|
|
|
|
1280
|
|
|
} |
1281
|
|
|
|
1282
|
|
|
/** |
1283
|
|
|
* @return string |
1284
|
|
|
*/ |
1285
|
|
|
public function getFilesystem() |
1286
|
|
|
{ |
1287
|
|
|
return $this->filesystem; |
1288
|
|
|
} |
1289
|
|
|
|
1290
|
|
|
/** |
1291
|
|
|
* @param string $filesystem The file system. |
1292
|
|
|
* @return self |
1293
|
|
|
*/ |
1294
|
|
|
public function setFilesystem($filesystem) |
1295
|
|
|
{ |
1296
|
|
|
$this->filesystem = $filesystem; |
1297
|
|
|
|
1298
|
|
|
return $this; |
1299
|
|
|
} |
1300
|
|
|
|
1301
|
|
|
/** |
1302
|
|
|
* Inject dependencies from a DI Container. |
1303
|
|
|
* |
1304
|
|
|
* @param Container $container A dependencies container instance. |
1305
|
|
|
* @return void |
1306
|
|
|
*/ |
1307
|
|
|
protected function setDependencies(Container $container) |
1308
|
|
|
{ |
1309
|
|
|
parent::setDependencies($container); |
1310
|
|
|
|
1311
|
|
|
$this->basePath = $container['config']['base_path']; |
1312
|
|
|
$this->publicPath = $container['config']['public_path']; |
1313
|
|
|
} |
1314
|
|
|
|
1315
|
|
|
/** |
1316
|
|
|
* Retrieve the base path to the storage directory. |
1317
|
|
|
* |
1318
|
|
|
* @return string |
1319
|
|
|
*/ |
1320
|
|
|
protected function basePath() |
1321
|
|
|
{ |
1322
|
|
|
if ($this['publicAccess']) { |
1323
|
|
|
return $this->publicPath; |
1324
|
|
|
} |
1325
|
|
|
|
1326
|
|
|
return $this->basePath; |
1327
|
|
|
} |
1328
|
|
|
|
1329
|
|
|
/** |
1330
|
|
|
* Build the path for a named route including the base path. |
1331
|
|
|
* |
1332
|
|
|
* The {@see self::basePath() base path} will be prepended to the given $path. |
1333
|
|
|
* |
1334
|
|
|
* If the given $path does not start with the {@see self::getUploadPath() upload path}, |
1335
|
|
|
* it will be prepended. |
1336
|
|
|
* |
1337
|
|
|
* @param string $path The end path. |
1338
|
|
|
* @return string |
1339
|
|
|
*/ |
1340
|
|
|
protected function pathFor($path) |
1341
|
|
|
{ |
1342
|
|
|
$path = trim($path, '/'); |
1343
|
|
|
$uploadPath = trim($this['uploadPath'], '/'); |
1344
|
|
|
$basePath = rtrim($this->basePath(), '/'); |
1345
|
|
|
|
1346
|
|
|
if (strpos($path, $uploadPath) !== 0) { |
1347
|
|
|
$basePath .= '/'.$uploadPath; |
1348
|
|
|
} |
1349
|
|
|
|
1350
|
|
|
return $basePath.'/'.$path; |
1351
|
|
|
} |
1352
|
|
|
|
1353
|
|
|
/** |
1354
|
|
|
* Attempts to create the upload path. |
1355
|
|
|
* |
1356
|
|
|
* @throws Exception If the upload path is unavailable. |
1357
|
|
|
* @return void |
1358
|
|
|
*/ |
1359
|
|
|
protected function assertValidUploadPath() |
1360
|
|
|
{ |
1361
|
|
|
$uploadPath = $this->pathFor($this['uploadPath']); |
1362
|
|
|
|
1363
|
|
|
if (!file_exists($uploadPath)) { |
1364
|
|
|
$this->logger->debug(sprintf( |
1365
|
|
|
'[%s] Upload directory [%s] does not exist; attempting to create path', |
1366
|
|
|
[ get_called_class().'::'.__FUNCTION__ ], |
1367
|
|
|
$uploadPath |
1368
|
|
|
)); |
1369
|
|
|
|
1370
|
|
|
mkdir($uploadPath, 0777, true); |
1371
|
|
|
} |
1372
|
|
|
|
1373
|
|
|
if (!is_writable($uploadPath)) { |
1374
|
|
|
throw new Exception(sprintf( |
1375
|
|
|
'Upload directory [%s] is not writeable', |
1376
|
|
|
$uploadPath |
1377
|
|
|
)); |
1378
|
|
|
} |
1379
|
|
|
} |
1380
|
|
|
|
1381
|
|
|
/** |
1382
|
|
|
* Converts a php.ini notation for size to an integer. |
1383
|
|
|
* |
1384
|
|
|
* @param mixed $size A php.ini notation for size. |
1385
|
|
|
* @throws InvalidArgumentException If the given parameter is invalid. |
1386
|
|
|
* @return integer Returns the size in bytes. |
1387
|
|
|
*/ |
1388
|
|
|
protected function parseIniSize($size) |
1389
|
|
|
{ |
1390
|
|
|
if (is_numeric($size)) { |
1391
|
|
|
return $size; |
1392
|
|
|
} |
1393
|
|
|
|
1394
|
|
|
if (!is_string($size)) { |
1395
|
|
|
throw new InvalidArgumentException( |
1396
|
|
|
'Size must be an integer (in bytes, e.g.: 1024) or a string (e.g.: 1M)' |
1397
|
|
|
); |
1398
|
|
|
} |
1399
|
|
|
|
1400
|
|
|
$quant = 'bkmgtpezy'; |
1401
|
|
|
$unit = preg_replace('/[^'.$quant.']/i', '', $size); |
1402
|
|
|
$size = preg_replace('/[^0-9\.]/', '', $size); |
1403
|
|
|
|
1404
|
|
|
if ($unit) { |
1405
|
|
|
$size = ($size * pow(1024, stripos($quant, $unit[0]))); |
1406
|
|
|
} |
1407
|
|
|
|
1408
|
|
|
return round($size); |
1409
|
|
|
} |
1410
|
|
|
|
1411
|
|
|
/** |
1412
|
|
|
* Determine if the given MIME type is acceptable. |
1413
|
|
|
* |
1414
|
|
|
* @param string $type A MIME type. |
1415
|
|
|
* @param string[] $accepted One or many acceptable MIME types. |
1416
|
|
|
* Defaults to the property's "acceptedMimetypes". |
1417
|
|
|
* @return boolean Returns TRUE if the MIME type is acceptable. |
1418
|
|
|
* Otherwise, returns FALSE. |
1419
|
|
|
*/ |
1420
|
|
|
protected function isAcceptedMimeType($type, array $accepted = null) |
1421
|
|
|
{ |
1422
|
|
|
if ($accepted === null) { |
1423
|
|
|
$accepted = $this['acceptedMimetypes']; |
1424
|
|
|
} |
1425
|
|
|
|
1426
|
|
|
if (empty($accepted)) { |
1427
|
|
|
return true; |
1428
|
|
|
} |
1429
|
|
|
|
1430
|
|
|
return in_array($type, $accepted); |
1431
|
|
|
} |
1432
|
|
|
|
1433
|
|
|
/** |
1434
|
|
|
* Determine if the given file size is acceptable. |
1435
|
|
|
* |
1436
|
|
|
* @param integer $size Number of bytes. |
1437
|
|
|
* @param integer $max The maximum number of bytes allowed. |
1438
|
|
|
* Defaults to the property's "maxFilesize". |
1439
|
|
|
* @return boolean Returns TRUE if the size is acceptable. |
1440
|
|
|
* Otherwise, returns FALSE. |
1441
|
|
|
*/ |
1442
|
|
|
protected function isAcceptedFilesize($size, $max = null) |
1443
|
|
|
{ |
1444
|
|
|
if ($max === null) { |
1445
|
|
|
$max = $this['maxFilesize']; |
1446
|
|
|
} |
1447
|
|
|
|
1448
|
|
|
if (empty($max)) { |
1449
|
|
|
return true; |
1450
|
|
|
} |
1451
|
|
|
|
1452
|
|
|
return ($size <= $max); |
1453
|
|
|
} |
1454
|
|
|
|
1455
|
|
|
/** |
1456
|
|
|
* Determine if the given file path is an absolute path. |
1457
|
|
|
* |
1458
|
|
|
* Note: Adapted from symfony\filesystem. |
1459
|
|
|
* |
1460
|
|
|
* @see https://github.com/symfony/symfony/blob/v3.2.2/LICENSE |
1461
|
|
|
* |
1462
|
|
|
* @param string $file A file path. |
1463
|
|
|
* @return boolean Returns TRUE if the given path is absolute. Otherwise, returns FALSE. |
1464
|
|
|
*/ |
1465
|
|
|
protected function isAbsolutePath($file) |
1466
|
|
|
{ |
1467
|
|
|
$file = (string)$file; |
1468
|
|
|
|
1469
|
|
|
return strspn($file, '/\\', 0, 1) |
1470
|
|
|
|| (strlen($file) > 3 |
1471
|
|
|
&& ctype_alpha($file[0]) |
1472
|
|
|
&& substr($file, 1, 1) === ':' |
1473
|
|
|
&& strspn($file, '/\\', 2, 1)) |
1474
|
|
|
|| null !== parse_url($file, PHP_URL_SCHEME); |
1475
|
|
|
} |
1476
|
|
|
|
1477
|
|
|
/** |
1478
|
|
|
* Determine if the given value is a data URI. |
1479
|
|
|
* |
1480
|
|
|
* @param mixed $val The value to check. |
1481
|
|
|
* @return boolean |
1482
|
|
|
*/ |
1483
|
|
|
protected function isDataUri($val) |
1484
|
|
|
{ |
1485
|
|
|
return is_string($val) && preg_match('/^data:/i', $val); |
1486
|
|
|
} |
1487
|
|
|
|
1488
|
|
|
/** |
1489
|
|
|
* Determine if the given value is a data array. |
1490
|
|
|
* |
1491
|
|
|
* @param mixed $val The value to check. |
1492
|
|
|
* @return boolean |
1493
|
|
|
*/ |
1494
|
|
|
protected function isDataArr($val) |
1495
|
|
|
{ |
1496
|
|
|
return is_array($val) && isset($val['id']); |
1497
|
|
|
} |
1498
|
|
|
|
1499
|
|
|
/** |
1500
|
|
|
* Retrieve the rename pattern tokens for the given file. |
1501
|
|
|
* |
1502
|
|
|
* @param string|array $path The string to be parsed or an associative array of information about the file. |
1503
|
|
|
* @param array|callable $args Extra rename tokens. |
1504
|
|
|
* @throws InvalidArgumentException If the given arguments are invalid. |
1505
|
|
|
* @throws UnexpectedValueException If the given path is invalid. |
1506
|
|
|
* @return string Returns the rendered target. |
1507
|
|
|
*/ |
1508
|
|
|
private function renamePatternArgs($path, $args = null) |
1509
|
|
|
{ |
1510
|
|
|
if (!is_string($path) && !is_array($path)) { |
1511
|
|
|
throw new InvalidArgumentException(sprintf( |
1512
|
|
|
'The target must be a string or an array from [pathfino()], received %s', |
1513
|
|
|
(is_object($path) ? get_class($path) : gettype($path)) |
1514
|
|
|
)); |
1515
|
|
|
} |
1516
|
|
|
|
1517
|
|
|
if (is_string($path)) { |
1518
|
|
|
$info = pathinfo($path); |
1519
|
|
|
} else { |
1520
|
|
|
$info = $path; |
1521
|
|
|
} |
1522
|
|
|
|
1523
|
|
|
if (!isset($info['basename']) || $info['basename'] === '') { |
1524
|
|
|
throw new UnexpectedValueException( |
1525
|
|
|
'The basename is missing from the target' |
1526
|
|
|
); |
1527
|
|
|
} |
1528
|
|
|
|
1529
|
|
|
if (!isset($info['filename']) || $info['filename'] === '') { |
1530
|
|
|
throw new UnexpectedValueException( |
1531
|
|
|
'The filename is missing from the target' |
1532
|
|
|
); |
1533
|
|
|
} |
1534
|
|
|
|
1535
|
|
|
if (!isset($info['extension'])) { |
1536
|
|
|
$info['extension'] = ''; |
1537
|
|
|
} |
1538
|
|
|
|
1539
|
|
|
$defaults = [ |
1540
|
|
|
'{{property}}' => $this->ident(), |
1541
|
|
|
'{{label}}' => $this['label'], |
1542
|
|
|
'{{fallback}}' => $this['fallbackFilename'], |
1543
|
|
|
'{{extension}}' => $info['extension'], |
1544
|
|
|
'{{basename}}' => $info['basename'], |
1545
|
|
|
'{{filename}}' => $info['filename'], |
1546
|
|
|
]; |
1547
|
|
|
|
1548
|
|
|
if ($args === null) { |
1549
|
|
|
$args = $defaults; |
1550
|
|
|
} else { |
1551
|
|
|
if (is_callable($args)) { |
1552
|
|
|
/** |
1553
|
|
|
* Rename Arguments Callback Routine |
1554
|
|
|
* |
1555
|
|
|
* @param array $info Information about the file path from {@see pathinfo()}. |
1556
|
|
|
* @param PropertyInterface $prop The related image property. |
1557
|
|
|
* @return array |
1558
|
|
|
*/ |
1559
|
|
|
$args = $args($info, $this); |
1560
|
|
|
} |
1561
|
|
|
|
1562
|
|
|
if (is_array($args)) { |
1563
|
|
|
$args = array_replace($defaults, $args); |
1564
|
|
|
} else { |
1565
|
|
|
throw new InvalidArgumentException(sprintf( |
1566
|
|
|
'Arguments must be an array or a callable that returns an array, received %s', |
1567
|
|
|
(is_object($args) ? get_class($args) : gettype($args)) |
1568
|
|
|
)); |
1569
|
|
|
} |
1570
|
|
|
} |
1571
|
|
|
|
1572
|
|
|
return $args; |
1573
|
|
|
} |
1574
|
|
|
|
1575
|
|
|
/** |
1576
|
|
|
* Retrieve normalized file upload data for this property. |
1577
|
|
|
* |
1578
|
|
|
* @return array A tree of normalized $_FILE entries. |
1579
|
|
|
*/ |
1580
|
|
|
public function getUploadedFiles() |
1581
|
|
|
{ |
1582
|
|
|
$propIdent = $this->ident(); |
1583
|
|
|
|
1584
|
|
|
$filterErrNoFile = function (array $file) { |
1585
|
|
|
return $file['error'] !== UPLOAD_ERR_NO_FILE; |
1586
|
|
|
}; |
1587
|
|
|
$uploadedFiles = static::parseUploadedFiles($_FILES, $filterErrNoFile, $propIdent); |
1588
|
|
|
|
1589
|
|
|
return $uploadedFiles; |
1590
|
|
|
} |
1591
|
|
|
|
1592
|
|
|
/** |
1593
|
|
|
* Parse a non-normalized, i.e. $_FILES superglobal, tree of uploaded file data. |
1594
|
|
|
* |
1595
|
|
|
* @link https://github.com/slimphp/Slim/blob/3.12.1/Slim/Http/UploadedFile.php |
1596
|
|
|
* Adapted from slim/slim. |
1597
|
|
|
* |
1598
|
|
|
* @todo Add support for "dot" notation on $searchKey. |
1599
|
|
|
* |
1600
|
|
|
* @param array $uploadedFiles The non-normalized tree of uploaded file data. |
1601
|
|
|
* @param callable $filterCallback If specified, the callback function to used to filter files. |
1602
|
|
|
* @param mixed $searchKey If specified, then only top-level keys containing these values are returned. |
1603
|
|
|
* @return array A tree of normalized $_FILE entries. |
1604
|
|
|
*/ |
1605
|
|
|
public static function parseUploadedFiles(array $uploadedFiles, callable $filterCallback = null, $searchKey = null) |
1606
|
|
|
{ |
1607
|
|
|
if ($searchKey !== null) { |
1608
|
|
|
if (is_array($searchKey)) { |
1609
|
|
|
$uploadedFiles = array_intersect_key($uploadedFiles, array_flip($searchKey)); |
1610
|
|
|
return static::parseUploadedFiles($uploadedFiles, $filterCallback); |
1611
|
|
|
} |
1612
|
|
|
|
1613
|
|
|
if (isset($uploadedFiles[$searchKey])) { |
1614
|
|
|
$uploadedFiles = [ |
1615
|
|
|
$searchKey => $uploadedFiles[$searchKey], |
1616
|
|
|
]; |
1617
|
|
|
$parsedFiles = static::parseUploadedFiles($uploadedFiles, $filterCallback); |
1618
|
|
|
if (isset($parsedFiles[$searchKey])) { |
1619
|
|
|
return $parsedFiles[$searchKey]; |
1620
|
|
|
} |
1621
|
|
|
} |
1622
|
|
|
|
1623
|
|
|
return []; |
1624
|
|
|
} |
1625
|
|
|
|
1626
|
|
|
$parsedFiles = []; |
1627
|
|
|
foreach ($uploadedFiles as $field => $uploadedFile) { |
1628
|
|
|
if (!isset($uploadedFile['error'])) { |
1629
|
|
|
if (is_array($uploadedFile)) { |
1630
|
|
|
$subArray = static::parseUploadedFiles($uploadedFile, $filterCallback); |
1631
|
|
View Code Duplication |
if (!empty($subArray)) { |
|
|
|
|
1632
|
|
|
if (!isset($parsedFiles[$field])) { |
1633
|
|
|
$parsedFiles[$field] = []; |
1634
|
|
|
} |
1635
|
|
|
|
1636
|
|
|
$parsedFiles[$field] = $subArray; |
1637
|
|
|
} |
1638
|
|
|
} |
1639
|
|
|
continue; |
1640
|
|
|
} |
1641
|
|
|
|
1642
|
|
|
if (!is_array($uploadedFile['error'])) { |
1643
|
|
|
if ($filterCallback === null || $filterCallback($uploadedFile, $field) === true) { |
1644
|
|
|
if (!isset($parsedFiles[$field])) { |
1645
|
|
|
$parsedFiles[$field] = []; |
1646
|
|
|
} |
1647
|
|
|
|
1648
|
|
|
$parsedFiles[$field] = [ |
1649
|
|
|
'tmp_name' => $uploadedFile['tmp_name'], |
1650
|
|
|
'name' => isset($uploadedFile['name']) ? $uploadedFile['name'] : null, |
1651
|
|
|
'type' => isset($uploadedFile['type']) ? $uploadedFile['type'] : null, |
1652
|
|
|
'size' => isset($uploadedFile['size']) ? $uploadedFile['size'] : null, |
1653
|
|
|
'error' => $uploadedFile['error'], |
1654
|
|
|
]; |
1655
|
|
|
} |
1656
|
|
|
} else { |
1657
|
|
|
$subArray = []; |
1658
|
|
|
foreach ($uploadedFile['error'] as $fileIdx => $error) { |
1659
|
|
|
// normalise subarray and re-parse to move the input's keyname up a level |
1660
|
|
|
$subArray[$fileIdx] = [ |
1661
|
|
|
'tmp_name' => $uploadedFile['tmp_name'][$fileIdx], |
1662
|
|
|
'name' => $uploadedFile['name'][$fileIdx], |
1663
|
|
|
'type' => $uploadedFile['type'][$fileIdx], |
1664
|
|
|
'size' => $uploadedFile['size'][$fileIdx], |
1665
|
|
|
'error' => $uploadedFile['error'][$fileIdx], |
1666
|
|
|
]; |
1667
|
|
|
|
1668
|
|
|
$subArray = static::parseUploadedFiles($subArray, $filterCallback); |
1669
|
|
View Code Duplication |
if (!empty($subArray)) { |
|
|
|
|
1670
|
|
|
if (!isset($parsedFiles[$field])) { |
1671
|
|
|
$parsedFiles[$field] = []; |
1672
|
|
|
} |
1673
|
|
|
|
1674
|
|
|
$parsedFiles[$field] = $subArray; |
1675
|
|
|
} |
1676
|
|
|
} |
1677
|
|
|
} |
1678
|
|
|
} |
1679
|
|
|
|
1680
|
|
|
return $parsedFiles; |
1681
|
|
|
} |
1682
|
|
|
|
1683
|
|
|
/** |
1684
|
|
|
* Normalize a file path string so that it can be checked safely. |
1685
|
|
|
* |
1686
|
|
|
* Attempt to avoid invalid encoding bugs by transcoding the path. Then |
1687
|
|
|
* remove any unnecessary path components including '.', '..' and ''. |
1688
|
|
|
* |
1689
|
|
|
* @link https://gist.github.com/thsutton/772287 |
1690
|
|
|
* |
1691
|
|
|
* @param string $path The path to normalise. |
1692
|
|
|
* @param string $encoding The name of the path iconv() encoding. |
1693
|
|
|
* @return string The path, normalised. |
1694
|
|
|
*/ |
1695
|
|
|
public static function normalizePath($path, $encoding = 'UTF-8') |
1696
|
|
|
{ |
1697
|
|
|
$key = $path; |
1698
|
|
|
|
1699
|
|
|
if (isset(static::$normalizePathCache[$key])) { |
1700
|
|
|
return static::$normalizePathCache[$key]; |
1701
|
|
|
} |
1702
|
|
|
|
1703
|
|
|
// Attempt to avoid path encoding problems. |
1704
|
|
|
$path = iconv($encoding, $encoding.'//IGNORE//TRANSLIT', $path); |
1705
|
|
|
|
1706
|
|
|
if (strpos($path, '..') !== false || strpos($path, './') !== false) { |
1707
|
|
|
// Process the components |
1708
|
|
|
$parts = explode('/', $path); |
1709
|
|
|
$safe = []; |
1710
|
|
|
foreach ($parts as $idx => $part) { |
1711
|
|
|
if ((empty($part) && !is_numeric($part)) || ($part === '.')) { |
1712
|
|
|
continue; |
1713
|
|
|
} elseif ($part === '..') { |
1714
|
|
|
array_pop($safe); |
1715
|
|
|
continue; |
1716
|
|
|
} else { |
1717
|
|
|
$safe[] = $part; |
1718
|
|
|
} |
1719
|
|
|
} |
1720
|
|
|
|
1721
|
|
|
// Return the "clean" path |
1722
|
|
|
$path = implode(DIRECTORY_SEPARATOR, $safe); |
1723
|
|
|
|
1724
|
|
|
if ($key[0] === '/' && $path[0] !== '/') { |
1725
|
|
|
$path = '/'.$path; |
1726
|
|
|
} |
1727
|
|
|
} |
1728
|
|
|
|
1729
|
|
|
static::$normalizePathCache[$key] = $path; |
1730
|
|
|
|
1731
|
|
|
return static::$normalizePathCache[$key]; |
1732
|
|
|
} |
1733
|
|
|
} |
1734
|
|
|
|
Our type inference engine has found an assignment of a scalar value (like a string, an integer or null) to a property which is an array.
Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property.
To type hint that a parameter can be either an array or null, you can set a type hint of array and a default value of null. The PHP interpreter will then accept both an array or null for that parameter.
The function can be called with either null or an array for the parameter
$needle
but will only accept an array as$haystack
.