1 | <?php |
||||
2 | |||||
3 | namespace LeKoala\FilePond; |
||||
4 | |||||
5 | use Exception; |
||||
6 | use LogicException; |
||||
7 | use RuntimeException; |
||||
8 | use SilverStripe\Forms\Form; |
||||
9 | use SilverStripe\Assets\File; |
||||
10 | use SilverStripe\ORM\SS_List; |
||||
11 | use SilverStripe\Assets\Image; |
||||
12 | use SilverStripe\Core\Convert; |
||||
13 | use SilverStripe\ORM\ArrayList; |
||||
14 | use SilverStripe\ORM\DataObject; |
||||
15 | use SilverStripe\Control\Director; |
||||
16 | use SilverStripe\Security\Security; |
||||
17 | use SilverStripe\View\Requirements; |
||||
18 | use SilverStripe\Control\HTTPRequest; |
||||
19 | use SilverStripe\Versioned\Versioned; |
||||
20 | use SilverStripe\Control\HTTPResponse; |
||||
21 | use SilverStripe\ORM\DataObjectInterface; |
||||
22 | use SilverStripe\ORM\ValidationException; |
||||
23 | use SilverStripe\ORM\FieldType\DBHTMLText; |
||||
24 | |||||
25 | /** |
||||
26 | * A FilePond field |
||||
27 | */ |
||||
28 | class FilePondField extends AbstractUploadField |
||||
29 | { |
||||
30 | const IMAGE_MODE_MIN = "min"; |
||||
31 | const IMAGE_MODE_MAX = "max"; |
||||
32 | const IMAGE_MODE_CROP = "crop"; |
||||
33 | const IMAGE_MODE_RESIZE = "resize"; |
||||
34 | const IMAGE_MODE_CROP_RESIZE = "crop_resize"; |
||||
35 | const DEFAULT_POSTER_HEIGHT = 264; |
||||
36 | const DEFAULT_POSTER_WIDTH = 352; |
||||
37 | |||||
38 | /** |
||||
39 | * @config |
||||
40 | * @var array<string> |
||||
41 | */ |
||||
42 | private static $allowed_actions = [ |
||||
0 ignored issues
–
show
introduced
by
![]() |
|||||
43 | 'upload', |
||||
44 | 'chunk', |
||||
45 | 'revert', |
||||
46 | ]; |
||||
47 | |||||
48 | /** |
||||
49 | * @config |
||||
50 | * @var boolean |
||||
51 | */ |
||||
52 | private static $enable_requirements = true; |
||||
53 | |||||
54 | /** |
||||
55 | * @config |
||||
56 | * @var boolean |
||||
57 | */ |
||||
58 | private static $enable_poster = false; |
||||
59 | |||||
60 | /** |
||||
61 | * @config |
||||
62 | * @var boolean |
||||
63 | */ |
||||
64 | private static $chunk_by_default = false; |
||||
65 | |||||
66 | /** |
||||
67 | * @config |
||||
68 | * @var boolean |
||||
69 | */ |
||||
70 | private static $enable_default_description = true; |
||||
0 ignored issues
–
show
|
|||||
71 | |||||
72 | /** |
||||
73 | * @config |
||||
74 | * @var boolean |
||||
75 | */ |
||||
76 | private static $auto_clear_temp_folder = true; |
||||
77 | |||||
78 | /** |
||||
79 | * @config |
||||
80 | * @var bool |
||||
81 | */ |
||||
82 | private static $auto_clear_threshold = true; |
||||
83 | |||||
84 | /** |
||||
85 | * @config |
||||
86 | * @var boolean |
||||
87 | */ |
||||
88 | private static $enable_auto_thumbnails = false; |
||||
0 ignored issues
–
show
|
|||||
89 | |||||
90 | /** |
||||
91 | * @config |
||||
92 | * @var int |
||||
93 | */ |
||||
94 | private static $poster_width = 352; |
||||
95 | |||||
96 | /** |
||||
97 | * @config |
||||
98 | * @var int |
||||
99 | */ |
||||
100 | private static $poster_height = 264; |
||||
101 | |||||
102 | /** |
||||
103 | * @var array<string|int,mixed|null> |
||||
104 | */ |
||||
105 | protected $filePondConfig = []; |
||||
106 | |||||
107 | /** |
||||
108 | * @var array<string,mixed>|null |
||||
109 | */ |
||||
110 | protected $customServerConfig = null; |
||||
111 | |||||
112 | /** |
||||
113 | * @var ?int |
||||
114 | */ |
||||
115 | protected $posterHeight = null; |
||||
116 | |||||
117 | /** |
||||
118 | * @var ?int |
||||
119 | */ |
||||
120 | protected $posterWidth = null; |
||||
121 | |||||
122 | /** |
||||
123 | * Create a new file field. |
||||
124 | * |
||||
125 | * @param string $name The internal field name, passed to forms. |
||||
126 | * @param string $title The field label. |
||||
127 | * @param SS_List $items Items assigned to this field |
||||
128 | */ |
||||
129 | public function __construct($name, $title = null, SS_List $items = null) |
||||
130 | { |
||||
131 | parent::__construct($name, $title, $items); |
||||
132 | |||||
133 | if (self::config()->chunk_by_default) { |
||||
134 | $this->setChunkUploads(true); |
||||
135 | } |
||||
136 | } |
||||
137 | |||||
138 | /** |
||||
139 | * Set a custom config value for this field |
||||
140 | * |
||||
141 | * @link https://pqina.nl/filepond/docs/patterns/api/filepond-instance/#properties |
||||
142 | * @param string $k |
||||
143 | * @param string|bool|float|int|array<mixed> $v |
||||
144 | * @return $this |
||||
145 | */ |
||||
146 | public function addFilePondConfig($k, $v) |
||||
147 | { |
||||
148 | $this->filePondConfig[$k] = $v; |
||||
149 | return $this; |
||||
150 | } |
||||
151 | |||||
152 | /** |
||||
153 | * @param string $k |
||||
154 | * @param mixed $default |
||||
155 | * @return mixed |
||||
156 | */ |
||||
157 | public function getCustomConfigValue($k, $default = null) |
||||
158 | { |
||||
159 | if (isset($this->filePondConfig[$k])) { |
||||
160 | return $this->filePondConfig[$k]; |
||||
161 | } |
||||
162 | return $default; |
||||
163 | } |
||||
164 | |||||
165 | /** |
||||
166 | * Custom configuration applied to this field |
||||
167 | * |
||||
168 | * @return array<mixed> |
||||
169 | */ |
||||
170 | public function getCustomFilePondConfig() |
||||
171 | { |
||||
172 | return $this->filePondConfig; |
||||
173 | } |
||||
174 | |||||
175 | /** |
||||
176 | * Get the value of chunkUploads |
||||
177 | * @return bool |
||||
178 | */ |
||||
179 | public function getChunkUploads() |
||||
180 | { |
||||
181 | if (!isset($this->filePondConfig['chunkUploads'])) { |
||||
182 | return false; |
||||
183 | } |
||||
184 | return $this->filePondConfig['chunkUploads']; |
||||
185 | } |
||||
186 | |||||
187 | /** |
||||
188 | * Get the value of customServerConfig |
||||
189 | * @return array<mixed> |
||||
190 | */ |
||||
191 | public function getCustomServerConfig() |
||||
192 | { |
||||
193 | return $this->customServerConfig; |
||||
194 | } |
||||
195 | |||||
196 | /** |
||||
197 | * Set the value of customServerConfig |
||||
198 | * |
||||
199 | * @param array<mixed> $customServerConfig |
||||
200 | * @return $this |
||||
201 | */ |
||||
202 | public function setCustomServerConfig(array $customServerConfig) |
||||
203 | { |
||||
204 | $this->customServerConfig = $customServerConfig; |
||||
205 | return $this; |
||||
206 | } |
||||
207 | |||||
208 | /** |
||||
209 | * Set the value of chunkUploads |
||||
210 | * |
||||
211 | * Note: please set max file upload first if you want |
||||
212 | * to see the size limit in the description |
||||
213 | * |
||||
214 | * @param bool $chunkUploads |
||||
215 | * @return $this |
||||
216 | */ |
||||
217 | public function setChunkUploads($chunkUploads) |
||||
218 | { |
||||
219 | $this->addFilePondConfig('chunkUploads', true); |
||||
220 | $this->addFilePondConfig('chunkForce', true); |
||||
221 | $this->addFilePondConfig('chunkSize', $this->computeMaxChunkSize()); |
||||
222 | if ($this->isDefaultMaxFileSize()) { |
||||
223 | $this->showDescriptionSize = false; |
||||
224 | } |
||||
225 | return $this; |
||||
226 | } |
||||
227 | |||||
228 | /** |
||||
229 | * @param array<mixed> $sizes |
||||
230 | * @return array<mixed> |
||||
231 | */ |
||||
232 | public function getImageSizeConfigFromArray($sizes) |
||||
233 | { |
||||
234 | $mode = null; |
||||
235 | if (isset($sizes[2])) { |
||||
236 | $mode = $sizes[2]; |
||||
237 | } |
||||
238 | return $this->getImageSizeConfig($sizes[0], $sizes[1], $mode); |
||||
239 | } |
||||
240 | |||||
241 | /** |
||||
242 | * @param int $width |
||||
243 | * @param int $height |
||||
244 | * @param string $mode min|max|crop|resize|crop_resize |
||||
245 | * @return array<mixed> |
||||
246 | */ |
||||
247 | public function getImageSizeConfig($width, $height, $mode = null) |
||||
248 | { |
||||
249 | if ($mode === null) { |
||||
250 | $mode = self::IMAGE_MODE_MIN; |
||||
251 | } |
||||
252 | $config = []; |
||||
253 | switch ($mode) { |
||||
254 | case self::IMAGE_MODE_MIN: |
||||
255 | $config['imageValidateSizeMinWidth'] = $width; |
||||
256 | $config['imageValidateSizeMinHeight'] = $height; |
||||
257 | break; |
||||
258 | case self::IMAGE_MODE_MAX: |
||||
259 | $config['imageValidateSizeMaxWidth'] = $width; |
||||
260 | $config['imageValidateSizeMaxHeight'] = $height; |
||||
261 | break; |
||||
262 | case self::IMAGE_MODE_CROP: |
||||
263 | // It crops only to given ratio and tries to keep the largest image |
||||
264 | $config['allowImageCrop'] = true; |
||||
265 | $config['imageCropAspectRatio'] = "{$width}:{$height}"; |
||||
266 | break; |
||||
267 | case self::IMAGE_MODE_RESIZE: |
||||
268 | // Cover will respect the aspect ratio and will scale to fill the target dimensions |
||||
269 | $config['allowImageResize'] = true; |
||||
270 | $config['imageResizeTargetWidth'] = $width; |
||||
271 | $config['imageResizeTargetHeight'] = $height; |
||||
272 | |||||
273 | // Don't use these settings and keep api simple |
||||
274 | // $config['imageResizeMode'] = 'cover'; |
||||
275 | // $config['imageResizeUpscale'] = true; |
||||
276 | break; |
||||
277 | case self::IMAGE_MODE_CROP_RESIZE: |
||||
278 | $config['allowImageResize'] = true; |
||||
279 | $config['imageResizeTargetWidth'] = $width; |
||||
280 | $config['imageResizeTargetHeight'] = $height; |
||||
281 | $config['allowImageCrop'] = true; |
||||
282 | $config['imageCropAspectRatio'] = "{$width}:{$height}"; |
||||
283 | break; |
||||
284 | default: |
||||
285 | throw new Exception("Unsupported '$mode' mode"); |
||||
286 | } |
||||
287 | return $config; |
||||
288 | } |
||||
289 | |||||
290 | /** |
||||
291 | * @link https://pqina.nl/filepond/docs/api/plugins/image-crop/ |
||||
292 | * @link https://pqina.nl/filepond/docs/api/plugins/image-resize/ |
||||
293 | * @link https://pqina.nl/filepond/docs/api/plugins/image-validate-size/ |
||||
294 | * @param int $width |
||||
295 | * @param int $height |
||||
296 | * @param string $mode min|max|crop|resize|crop_resize |
||||
297 | * @return $this |
||||
298 | */ |
||||
299 | public function setImageSize($width, $height, $mode = null) |
||||
300 | { |
||||
301 | $config = $this->getImageSizeConfig($width, $height, $mode); |
||||
302 | foreach ($config as $k => $v) { |
||||
303 | $this->addFilePondConfig($k, $v); |
||||
304 | } |
||||
305 | |||||
306 | // We need a custom poster size |
||||
307 | $this->adjustPosterSize($width, $height); |
||||
308 | |||||
309 | return $this; |
||||
310 | } |
||||
311 | |||||
312 | /** |
||||
313 | * This is a frontend alternative to setRenamePattern |
||||
314 | * |
||||
315 | * @link https://pqina.nl/filepond/docs/api/plugins/file-rename/ |
||||
316 | * @param string $name The name (extension is added automatically) |
||||
317 | * @return $this |
||||
318 | */ |
||||
319 | public function setRenameFile($name) |
||||
320 | { |
||||
321 | $this->addFilePondConfig('fileRenameFunction', $name); |
||||
322 | return $this; |
||||
323 | } |
||||
324 | |||||
325 | /** |
||||
326 | * @param int $width |
||||
327 | * @param int $height |
||||
328 | * @return void |
||||
329 | */ |
||||
330 | protected function adjustPosterSize($width, $height) |
||||
331 | { |
||||
332 | // If the height is smaller than our default, make smaller |
||||
333 | if ($height < self::getDefaultPosterHeight()) { |
||||
334 | $this->posterHeight = $height; |
||||
335 | $this->posterWidth = $width; |
||||
336 | } else { |
||||
337 | // Adjust width to keep aspect ratio with our default height |
||||
338 | $ratio = $height / self::getDefaultPosterHeight(); |
||||
339 | //@phpstan-ignore-next-line |
||||
340 | $this->posterWidth = round($width / $ratio); |
||||
341 | } |
||||
342 | } |
||||
343 | |||||
344 | /** |
||||
345 | * @return int |
||||
346 | */ |
||||
347 | public function getPosterWidth() |
||||
348 | { |
||||
349 | if ($this->posterWidth) { |
||||
0 ignored issues
–
show
The expression
$this->posterWidth of type integer|null is loosely compared to true ; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.
In PHP, under loose comparison (like For 0 == false // true
0 == null // true
123 == false // false
123 == null // false
// It is often better to use strict comparison
0 === false // false
0 === null // false
![]() |
|||||
350 | return $this->posterWidth; |
||||
351 | } |
||||
352 | return self::getDefaultPosterWidth(); |
||||
353 | } |
||||
354 | |||||
355 | /** |
||||
356 | * @return int |
||||
357 | */ |
||||
358 | public function getPosterHeight() |
||||
359 | { |
||||
360 | if ($this->posterHeight) { |
||||
0 ignored issues
–
show
The expression
$this->posterHeight of type integer|null is loosely compared to true ; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.
In PHP, under loose comparison (like For 0 == false // true
0 == null // true
123 == false // false
123 == null // false
// It is often better to use strict comparison
0 === false // false
0 === null // false
![]() |
|||||
361 | return $this->posterHeight; |
||||
362 | } |
||||
363 | return self::getDefaultPosterHeight(); |
||||
364 | } |
||||
365 | |||||
366 | /** |
||||
367 | * Return the config applied for this field |
||||
368 | * |
||||
369 | * Typically converted to json and set in a data attribute |
||||
370 | * |
||||
371 | * @return array<string,mixed> |
||||
372 | */ |
||||
373 | public function getFilePondConfig() |
||||
374 | { |
||||
375 | $this->fixName(); |
||||
376 | $name = $this->getName(); |
||||
377 | $multiple = $this->getIsMultiUpload(); |
||||
378 | |||||
379 | $i18nConfig = [ |
||||
380 | 'labelIdle' => _t('FilePondField.labelIdle', 'Drag & Drop your files or <span class="filepond--label-action"> Browse </span>'), |
||||
381 | 'labelFileProcessing' => _t('FilePondField.labelFileProcessing', 'Uploading'), |
||||
382 | 'labelFileProcessingComplete' => _t('FilePondField.labelFileProcessingComplete', 'Upload complete'), |
||||
383 | 'labelFileProcessingAborted' => _t('FilePondField.labelFileProcessingAborted', 'Upload cancelled'), |
||||
384 | 'labelTapToCancel' => _t('FilePondField.labelTapToCancel', 'tap to cancel'), |
||||
385 | 'labelTapToRetry' => _t('FilePondField.labelTapToCancel', 'tap to retry'), |
||||
386 | 'labelTapToUndo' => _t('FilePondField.labelTapToCancel', 'tap to undo'), |
||||
387 | ]; |
||||
388 | |||||
389 | // Base config |
||||
390 | $config = [ |
||||
391 | 'name' => $name, // This will also apply to the hidden fields |
||||
392 | 'allowMultiple' => $multiple, |
||||
393 | 'maxFiles' => $this->getAllowedMaxFileNumber(), |
||||
394 | 'server' => $this->getServerOptions(), |
||||
395 | 'files' => $this->getExistingUploadsData(), |
||||
396 | ]; |
||||
397 | $maxFileSize = $this->getMaxFileSize(); |
||||
398 | if ($maxFileSize) { |
||||
399 | $config['maxFileSize'] = $maxFileSize; |
||||
400 | } |
||||
401 | |||||
402 | $acceptedFileTypes = $this->getAcceptedFileTypes(); |
||||
403 | if (!empty($acceptedFileTypes)) { |
||||
404 | $config['acceptedFileTypes'] = array_values($acceptedFileTypes); |
||||
405 | } |
||||
406 | |||||
407 | // image poster |
||||
408 | // @link https://pqina.nl/filepond/docs/api/plugins/file-poster/#usage |
||||
409 | if (self::config()->enable_poster) { |
||||
410 | $config['filePosterHeight'] = self::config()->poster_height ?? self::DEFAULT_POSTER_HEIGHT; |
||||
411 | } |
||||
412 | |||||
413 | // image validation/crop based on record |
||||
414 | /** @var DataObject|null $record */ |
||||
415 | $record = $this->getForm()->getRecord(); |
||||
416 | if ($record) { |
||||
0 ignored issues
–
show
|
|||||
417 | $sizes = $record->config()->image_sizes; |
||||
418 | $name = $this->getSafeName(); |
||||
419 | if ($sizes && isset($sizes[$name])) { |
||||
420 | $newConfig = $this->getImageSizeConfigFromArray($sizes[$name]); |
||||
421 | $config = array_merge($config, $newConfig); |
||||
422 | $this->adjustPosterSize($sizes[$name][0], $sizes[$name][1]); |
||||
423 | } |
||||
424 | } |
||||
425 | |||||
426 | |||||
427 | // Any custom setting will override the base ones |
||||
428 | $config = array_merge($config, $i18nConfig, $this->filePondConfig); |
||||
429 | |||||
430 | return $config; |
||||
431 | } |
||||
432 | |||||
433 | /** |
||||
434 | * Compute best size for chunks based on server settings |
||||
435 | * |
||||
436 | * @return float |
||||
437 | */ |
||||
438 | protected function computeMaxChunkSize() |
||||
439 | { |
||||
440 | $upload_max_filesize = ini_get('upload_max_filesize'); |
||||
441 | $post_max_size = ini_get('post_max_size'); |
||||
442 | |||||
443 | $upload_max_filesize = $upload_max_filesize ? $upload_max_filesize : "2MB"; |
||||
444 | $post_max_size = $post_max_size ? $post_max_size : "2MB"; |
||||
445 | |||||
446 | $maxUpload = Convert::memstring2bytes($upload_max_filesize); |
||||
447 | $maxPost = Convert::memstring2bytes($post_max_size); |
||||
448 | |||||
449 | // ~90%, allow some overhead |
||||
450 | return round(min($maxUpload, $maxPost) * 0.9); |
||||
451 | } |
||||
452 | |||||
453 | /** |
||||
454 | * @param array<mixed>|int|string $value |
||||
455 | * @param DataObject|array<string,mixed> $record |
||||
456 | * @return $this |
||||
457 | */ |
||||
458 | public function setValue($value, $record = null) |
||||
459 | { |
||||
460 | // Normalize values to something similar to UploadField usage |
||||
461 | if (is_numeric($value)) { |
||||
462 | $value = ['Files' => [$value]]; |
||||
463 | } elseif (is_array($value) && empty($value['Files'])) { |
||||
464 | // make sure we don't assign {"name":"","full_path":"","type":"","tmp_name":"","error":4,"size":0} |
||||
465 | // if $_FILES is not empty |
||||
466 | if (isset($value['tmp_name'])) { |
||||
467 | $value = null; |
||||
468 | } |
||||
469 | $value = ['Files' => $value]; |
||||
470 | } |
||||
471 | if ($record) { |
||||
472 | $name = $this->name; |
||||
473 | if ($record instanceof DataObject && $record->hasMethod($name)) { |
||||
474 | $data = $record->$name(); |
||||
475 | // Wrap |
||||
476 | if ($data instanceof DataObject) { |
||||
477 | $data = new ArrayList([$data]); |
||||
478 | } |
||||
479 | foreach ($data as $uploadedItem) { |
||||
480 | $this->trackFileID($uploadedItem->ID); |
||||
481 | } |
||||
482 | } |
||||
483 | } |
||||
484 | //@phpstan-ignore-next-line |
||||
485 | return parent::setValue($value, $record); |
||||
0 ignored issues
–
show
It seems like
$value can also be of type string ; however, parameter $value of SilverStripe\Forms\FileUploadReceiver::setValue() does only seem to accept array , 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
![]() |
|||||
486 | } |
||||
487 | |||||
488 | /** |
||||
489 | * Get the currently used form. |
||||
490 | * |
||||
491 | * @return Form|null |
||||
492 | */ |
||||
493 | public function getForm() |
||||
494 | { |
||||
495 | return $this->form; |
||||
496 | } |
||||
497 | |||||
498 | /** |
||||
499 | * Configure our endpoint |
||||
500 | * |
||||
501 | * @link https://pqina.nl/filepond/docs/patterns/api/server/ |
||||
502 | * @return array<mixed> |
||||
503 | */ |
||||
504 | public function getServerOptions() |
||||
505 | { |
||||
506 | if (!empty($this->customServerConfig)) { |
||||
507 | return $this->customServerConfig; |
||||
508 | } |
||||
509 | if (!$this->getForm()) { |
||||
510 | throw new LogicException( |
||||
511 | 'Field must be associated with a form to call getServerOptions(). Please use $field->setForm($form);' |
||||
512 | ); |
||||
513 | } |
||||
514 | $endpoint = $this->getChunkUploads() ? 'chunk' : 'upload'; |
||||
515 | $server = [ |
||||
516 | 'process' => $this->getUploadEnabled() ? $this->getLinkParameters($endpoint) : null, |
||||
517 | 'fetch' => null, |
||||
518 | 'revert' => $this->getUploadEnabled() ? $this->getLinkParameters('revert') : null, |
||||
519 | ]; |
||||
520 | if ($this->getUploadEnabled() && $this->getChunkUploads()) { |
||||
521 | $server['fetch'] = $this->getLinkParameters($endpoint . "?fetch="); |
||||
522 | $server['patch'] = $this->getLinkParameters($endpoint . "?patch="); |
||||
523 | } |
||||
524 | return $server; |
||||
525 | } |
||||
526 | |||||
527 | /** |
||||
528 | * Configure the following parameters: |
||||
529 | * |
||||
530 | * url : Path to the end point |
||||
531 | * method : Request method to use |
||||
532 | * withCredentials : Toggles the XMLHttpRequest withCredentials on or off |
||||
533 | * headers : An object containing additional headers to send |
||||
534 | * timeout : Timeout for this action |
||||
535 | * onload : Called when server response is received, useful for getting the unique file id from the server response |
||||
536 | * onerror : Called when server error is received, receis the response body, useful to select the relevant error data |
||||
537 | * |
||||
538 | * @param string $action |
||||
539 | * @return array<mixed> |
||||
540 | */ |
||||
541 | protected function getLinkParameters($action) |
||||
542 | { |
||||
543 | $form = $this->getForm(); |
||||
544 | $token = $form->getSecurityToken()->getValue(); |
||||
545 | /** @var DataObject|null $record */ |
||||
546 | $record = $form->getRecord(); |
||||
547 | |||||
548 | $headers = [ |
||||
549 | 'X-SecurityID' => $token |
||||
550 | ]; |
||||
551 | // Allow us to track the record instance |
||||
552 | if ($record) { |
||||
0 ignored issues
–
show
|
|||||
553 | $headers['X-RecordClassName'] = get_class($record); |
||||
554 | $headers['X-RecordID'] = $record->ID; |
||||
555 | } |
||||
556 | return [ |
||||
557 | 'url' => $this->Link($action), |
||||
558 | 'headers' => $headers, |
||||
559 | ]; |
||||
560 | } |
||||
561 | |||||
562 | /** |
||||
563 | * The maximum size of a file, for instance 5MB or 750KB |
||||
564 | * Suitable for JS usage |
||||
565 | * |
||||
566 | * @return string |
||||
567 | */ |
||||
568 | public function getMaxFileSize() |
||||
569 | { |
||||
570 | $size = $this->getValidator()->getAllowedMaxFileSize(); |
||||
571 | if (!$size) { |
||||
572 | return ''; |
||||
573 | } |
||||
574 | |||||
575 | // Only supports KB and MB |
||||
576 | if ($size < 1024 * 1024) { |
||||
577 | $size = round($size / 1024) . ' KB'; |
||||
578 | } else { |
||||
579 | $size = round($size / (1024 * 1024)) . ' MB'; |
||||
580 | } |
||||
581 | |||||
582 | return str_replace(' ', '', $size); |
||||
583 | } |
||||
584 | |||||
585 | /** |
||||
586 | * Set initial values to FilePondField |
||||
587 | * See: https://pqina.nl/filepond/docs/patterns/api/filepond-object/#setting-initial-files |
||||
588 | * |
||||
589 | * @return array<mixed> |
||||
590 | */ |
||||
591 | public function getExistingUploadsData() |
||||
592 | { |
||||
593 | // Both Value() & dataValue() seem to return an array eg: ['Files' => [258, 259, 257]] |
||||
594 | $fileIDarray = $this->Value() ?: ['Files' => []]; |
||||
595 | if (!isset($fileIDarray['Files']) || !count($fileIDarray['Files'])) { |
||||
596 | return []; |
||||
597 | } |
||||
598 | |||||
599 | $existingUploads = []; |
||||
600 | foreach ($fileIDarray['Files'] as $fileID) { |
||||
601 | /** @var File|null $file */ |
||||
602 | $file = File::get()->byID($fileID); |
||||
603 | if (!$file) { |
||||
604 | continue; |
||||
605 | } |
||||
606 | $existingUpload = [ |
||||
607 | // the server file reference |
||||
608 | 'source' => (int) $fileID, |
||||
609 | // set type to local to indicate an already uploaded file |
||||
610 | 'options' => [ |
||||
611 | 'type' => 'local', |
||||
612 | // file information |
||||
613 | 'file' => [ |
||||
614 | 'name' => $file->Name, |
||||
615 | 'size' => (int) $file->getAbsoluteSize(), |
||||
616 | 'type' => $file->getMimeType(), |
||||
617 | ], |
||||
618 | ], |
||||
619 | 'metadata' => [] |
||||
620 | ]; |
||||
621 | |||||
622 | // Show poster |
||||
623 | // @link https://pqina.nl/filepond/docs/api/plugins/file-poster/#usage |
||||
624 | if (self::config()->enable_poster && $file instanceof Image && $file->ID) { |
||||
625 | // Size matches the one from asset admin or from or set size |
||||
626 | $w = self::getDefaultPosterWidth(); |
||||
627 | if ($this->posterWidth) { |
||||
0 ignored issues
–
show
The expression
$this->posterWidth of type integer|null is loosely compared to true ; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.
In PHP, under loose comparison (like For 0 == false // true
0 == null // true
123 == false // false
123 == null // false
// It is often better to use strict comparison
0 === false // false
0 === null // false
![]() |
|||||
628 | $w = $this->posterWidth; |
||||
629 | } |
||||
630 | $h = self::getDefaultPosterHeight(); |
||||
631 | if ($this->posterHeight) { |
||||
0 ignored issues
–
show
The expression
$this->posterHeight of type integer|null is loosely compared to true ; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.
In PHP, under loose comparison (like For 0 == false // true
0 == null // true
123 == false // false
123 == null // false
// It is often better to use strict comparison
0 === false // false
0 === null // false
![]() |
|||||
632 | $h = $this->posterHeight; |
||||
633 | } |
||||
634 | /** @var Image|null $resizedImage */ |
||||
635 | $resizedImage = $file->Fill($w, $h); |
||||
636 | if ($resizedImage) { |
||||
637 | $poster = $resizedImage->getAbsoluteURL(); |
||||
638 | $existingUpload['options']['metadata']['poster'] = $poster; |
||||
639 | } |
||||
640 | } |
||||
641 | $existingUploads[] = $existingUpload; |
||||
642 | } |
||||
643 | return $existingUploads; |
||||
644 | } |
||||
645 | |||||
646 | /** |
||||
647 | * @return int |
||||
648 | */ |
||||
649 | public static function getDefaultPosterWidth() |
||||
650 | { |
||||
651 | return self::config()->poster_width ?? self::DEFAULT_POSTER_WIDTH; |
||||
652 | } |
||||
653 | |||||
654 | /** |
||||
655 | * @return int |
||||
656 | */ |
||||
657 | public static function getDefaultPosterHeight() |
||||
658 | { |
||||
659 | return self::config()->poster_height ?? self::DEFAULT_POSTER_HEIGHT; |
||||
660 | } |
||||
661 | |||||
662 | /** |
||||
663 | * @return void |
||||
664 | */ |
||||
665 | public static function Requirements() |
||||
666 | { |
||||
667 | // It includes css styles already |
||||
668 | Requirements::javascript('lekoala/silverstripe-filepond: javascript/filepond-input.min.js'); |
||||
669 | } |
||||
670 | |||||
671 | public function getAttributes() |
||||
672 | { |
||||
673 | // don't use parent as it will include data-schema that we don'tt need |
||||
674 | $attributes = array( |
||||
675 | 'class' => $this->extraClass(), |
||||
676 | 'type' => 'file', |
||||
677 | 'multiple' => $this->getIsMultiUpload(), |
||||
678 | 'id' => $this->ID(), |
||||
679 | ); |
||||
680 | |||||
681 | $attributes = array_merge($attributes, $this->attributes); |
||||
682 | |||||
683 | $this->fixName(); |
||||
684 | $attributes['name'] = $this->getName(); |
||||
685 | |||||
686 | $this->extend('updateAttributes', $attributes); |
||||
687 | |||||
688 | return $attributes; |
||||
689 | } |
||||
690 | |||||
691 | /** |
||||
692 | * Make sure the name is correct |
||||
693 | * @return void |
||||
694 | */ |
||||
695 | protected function fixName() |
||||
696 | { |
||||
697 | $name = $this->getName(); |
||||
698 | $multiple = $this->getIsMultiUpload(); |
||||
699 | |||||
700 | // Multi uploads need [] |
||||
701 | if ($multiple && strpos($name, '[]') === false) { |
||||
702 | $name .= '[]'; |
||||
703 | $this->setName($name); |
||||
704 | } |
||||
705 | } |
||||
706 | |||||
707 | /** |
||||
708 | * @param array<string,mixed> $properties |
||||
709 | * @return DBHTMLText |
||||
710 | */ |
||||
711 | public function FieldHolder($properties = array()) |
||||
712 | { |
||||
713 | if (self::config()->enable_requirements) { |
||||
714 | self::Requirements(); |
||||
715 | } |
||||
716 | return parent::FieldHolder($properties); |
||||
717 | } |
||||
718 | |||||
719 | /** |
||||
720 | * @param array<mixed> $properties |
||||
721 | * @return DBHTMLText|string |
||||
722 | */ |
||||
723 | public function Field($properties = array()) |
||||
724 | { |
||||
725 | $html = parent::Field($properties); |
||||
726 | |||||
727 | $config = $this->getFilePondConfig(); |
||||
728 | |||||
729 | // Simply wrap with custom element and set config |
||||
730 | $html = "<filepond-input data-config='" . json_encode($config) . "'>" . $html . '</filepond-input>'; |
||||
731 | |||||
732 | return $html; |
||||
733 | } |
||||
734 | |||||
735 | /** |
||||
736 | * Check the incoming request |
||||
737 | * |
||||
738 | * @param HTTPRequest $request |
||||
739 | * @return array<mixed> |
||||
740 | */ |
||||
741 | public function prepareUpload(HTTPRequest $request) |
||||
742 | { |
||||
743 | $name = $this->getName(); |
||||
744 | $tmpFile = $request->postVar($name); |
||||
745 | if (!$tmpFile) { |
||||
746 | throw new RuntimeException("No file"); |
||||
747 | } |
||||
748 | $tmpFile = $this->normalizeTempFile($tmpFile); |
||||
749 | |||||
750 | // Update $tmpFile with a better name |
||||
751 | if ($this->renamePattern) { |
||||
752 | $tmpFile['name'] = $this->changeFilenameWithPattern( |
||||
753 | $tmpFile['name'], |
||||
754 | $this->renamePattern |
||||
755 | ); |
||||
756 | } |
||||
757 | |||||
758 | return $tmpFile; |
||||
759 | } |
||||
760 | |||||
761 | /** |
||||
762 | * @param HTTPRequest $request |
||||
763 | * @return void |
||||
764 | */ |
||||
765 | protected function securityChecks(HTTPRequest $request) |
||||
766 | { |
||||
767 | if ($this->isDisabled() || $this->isReadonly()) { |
||||
768 | throw new RuntimeException("Field is disabled or readonly"); |
||||
769 | } |
||||
770 | |||||
771 | // CSRF check |
||||
772 | $token = $this->getForm()->getSecurityToken(); |
||||
773 | if (!$token->checkRequest($request)) { |
||||
774 | throw new RuntimeException("Invalid token"); |
||||
775 | } |
||||
776 | } |
||||
777 | |||||
778 | /** |
||||
779 | * @param File $file |
||||
780 | * @param HTTPRequest $request |
||||
781 | * @return void |
||||
782 | */ |
||||
783 | protected function setFileDetails(File $file, HTTPRequest $request) |
||||
784 | { |
||||
785 | // Mark as temporary until properly associated with a record |
||||
786 | // Files will be unmarked later on by saveInto method |
||||
787 | $file->IsTemporary = true; //@phpstan-ignore-line |
||||
788 | |||||
789 | // We can also track the record |
||||
790 | $RecordID = $request->getHeader('X-RecordID'); |
||||
791 | $RecordClassName = $request->getHeader('X-RecordClassName'); |
||||
792 | if (!$file->ObjectID) { //@phpstan-ignore-line |
||||
793 | $file->ObjectID = $RecordID; |
||||
794 | } |
||||
795 | if (!$file->ObjectClass) { //@phpstan-ignore-line |
||||
796 | $file->ObjectClass = $RecordClassName; |
||||
797 | } |
||||
798 | |||||
799 | if ($file->isChanged()) { |
||||
800 | // If possible, prevent creating a version for no reason |
||||
801 | // @link https://docs.silverstripe.org/en/4/developer_guides/model/versioning/#writing-changes-to-a-versioned-dataobject |
||||
802 | if ($file->hasExtension(Versioned::class)) { |
||||
803 | $file->writeWithoutVersion(); |
||||
804 | } else { |
||||
805 | $file->write(); |
||||
806 | } |
||||
807 | } |
||||
808 | } |
||||
809 | |||||
810 | /** |
||||
811 | * Creates a single file based on a form-urlencoded upload. |
||||
812 | * |
||||
813 | * 1 client uploads file my-file.jpg as multipart/form-data using a POST request |
||||
814 | * 2 server saves file to unique location tmp/12345/my-file.jpg |
||||
815 | * 3 server returns unique location id 12345 in text/plain response |
||||
816 | * 4 client stores unique id 12345 in a hidden input field |
||||
817 | * 5 client submits the FilePond parent form containing the hidden input field with the unique id |
||||
818 | * 6 server uses the unique id to move tmp/12345/my-file.jpg to its final location and remove the tmp/12345 folder |
||||
819 | * |
||||
820 | * Along with the file object, FilePond also sends the file metadata to the server, both these objects are given the same name. |
||||
821 | * |
||||
822 | * @param HTTPRequest $request |
||||
823 | * @return HTTPResponse |
||||
824 | */ |
||||
825 | public function upload(HTTPRequest $request) |
||||
826 | { |
||||
827 | try { |
||||
828 | $this->securityChecks($request); |
||||
829 | $tmpFile = $this->prepareUpload($request); |
||||
830 | } catch (Exception $ex) { |
||||
831 | return $this->httpError(400, $ex->getMessage()); |
||||
832 | } |
||||
833 | |||||
834 | $file = $this->saveTemporaryFile($tmpFile, $error); |
||||
835 | |||||
836 | // Handle upload errors |
||||
837 | if ($error) { |
||||
838 | $this->getUpload()->clearErrors(); |
||||
839 | $jsonError = json_encode($error); |
||||
840 | $jsonError = $jsonError ? $jsonError : json_last_error_msg(); |
||||
841 | return $this->httpError(400, $jsonError); |
||||
842 | } |
||||
843 | |||||
844 | // File can be an AssetContainer and not a DataObject |
||||
845 | if ($file instanceof DataObject) { |
||||
846 | $this->setFileDetails($file, $request); //@phpstan-ignore-line |
||||
847 | } |
||||
848 | |||||
849 | $this->getUpload()->clearErrors(); |
||||
850 | $fileId = $file->ID; //@phpstan-ignore-line |
||||
0 ignored issues
–
show
|
|||||
851 | $this->trackFileID($fileId); |
||||
852 | |||||
853 | if (self::config()->auto_clear_temp_folder) { |
||||
854 | // Set a limit of 100 because otherwise it would be really slow |
||||
855 | self::clearTemporaryUploads(true, 100); |
||||
856 | } |
||||
857 | |||||
858 | // server returns unique location id 12345 in text/plain response |
||||
859 | $response = new HTTPResponse($fileId); |
||||
860 | $response->addHeader('Content-Type', 'text/plain'); |
||||
861 | return $response; |
||||
862 | } |
||||
863 | |||||
864 | /** |
||||
865 | * @link https://pqina.nl/filepond/docs/api/server/#process-chunks |
||||
866 | * @param HTTPRequest $request |
||||
867 | * @return HTTPResponse |
||||
868 | */ |
||||
869 | public function chunk(HTTPRequest $request) |
||||
870 | { |
||||
871 | try { |
||||
872 | $this->securityChecks($request); |
||||
873 | } catch (Exception $ex) { |
||||
874 | return $this->httpError(400, $ex->getMessage()); |
||||
875 | } |
||||
876 | |||||
877 | $method = $request->httpMethod(); |
||||
878 | |||||
879 | // The random token is returned as a query string |
||||
880 | $id = $request->getVar('patch'); |
||||
881 | |||||
882 | // FilePond will send a POST request (without file) to start a chunked transfer, |
||||
883 | // expecting to receive a unique transfer id in the response body, it'll add the Upload-Length header to this request. |
||||
884 | if ($method == "POST") { |
||||
885 | // Initial post payload doesn't contain name |
||||
886 | // It would be better to return some kind of random token instead |
||||
887 | // But FilePond stores the id upon the first request :-( |
||||
888 | $file = new File(); |
||||
889 | $this->setFileDetails($file, $request); |
||||
890 | $fileId = $file->ID; |
||||
891 | $this->trackFileID($fileId); |
||||
892 | $response = new HTTPResponse((string)$fileId, 200); |
||||
893 | $response->addHeader('Content-Type', 'text/plain'); |
||||
894 | return $response; |
||||
895 | } |
||||
896 | |||||
897 | // location of patch files |
||||
898 | $filePath = TEMP_PATH . "/filepond-" . $id; |
||||
899 | |||||
900 | // FilePond will send a HEAD request to determine which chunks have already been uploaded, |
||||
901 | // expecting the file offset of the next expected chunk in the Upload-Offset response header. |
||||
902 | if ($method == "HEAD") { |
||||
903 | $nextOffset = 0; |
||||
904 | while (is_file($filePath . '.patch.' . $nextOffset)) { |
||||
905 | $nextOffset++; |
||||
906 | } |
||||
907 | |||||
908 | $response = new HTTPResponse((string)$nextOffset, 200); |
||||
909 | $response->addHeader('Content-Type', 'text/plain'); |
||||
910 | $response->addHeader('Upload-Offset', (string)$nextOffset); |
||||
911 | return $response; |
||||
912 | } |
||||
913 | |||||
914 | // FilePond will send a PATCH request to push a chunk to the server. |
||||
915 | // Each of these requests is accompanied by a Content-Type, Upload-Offset, Upload-Name, and Upload-Length header. |
||||
916 | if ($method != "PATCH") { |
||||
917 | return $this->httpError(400, "Invalid method"); |
||||
918 | } |
||||
919 | |||||
920 | // The name of the file being transferred |
||||
921 | $uploadName = $request->getHeader('Upload-Name'); |
||||
922 | // The offset of the chunk being transferred (starts with 0) |
||||
923 | $offset = $request->getHeader('Upload-Offset'); |
||||
924 | // The total size of the file being transferred (in bytes) |
||||
925 | $length = (int) $request->getHeader('Upload-Length'); |
||||
926 | |||||
927 | // should be numeric values, else exit |
||||
928 | if (!is_numeric($offset) || !is_numeric($length)) { |
||||
929 | return $this->httpError(400, "Invalid offset or length"); |
||||
930 | } |
||||
931 | |||||
932 | // write patch file for this request |
||||
933 | file_put_contents($filePath . '.patch.' . $offset, $request->getBody()); |
||||
934 | |||||
935 | // calculate total size of patches |
||||
936 | $size = 0; |
||||
937 | $patch = glob($filePath . '.patch.*'); |
||||
938 | if ($patch) { |
||||
0 ignored issues
–
show
The expression
$patch of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent. Consider making the comparison explicit by using ![]() |
|||||
939 | foreach ($patch as $filename) { |
||||
940 | $size += filesize($filename); |
||||
941 | } |
||||
942 | } |
||||
943 | |||||
944 | // check if we are above our size limit |
||||
945 | $maxAllowedSize = $this->getValidator()->getAllowedMaxFileSize(); |
||||
946 | if ($maxAllowedSize && $size > $maxAllowedSize) { |
||||
947 | return $this->httpError(400, "File must not be larger than " . $this->getMaxFileSize()); |
||||
948 | } |
||||
949 | |||||
950 | // if total size equals length of file we have gathered all patch files |
||||
951 | if ($size >= $length) { |
||||
952 | // create output file |
||||
953 | $outputFile = fopen($filePath, 'wb'); |
||||
954 | if ($patch && $outputFile) { |
||||
0 ignored issues
–
show
The expression
$patch of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent. Consider making the comparison explicit by using ![]() |
|||||
955 | // write patches to file |
||||
956 | foreach ($patch as $filename) { |
||||
957 | // get offset from filename |
||||
958 | list($dir, $offset) = explode('.patch.', $filename, 2); |
||||
959 | // read patch and close |
||||
960 | $patchFile = fopen($filename, 'rb'); |
||||
961 | $patchFileSize = filesize($filename); |
||||
962 | if ($patchFile && $patchFileSize) { |
||||
963 | $patchContent = fread($patchFile, $patchFileSize); |
||||
964 | if ($patchContent) { |
||||
965 | fclose($patchFile); |
||||
966 | |||||
967 | // apply patch |
||||
968 | fseek($outputFile, (int) $offset); |
||||
969 | fwrite($outputFile, $patchContent); |
||||
970 | } |
||||
971 | } |
||||
972 | } |
||||
973 | // remove patches |
||||
974 | foreach ($patch as $filename) { |
||||
975 | unlink($filename); |
||||
976 | } |
||||
977 | // done with file |
||||
978 | fclose($outputFile); |
||||
979 | } |
||||
980 | |||||
981 | // Finalize real filename |
||||
982 | |||||
983 | // We need to class this as it mutates the state and set the record if any |
||||
984 | $relationClass = $this->getRelationAutosetClass(File::class); |
||||
0 ignored issues
–
show
|
|||||
985 | $realFilename = $this->getFolderName() . "/" . $uploadName; |
||||
986 | if ($this->renamePattern) { |
||||
987 | $realFilename = $this->changeFilenameWithPattern( |
||||
988 | $realFilename, |
||||
989 | $this->renamePattern |
||||
990 | ); |
||||
991 | } |
||||
992 | |||||
993 | // write output file to asset store |
||||
994 | $file = $this->getFileByID($id); |
||||
995 | if (!$file) { |
||||
996 | return $this->httpError(400, "File $id not found"); |
||||
997 | } |
||||
998 | $file->setFromLocalFile($filePath); |
||||
999 | $file->setFilename($realFilename); |
||||
1000 | $file->Title = $uploadName; |
||||
1001 | // Set proper class |
||||
1002 | $relationClass = File::get_class_for_file_extension( |
||||
1003 | File::get_file_extension($realFilename) |
||||
1004 | ); |
||||
1005 | $file->setClassName($relationClass); |
||||
1006 | $file->write(); |
||||
1007 | // Reload file instance to get the right class |
||||
1008 | // it is not cached so we should get a fresh record |
||||
1009 | $file = $this->getFileByID($id); |
||||
1010 | // since we don't go through our upload object, call extension manually |
||||
1011 | $file->extend('onAfterUpload'); |
||||
1012 | |||||
1013 | // Cleanup temp files |
||||
1014 | $patch = glob($filePath . '.patch.*'); |
||||
1015 | if ($patch) { |
||||
0 ignored issues
–
show
The expression
$patch of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent. Consider making the comparison explicit by using ![]() |
|||||
1016 | foreach ($patch as $filename) { |
||||
1017 | unlink($filename); |
||||
1018 | } |
||||
1019 | } |
||||
1020 | } |
||||
1021 | $response = new HTTPResponse('', 204); |
||||
1022 | return $response; |
||||
1023 | } |
||||
1024 | |||||
1025 | /** |
||||
1026 | * @link https://pqina.nl/filepond/docs/api/server/#revert |
||||
1027 | * @param HTTPRequest $request |
||||
1028 | * @return HTTPResponse |
||||
1029 | */ |
||||
1030 | public function revert(HTTPRequest $request) |
||||
1031 | { |
||||
1032 | try { |
||||
1033 | $this->securityChecks($request); |
||||
1034 | } catch (Exception $ex) { |
||||
1035 | return $this->httpError(400, $ex->getMessage()); |
||||
1036 | } |
||||
1037 | |||||
1038 | $method = $request->httpMethod(); |
||||
1039 | |||||
1040 | if ($method != "DELETE") { |
||||
1041 | return $this->httpError(400, "Invalid method"); |
||||
1042 | } |
||||
1043 | |||||
1044 | $fileID = (int) $request->getBody(); |
||||
1045 | if (!in_array($fileID, $this->getTrackedIDs())) { |
||||
1046 | return $this->httpError(400, "Invalid ID"); |
||||
1047 | } |
||||
1048 | $file = File::get()->byID($fileID); |
||||
1049 | if (!$file->IsTemporary) { |
||||
1050 | return $this->httpError(400, "Invalid file"); |
||||
1051 | } |
||||
1052 | if (!$file->canDelete()) { |
||||
1053 | return $this->httpError(400, "Cannot delete file"); |
||||
1054 | } |
||||
1055 | $file->delete(); |
||||
1056 | $response = new HTTPResponse('', 200); |
||||
1057 | return $response; |
||||
1058 | } |
||||
1059 | |||||
1060 | /** |
||||
1061 | * Clear temp folder that should not contain any file other than temporary |
||||
1062 | * |
||||
1063 | * @param boolean $doDelete Set to true to actually delete the files, otherwise it's just a dry-run |
||||
1064 | * @param int $limit |
||||
1065 | * @return File[] List of files removed |
||||
1066 | */ |
||||
1067 | public static function clearTemporaryUploads($doDelete = false, $limit = 0) |
||||
1068 | { |
||||
1069 | $tempFiles = File::get()->filter('IsTemporary', true); |
||||
1070 | if ($limit) { |
||||
1071 | $tempFiles = $tempFiles->limit($limit); |
||||
1072 | } |
||||
1073 | |||||
1074 | $threshold = self::config()->auto_clear_threshold; |
||||
1075 | |||||
1076 | // Set a default threshold if none set |
||||
1077 | if (!$threshold) { |
||||
1078 | if (Director::isDev()) { |
||||
1079 | $threshold = '-10 minutes'; |
||||
1080 | } else { |
||||
1081 | $threshold = '-1 day'; |
||||
1082 | } |
||||
1083 | } |
||||
1084 | if (is_int($threshold)) { |
||||
1085 | $thresholdTime = time() - $threshold; |
||||
1086 | } else { |
||||
1087 | $thresholdTime = strtotime($threshold); |
||||
1088 | } |
||||
1089 | |||||
1090 | // Update query to avoid fetching unecessary records |
||||
1091 | $tempFiles = $tempFiles->filter("Created:LessThan", date('Y-m-d H:i:s', $thresholdTime)); |
||||
1092 | |||||
1093 | $filesDeleted = []; |
||||
1094 | foreach ($tempFiles as $tempFile) { |
||||
1095 | $createdTime = strtotime($tempFile->Created); |
||||
1096 | if ($createdTime < $thresholdTime) { |
||||
1097 | $filesDeleted[] = $tempFile; |
||||
1098 | if ($doDelete) { |
||||
1099 | if ($tempFile->hasExtension(Versioned::class)) { |
||||
1100 | $tempFile->deleteFromStage(Versioned::LIVE); |
||||
1101 | $tempFile->deleteFromStage(Versioned::DRAFT); |
||||
1102 | } else { |
||||
1103 | $tempFile->delete(); |
||||
1104 | } |
||||
1105 | } |
||||
1106 | } |
||||
1107 | } |
||||
1108 | return $filesDeleted; |
||||
1109 | } |
||||
1110 | |||||
1111 | /** |
||||
1112 | * Allows tracking uploaded ids to prevent unauthorized attachements |
||||
1113 | * |
||||
1114 | * @param int|string $fileId |
||||
1115 | * @return void |
||||
1116 | */ |
||||
1117 | public function trackFileID($fileId) |
||||
1118 | { |
||||
1119 | $fileId = is_string($fileId) ? intval($fileId) : $fileId; |
||||
1120 | $session = $this->getRequest()->getSession(); |
||||
1121 | $uploadedIDs = $this->getTrackedIDs(); |
||||
1122 | if (!in_array($fileId, $uploadedIDs)) { |
||||
1123 | $uploadedIDs[] = $fileId; |
||||
1124 | } |
||||
1125 | $session->set('FilePond', $uploadedIDs); |
||||
1126 | } |
||||
1127 | |||||
1128 | /** |
||||
1129 | * Get all authorized tracked ids |
||||
1130 | * @return array<mixed> |
||||
1131 | */ |
||||
1132 | public function getTrackedIDs() |
||||
1133 | { |
||||
1134 | $session = $this->getRequest()->getSession(); |
||||
1135 | $uploadedIDs = $session->get('FilePond'); |
||||
1136 | if ($uploadedIDs) { |
||||
1137 | return $uploadedIDs; |
||||
1138 | } |
||||
1139 | return []; |
||||
1140 | } |
||||
1141 | |||||
1142 | public function saveInto(DataObjectInterface $record) |
||||
1143 | { |
||||
1144 | // Note that the list of IDs is based on the value sent by the user |
||||
1145 | // It can be spoofed because checks are minimal (by default, canView = true and only check if isInDB) |
||||
1146 | $IDs = $this->getItemIDs(); |
||||
1147 | |||||
1148 | $Member = Security::getCurrentUser(); |
||||
1149 | |||||
1150 | // Ensure the files saved into the DataObject have been tracked (either because already on the DataObject or uploaded by the user) |
||||
1151 | $trackedIDs = $this->getTrackedIDs(); |
||||
1152 | foreach ($IDs as $ID) { |
||||
1153 | if (!in_array($ID, $trackedIDs)) { |
||||
1154 | throw new ValidationException("Invalid file ID : $ID"); |
||||
1155 | } |
||||
1156 | } |
||||
1157 | |||||
1158 | // Move files out of temporary folder |
||||
1159 | foreach ($IDs as $ID) { |
||||
1160 | $file = $this->getFileByID($ID); |
||||
1161 | //@phpstan-ignore-next-line |
||||
1162 | if ($file && $file->IsTemporary) { |
||||
1163 | // The record does not have an ID which is a bad idea to attach the file to it |
||||
1164 | if (!$record->ID) { |
||||
1165 | $record->write(); |
||||
1166 | } |
||||
1167 | // Check if the member is owner |
||||
1168 | if ($Member && $Member->ID != $file->OwnerID) { |
||||
1169 | throw new ValidationException("Failed to authenticate owner"); |
||||
1170 | } |
||||
1171 | $file->IsTemporary = false; |
||||
1172 | $file->ObjectID = $record->ID; //@phpstan-ignore-line |
||||
1173 | $file->ObjectClass = get_class($record); //@phpstan-ignore-line |
||||
1174 | $file->write(); |
||||
1175 | } else { |
||||
1176 | // File was uploaded earlier, no need to do anything |
||||
1177 | } |
||||
1178 | } |
||||
1179 | |||||
1180 | // Proceed |
||||
1181 | parent::saveInto($record); |
||||
1182 | |||||
1183 | return $this; |
||||
1184 | } |
||||
1185 | |||||
1186 | /** |
||||
1187 | * @return string |
||||
1188 | */ |
||||
1189 | public function Type() |
||||
1190 | { |
||||
1191 | return 'filepond'; |
||||
1192 | } |
||||
1193 | } |
||||
1194 |