Passed
Push — master ( fbfe31...68937e )
by Thomas
02:42
created

FilePondField::clearTemporaryUploads()   B

Complexity

Conditions 9
Paths 60

Size

Total Lines 42
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
eloc 26
c 1
b 0
f 0
nc 60
nop 2
dl 0
loc 42
rs 8.0555
1
<?php
2
3
namespace LeKoala\FilePond;
4
5
use Exception;
6
use LogicException;
7
use RuntimeException;
8
use SilverStripe\Assets\File;
9
use SilverStripe\ORM\SS_List;
10
use SilverStripe\ORM\ArrayList;
11
use SilverStripe\ORM\DataObject;
12
use SilverStripe\Security\Member;
13
use SilverStripe\Control\Director;
14
use SilverStripe\Security\Security;
15
use SilverStripe\View\Requirements;
16
use SilverStripe\Control\HTTPRequest;
17
use SilverStripe\Versioned\Versioned;
18
use SilverStripe\Control\HTTPResponse;
19
use SilverStripe\ORM\DataObjectInterface;
20
use SilverStripe\ORM\ValidationException;
21
22
/**
23
 * A FilePond field
24
 */
25
class FilePondField extends AbstractUploadField
26
{
27
28
    /**
29
     * @config
30
     * @var array
31
     */
32
    private static $allowed_actions = [
0 ignored issues
show
introduced by
The private property $allowed_actions is not used, and could be removed.
Loading history...
33
        'upload',
34
        'chunk',
35
    ];
36
37
    /**
38
     * @config
39
     * @var boolean
40
     */
41
    private static $enable_requirements = true;
42
43
    /**
44
     * @config
45
     * @var boolean
46
     */
47
    private static $enable_validation = true;
48
49
    /**
50
     * @config
51
     * @var boolean
52
     */
53
    private static $enable_poster = false;
54
55
    /**
56
     * @config
57
     * @var boolean
58
     */
59
    private static $enable_image = false;
60
61
    /**
62
     * @config
63
     * @var boolean
64
     */
65
    private static $enable_polyfill = true;
66
67
    /**
68
     * @config
69
     * @var boolean
70
     */
71
    private static $enable_ajax_init = true;
72
73
    /**
74
     * @config
75
     * @var boolean
76
     */
77
    private static $chunk_by_default = false;
78
79
    /**
80
     * @config
81
     * @var boolean
82
     */
83
    private static $enable_default_description = true;
0 ignored issues
show
introduced by
The private property $enable_default_description is not used, and could be removed.
Loading history...
84
85
    /**
86
     * @config
87
     * @var boolean
88
     */
89
    private static $auto_clear_temp_folder = true;
90
91
    /**
92
     * @config
93
     * @var int
94
     */
95
    private static $auto_clear_threshold = true;
96
97
    /**
98
     * @var array
99
     */
100
    protected $filePondConfig = [];
101
102
    /**
103
     * @var bool
104
     */
105
    protected $chunkUploads = false;
106
107
    /**
108
     * Create a new file field.
109
     *
110
     * @param string $name The internal field name, passed to forms.
111
     * @param string $title The field label.
112
     * @param SS_List $items Items assigned to this field
113
     */
114
    public function __construct($name, $title = null, SS_List $items = null)
115
    {
116
        parent::__construct($name, $title, $items);
117
118
        if (self::config()->chunk_by_default) {
119
            $this->setChunkUploads(true);
120
        }
121
    }
122
123
    /**
124
     * Set a custom config value for this field
125
     *
126
     * @link https://pqina.nl/filepond/docs/patterns/api/filepond-instance/#properties
127
     * @param string $k
128
     * @param string $v
129
     * @return $this
130
     */
131
    public function addFilePondConfig($k, $v)
132
    {
133
        $this->filePondConfig[$k] = $v;
134
        return $this;
135
    }
136
137
    /**
138
     * Custom configuration applied to this field
139
     *
140
     * @return array
141
     */
142
    public function getCustomFilePondConfig()
143
    {
144
        return $this->filePondConfig;
145
    }
146
    /**
147
     * Get the value of chunkUploads
148
     * @return bool
149
     */
150
    public function getChunkUploads()
151
    {
152
        return $this->chunkUploads;
153
    }
154
155
    /**
156
     * Set the value of chunkUploads
157
     *
158
     * @param bool $chunkUploads
159
     * @return $this
160
     */
161
    public function setChunkUploads($chunkUploads)
162
    {
163
        $this->chunkUploads = $chunkUploads;
164
        return $this;
165
    }
166
167
    /**
168
     * Return the config applied for this field
169
     *
170
     * Typically converted to json and set in a data attribute
171
     *
172
     * @return array
173
     */
174
    public function getFilePondConfig()
175
    {
176
        $name = $this->getName();
177
        $multiple = $this->getIsMultiUpload();
178
179
        // Multi uploads need []
180
        if ($multiple && strpos($name, '[]') === false) {
181
            $name .= '[]';
182
            $this->setName($name);
183
        }
184
185
        $i18nConfig = [
186
            'labelIdle' => _t('FilePondField.labelIdle', 'Drag & Drop your files or <span class="filepond--label-action"> Browse </span>'),
187
            'labelFileProcessing' => _t('FilePondField.labelFileProcessing', 'Uploading'),
188
            'labelFileProcessingComplete' => _t('FilePondField.labelFileProcessingComplete', 'Upload complete'),
189
            'labelFileProcessingAborted' => _t('FilePondField.labelFileProcessingAborted', 'Upload cancelled'),
190
            'labelTapToCancel' => _t('FilePondField.labelTapToCancel', 'tap to cancel'),
191
            'labelTapToRetry' => _t('FilePondField.labelTapToCancel', 'tap to retry'),
192
            'labelTapToUndo' => _t('FilePondField.labelTapToCancel', 'tap to undo'),
193
        ];
194
        $config = [
195
            'name' => $name, // This will also apply to the hidden fields
196
            'allowMultiple' => $multiple,
197
            'maxFiles' => $this->getAllowedMaxFileNumber(),
198
            'maxFileSize' => $this->getMaxFileSize(),
199
            'server' => $this->getServerOptions(),
200
            'files' => $this->getExistingUploadsData(),
201
        ];
202
        if ($this->chunkUploads) {
203
            $config['chunkUploads'] = true;
204
            $config['chunkForce'] = true;
205
        }
206
207
        $acceptedFileTypes = $this->getAcceptedFileTypes();
208
        if (!empty($acceptedFileTypes)) {
209
            $config['acceptedFileTypes'] = array_values($acceptedFileTypes);
210
        }
211
212
        // image validation
213
        $record = $this->getForm()->getRecord();
214
        if ($record) {
0 ignored issues
show
introduced by
$record is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
215
            $sizes = $record->config()->image_sizes;
216
            $name = $this->getSafeName();
217
            if ($sizes && isset($sizes[$name])) {
218
                if (isset($sizes[$name][2]) && $sizes[$name][2] == 'max') {
219
                    $config['imageValidateSizeMaxWidth'] = $sizes[$name][0];
220
                    $config['imageValidateSizeMaxHeight'] = $sizes[$name][1];
221
                } else {
222
                    $config['imageValidateSizeMinWidth'] = $sizes[$name][0];
223
                    $config['imageValidateSizeMinHeight'] = $sizes[$name][1];
224
                }
225
            }
226
        }
227
228
        $config = array_merge($config, $i18nConfig, $this->filePondConfig);
229
230
        return $config;
231
    }
232
233
    /**
234
     * @inheritDoc
235
     */
236
    public function setValue($value, $record = null)
237
    {
238
        // Normalize values to something similar to UploadField usage
239
        if (is_numeric($value)) {
240
            $value = ['Files' => [$value]];
241
        } elseif (is_array($value) && empty($value['Files'])) {
242
            $value = ['Files' => $value];
243
        }
244
        // Track existing record data
245
        if ($record) {
246
            $name = $this->name;
247
            if ($record instanceof DataObject && $record->hasMethod($name)) {
248
                $data = $record->$name();
249
                // Wrap
250
                if ($data instanceof DataObject) {
251
                    $data = new ArrayList([$data]);
252
                }
253
                foreach ($data as $uploadedItem) {
254
                    $this->trackFileID($uploadedItem->ID);
255
                }
256
            }
257
        }
258
        return parent::setValue($value, $record);
259
    }
260
261
262
    /**
263
     * Configure our endpoint
264
     *
265
     * @link https://pqina.nl/filepond/docs/patterns/api/server/
266
     * @return array
267
     */
268
    public function getServerOptions()
269
    {
270
        if (!$this->getForm()) {
271
            throw new LogicException(
272
                'Field must be associated with a form to call getServerOptions(). Please use $field->setForm($form);'
273
            );
274
        }
275
        $endpoint = $this->chunkUploads ? 'chunk' : 'upload';
276
        $server = [
277
            'process' => $this->getUploadEnabled() ? $this->getLinkParameters($endpoint) : null,
278
            'fetch' => null,
279
            'revert' => null,
280
        ];
281
        if ($this->chunkUploads) {
282
            $server['fetch'] =  $this->getLinkParameters($endpoint . "?fetch=");
283
            $server['patch'] =  $this->getLinkParameters($endpoint . "?patch=");
284
        }
285
        return $server;
286
    }
287
288
    /**
289
     * Configure the following parameters:
290
     *
291
     * url : Path to the end point
292
     * method : Request method to use
293
     * withCredentials : Toggles the XMLHttpRequest withCredentials on or off
294
     * headers : An object containing additional headers to send
295
     * timeout : Timeout for this action
296
     * onload : Called when server response is received, useful for getting the unique file id from the server response
297
     * onerror : Called when server error is received, receis the response body, useful to select the relevant error data
298
     *
299
     * @param string $action
300
     * @return array
301
     */
302
    protected function getLinkParameters($action)
303
    {
304
        $form = $this->getForm();
305
        $token = $form->getSecurityToken()->getValue();
306
        $record = $form->getRecord();
307
308
        $headers = [
309
            'X-SecurityID' => $token
310
        ];
311
        // Allow us to track the record instance
312
        if ($record) {
0 ignored issues
show
introduced by
$record is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
313
            $headers['X-RecordClassName'] = get_class($record);
314
            $headers['X-RecordID'] = $record->ID;
315
        }
316
        return [
317
            'url' => $this->Link($action),
318
            'headers' => $headers,
319
        ];
320
    }
321
322
    /**
323
     * The maximum size of a file, for instance 5MB or 750KB
324
     * Suitable for JS usage
325
     *
326
     * @return string
327
     */
328
    public function getMaxFileSize()
329
    {
330
        return str_replace(' ', '', File::format_size($this->getValidator()->getAllowedMaxFileSize()));
331
    }
332
333
    /**
334
     * Set initial values to FilePondField
335
     * See: https://pqina.nl/filepond/docs/patterns/api/filepond-object/#setting-initial-files
336
     *
337
     * @return array
338
     */
339
    public function getExistingUploadsData()
340
    {
341
        // Both Value() & dataValue() seem to return an array eg: ['Files' => [258, 259, 257]]
342
        $fileIDarray = $this->Value() ?: ['Files' => []];
343
        if (!isset($fileIDarray['Files']) || !count($fileIDarray['Files'])) {
344
            return [];
345
        }
346
347
        $existingUploads = [];
348
        foreach ($fileIDarray['Files'] as $fileID) {
349
            /* @var $file File */
350
            $file = File::get()->byID($fileID);
351
            if (!$file) {
352
                continue;
353
            }
354
            // $poster = null;
355
            // if ($file instanceof Image) {
356
            //     $w = self::config()->get('thumbnail_width');
357
            //     $h = self::config()->get('thumbnail_height');
358
            //     $poster = $file->Fill($w, $h)->getAbsoluteURL();
359
            // }
360
            $existingUploads[] = [
361
                // the server file reference
362
                'source' => (int) $fileID,
363
                // set type to local to indicate an already uploaded file
364
                'options' => [
365
                    'type' => 'local',
366
                    // file information
367
                    'file' => [
368
                        'name' => $file->Name,
369
                        'size' => (int) $file->getAbsoluteSize(),
370
                        'type' => $file->getMimeType(),
371
                    ],
372
                    // poster
373
                    // 'metadata' => [
374
                    //     'poster' => $poster
375
                    // ]
376
                ],
377
378
            ];
379
        }
380
        return $existingUploads;
381
    }
382
383
    /**
384
     * Requirements are NOT versioned since filepond is regularly updated
385
     *
386
     * @return void
387
     */
388
    public static function Requirements()
389
    {
390
        // Polyfill to ensure max compatibility
391
        if (self::config()->enable_polyfill) {
392
            Requirements::javascript("https://cdn.jsdelivr.net/gh/pqina/filepond-polyfill/dist/filepond-polyfill.min.js");
393
        }
394
395
        // File/image validation plugins
396
        if (self::config()->enable_validation) {
397
            Requirements::javascript("https://cdn.jsdelivr.net/gh/pqina/filepond-plugin-file-validate-type/dist/filepond-plugin-file-validate-type.min.js");
398
            Requirements::javascript("https://cdn.jsdelivr.net/gh/pqina/filepond-plugin-file-validate-size/dist/filepond-plugin-file-validate-size.min.js");
399
            Requirements::javascript("https://cdn.jsdelivr.net/gh/pqina/filepond-plugin-image-validate-size/dist/filepond-plugin-image-validate-size.js");
400
        }
401
402
        // Poster plugins
403
        if (self::config()->enable_poster) {
404
            Requirements::javascript("https://cdn.jsdelivr.net/gh/pqina/filepond-plugin-file-metadata/dist/filepond-plugin-file-metadata.min.js");
405
            Requirements::css("https://cdn.jsdelivr.net/gh/pqina/filepond-plugin-file-poster/dist/filepond-plugin-file-poster.min.css");
406
            Requirements::javascript("https://cdn.jsdelivr.net/gh/pqina/filepond-plugin-file-poster/dist/filepond-plugin-file-poster.min.js");
407
        }
408
409
        // Image plugins
410
        if (self::config()->enable_image) {
411
            Requirements::javascript("https://cdn.jsdelivr.net/gh/pqina/filepond-plugin-image-exif-orientation/dist/filepond-plugin-image-exif-orientation.js");
412
            Requirements::css("https://cdn.jsdelivr.net/gh/pqina/unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.min.css");
413
            Requirements::javascript("https://cdn.jsdelivr.net/gh/pqina/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.min.js");
414
        }
415
416
        // Base elements
417
        Requirements::css("https://cdn.jsdelivr.net/gh/pqina/filepond/dist/filepond.css");
418
        Requirements::javascript("https://cdn.jsdelivr.net/gh/pqina/filepond/dist/filepond.js");
419
420
        // Our custom init
421
        Requirements::javascript('lekoala/silverstripe-filepond:javascript/FilePondField.js');
422
423
        // In the cms, init will not be triggered
424
        if (self::config()->enable_ajax_init && Director::is_ajax()) {
425
            Requirements::javascript('lekoala/silverstripe-filepond:javascript/FilePondField-init.js?t=' . time());
426
        }
427
    }
428
429
    public function FieldHolder($properties = array())
430
    {
431
        $config = $this->getFilePondConfig();
432
433
        $this->setAttribute('data-config', json_encode($config));
434
435
        if (self::config()->enable_requirements) {
436
            self::Requirements();
437
        }
438
439
        return parent::FieldHolder($properties);
440
    }
441
442
    /**
443
     * Check the incoming request
444
     *
445
     * @param HTTPRequest $request
446
     * @return array
447
     */
448
    public function prepareUpload(HTTPRequest $request)
449
    {
450
        $name = $this->getName();
451
        $tmpFile = $request->postVar($name);
452
        if (!$tmpFile) {
453
            throw new RuntimeException("No file");
454
        }
455
        $tmpFile = $this->normalizeTempFile($tmpFile);
456
457
        // Update $tmpFile with a better name
458
        if ($this->renamePattern) {
459
            $tmpFile['name'] = $this->changeFilenameWithPattern(
460
                $tmpFile['name'],
461
                $this->renamePattern
462
            );
463
        }
464
465
        return $tmpFile;
466
    }
467
468
    /**
469
     * @param HTTPRequest $request
470
     * @return void
471
     */
472
    protected function securityChecks(HTTPRequest $request)
473
    {
474
        if ($this->isDisabled() || $this->isReadonly()) {
475
            throw new RuntimeException("Field is disabled or readonly");
476
        }
477
478
        // CSRF check
479
        $token = $this->getForm()->getSecurityToken();
480
        if (!$token->checkRequest($request)) {
481
            throw new RuntimeException("Invalid token");
482
        }
483
    }
484
485
    /**
486
     * @param File $file
487
     * @param HTTPRequest $request
488
     * @return void
489
     */
490
    protected function setFileDetails(File $file, HTTPRequest $request)
491
    {
492
        // Mark as temporary until properly associated with a record
493
        // Files will be unmarked later on by saveInto method
494
        $file->IsTemporary = true;
495
496
        // We can also track the record
497
        $RecordID = $request->getHeader('X-RecordID');
498
        $RecordClassName = $request->getHeader('X-RecordClassName');
499
        if (!$file->ObjectID) {
500
            $file->ObjectID = $RecordID;
501
        }
502
        if (!$file->ObjectClass) {
503
            $file->ObjectClass = $RecordClassName;
504
        }
505
506
        if ($file->isChanged()) {
507
            // If possible, prevent creating a version for no reason
508
            // @link https://docs.silverstripe.org/en/4/developer_guides/model/versioning/#writing-changes-to-a-versioned-dataobject
509
            if ($file->hasExtension(Versioned::class)) {
510
                $file->writeWithoutVersion();
511
            } else {
512
                $file->write();
513
            }
514
        }
515
    }
516
517
    /**
518
     * Creates a single file based on a form-urlencoded upload.
519
     *
520
     * 1 client uploads file my-file.jpg as multipart/form-data using a POST request
521
     * 2 server saves file to unique location tmp/12345/my-file.jpg
522
     * 3 server returns unique location id 12345 in text/plain response
523
     * 4 client stores unique id 12345 in a hidden input field
524
     * 5 client submits the FilePond parent form containing the hidden input field with the unique id
525
     * 6 server uses the unique id to move tmp/12345/my-file.jpg to its final location and remove the tmp/12345 folder
526
     *
527
     * Along with the file object, FilePond also sends the file metadata to the server, both these objects are given the same name.
528
     *
529
     * @param HTTPRequest $request
530
     * @return HTTPResponse
531
     */
532
    public function upload(HTTPRequest $request)
533
    {
534
        try {
535
            $this->securityChecks($request);
536
            $tmpFile = $this->prepareUpload($request);
537
        } catch (Exception $ex) {
538
            return $this->httpError(400, $ex->getMessage());
539
        }
540
541
        $file = $this->saveTemporaryFile($tmpFile, $error);
542
543
        // Handle upload errors
544
        if ($error) {
545
            $this->getUpload()->clearErrors();
546
            return $this->httpError(400, json_encode($error));
547
        }
548
549
        // File can be an AssetContainer and not a DataObject
550
        if ($file instanceof DataObject) {
551
            $this->setFileDetails($file, $request);
552
        }
553
554
        $this->getUpload()->clearErrors();
555
        $fileId = $file->ID;
0 ignored issues
show
Bug introduced by
Accessing ID on the interface SilverStripe\Assets\Storage\AssetContainer suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
556
        $this->trackFileID($fileId);
557
558
        if (self::config()->auto_clear_temp_folder) {
559
            // Set a limit of 100 because otherwise it would be really slow
560
            self::clearTemporaryUploads(true, 100);
561
        }
562
563
        // server returns unique location id 12345 in text/plain response
564
        $response = new HTTPResponse($fileId);
565
        $response->addHeader('Content-Type', 'text/plain');
566
        return $response;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response returns the type SilverStripe\Control\HTTPResponse which is incompatible with the return type mandated by LeKoala\FilePond\AbstractUploadField::upload() of LeKoala\FilePond\HTTPResponse.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
567
    }
568
569
    /**
570
     * @link https://pqina.nl/filepond/docs/api/server/#process-chunks
571
     * @param HTTPRequest $request
572
     * @return void
573
     */
574
    public function chunk(HTTPRequest $request)
575
    {
576
        try {
577
            $this->securityChecks($request);
578
        } catch (Exception $ex) {
579
            return $this->httpError(400, $ex->getMessage());
580
        }
581
582
        $method = $request->httpMethod();
583
584
        // The random token is returned as a query string
585
        $id = $request->getVar('patch');
586
587
        // FilePond will send a POST request (without file) to start a chunked transfer,
588
        // expecting to receive a unique transfer id in the response body, it'll add the Upload-Length header to this request.
589
        if ($method == "POST") {
590
            // Initial post payload doesn't contain name
591
            $file = new File();
592
            $this->setFileDetails($file, $request);
593
            $fileId = $file->ID;
594
            $this->trackFileID($fileId);
595
            $response = new HTTPResponse($fileId, 200);
596
            $response->addHeader('Content-Type', 'text/plain');
597
            return $response;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response returns the type SilverStripe\Control\HTTPResponse which is incompatible with the documented return type void.
Loading history...
598
        }
599
600
        // FilePond will send a HEAD request to determine which chunks have already been uploaded,
601
        // expecting the file offset of the next expected chunk in the Upload-Offset response header.
602
        if ($method == "HEAD") {
603
            $nextOffset = 0;
604
605
            //TODO: iterate over temp files and check next offset
606
            $response = new HTTPResponse($nextOffset, 200);
607
            $response->addHeader('Content-Type', 'text/plain');
608
            return $response;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response returns the type SilverStripe\Control\HTTPResponse which is incompatible with the documented return type void.
Loading history...
609
        }
610
611
        // FilePond will send a PATCH request to push a chunk to the server.
612
        // Each of these requests is accompanied by a Content-Type, Upload-Offset, Upload-Name, and Upload-Length header.
613
        if ($method != "PATCH") {
614
            return $this->httpError(400, "Invalid method");
615
        }
616
617
        // The name of the file being transferred
618
        $uploadName = $request->getHeader('Upload-Name');
619
        // The total size of the file being transferred
620
        $offset = $request->getHeader('Upload-Offset');
621
        // The offset of the chunk being transferred
622
        $length = $request->getHeader('Upload-Length');
623
624
        // location of patch files
625
        $filePath = TEMP_PATH . "/filepond-" . $id;
626
627
        // should be numeric values, else exit
628
        if (!is_numeric($offset) || !is_numeric($length)) {
629
            return $this->httpError(400, "Invalid offset or length");
630
        }
631
632
        // write patch file for this request
633
        file_put_contents($filePath . '.patch.' . $offset, $request->getBody());
634
635
        // calculate total size of patches
636
        $size = 0;
637
        $patch = glob($filePath . '.patch.*');
638
        foreach ($patch as $filename) {
639
            $size += filesize($filename);
640
        }
641
        // if total size equals length of file we have gathered all patch files
642
        if ($size >= $length) {
643
            // create output file
644
            $outputFile = fopen($filePath, 'wb');
645
            // write patches to file
646
            foreach ($patch as $filename) {
647
                // get offset from filename
648
                list($dir, $offset) = explode('.patch.', $filename, 2);
649
                // read patch and close
650
                $patchFile = fopen($filename, 'rb');
651
                $patchContent = fread($patchFile, filesize($filename));
652
                fclose($patchFile);
653
654
                // apply patch
655
                fseek($outputFile, $offset);
0 ignored issues
show
Bug introduced by
$offset of type string is incompatible with the type integer expected by parameter $offset of fseek(). ( Ignorable by Annotation )

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

655
                fseek($outputFile, /** @scrutinizer ignore-type */ $offset);
Loading history...
656
                fwrite($outputFile, $patchContent);
657
            }
658
            // remove patches
659
            foreach ($patch as $filename) {
660
                unlink($filename);
661
            }
662
            // done with file
663
            fclose($outputFile);
664
665
            // Finalize real filename
666
            $realFilename = $this->getFolderName() . "/" . $uploadName;
667
            if ($this->renamePattern) {
668
                $realFilename = $this->changeFilenameWithPattern(
669
                    $realFilename,
670
                    $this->renamePattern
671
                );
672
            }
673
674
            // write output file to asset store
675
            $file = $this->getFileByID($id);
676
            if (!$file) {
0 ignored issues
show
introduced by
$file is of type SilverStripe\Assets\File, thus it always evaluated to true.
Loading history...
677
                return $this->httpError("File $id not found");
678
            }
679
            $file->setFromLocalFile($filePath);
680
            $file->setFilename($realFilename);
681
            $file->write();
682
        }
683
        $response = new HTTPResponse('', 204);
684
        return $response;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response returns the type SilverStripe\Control\HTTPResponse which is incompatible with the documented return type void.
Loading history...
685
    }
686
687
    /**
688
     * Clear temp folder that should not contain any file other than temporary
689
     *
690
     * @param boolean $doDelete Set to true to actually delete the files, otherwise it's just a dry-run
691
     * @param int $limit
692
     * @return File[] List of files removed
693
     */
694
    public static function clearTemporaryUploads($doDelete = false, $limit = 0)
695
    {
696
        $tempFiles = File::get()->filter('IsTemporary', true);
697
        if ($limit) {
698
            $tempFiles = $tempFiles->limit($limit);
699
        }
700
701
        $threshold = self::config()->auto_clear_threshold;
702
703
        // Set a default threshold if none set
704
        if (!$threshold) {
705
            if (Director::isDev()) {
706
                $threshold = '-10 minutes';
707
            } else {
708
                $threshold = '-1 day';
709
            }
710
        }
711
        if (is_int($threshold)) {
712
            $thresholdTime = time() - $threshold;
713
        } else {
714
            $thresholdTime = strtotime($threshold);
715
        }
716
717
        // Update query to avoid fetching unecessary records
718
        $tempFiles = $tempFiles->filter("Created:LessThan", date('Y-m-d H:i:s', $thresholdTime));
719
720
        $filesDeleted = [];
721
        foreach ($tempFiles as $tempFile) {
722
            $createdTime = strtotime($tempFile->Created);
723
            if ($createdTime < $thresholdTime) {
724
                $filesDeleted[] = $tempFile;
725
                if ($doDelete) {
726
                    if ($tempFile->hasExtension(Versioned::class)) {
727
                        $tempFile->deleteFromStage(Versioned::LIVE);
728
                        $tempFile->deleteFromStage(Versioned::DRAFT);
729
                    } else {
730
                        $tempFile->delete();
731
                    }
732
                }
733
            }
734
        }
735
        return $filesDeleted;
736
    }
737
738
    /**
739
     * Allows tracking uploaded ids to prevent unauthorized attachements
740
     *
741
     * @param int $fileId
742
     * @return void
743
     */
744
    public function trackFileID($fileId)
745
    {
746
        $session = $this->getRequest()->getSession();
747
        $uploadedIDs = $this->getTrackedIDs();
748
        if (!in_array($fileId, $uploadedIDs)) {
749
            $uploadedIDs[] = $fileId;
750
        }
751
        $session->set('FilePond', $uploadedIDs);
752
    }
753
754
    /**
755
     * Get all authorized tracked ids
756
     * @return array
757
     */
758
    public function getTrackedIDs()
759
    {
760
        $session = $this->getRequest()->getSession();
761
        $uploadedIDs = $session->get('FilePond');
762
        if ($uploadedIDs) {
763
            return $uploadedIDs;
764
        }
765
        return [];
766
    }
767
768
    public function saveInto(DataObjectInterface $record)
769
    {
770
        // Note that the list of IDs is based on the value sent by the user
771
        // It can be spoofed because checks are minimal (by default, canView = true and only check if isInDB)
772
        $IDs = $this->getItemIDs();
773
774
        $Member = Security::getCurrentUser();
775
776
        // Ensure the files saved into the DataObject have been tracked (either because already on the DataObject or uploaded by the user)
777
        $trackedIDs = $this->getTrackedIDs();
778
        foreach ($IDs as $ID) {
779
            if (!in_array($ID, $trackedIDs)) {
780
                throw new ValidationException("Invalid file ID : $ID");
781
            }
782
        }
783
784
        // Move files out of temporary folder
785
        foreach ($IDs as $ID) {
786
            $file = $this->getFileByID($ID);
787
            if ($file && $file->IsTemporary) {
788
                // The record does not have an ID which is a bad idea to attach the file to it
789
                if (!$record->ID) {
790
                    $record->write();
791
                }
792
                // Check if the member is owner
793
                if ($Member && $Member->ID != $file->OwnerID) {
794
                    throw new ValidationException("Failed to authenticate owner");
795
                }
796
                $file->IsTemporary = false;
797
                $file->ObjectID = $record->ID;
798
                $file->ObjectClass = get_class($record);
799
                $file->write();
800
            } else {
801
                // File was uploaded earlier, no need to do anything
802
            }
803
        }
804
805
        // Proceed
806
        return parent::saveInto($record);
807
    }
808
809
    public function Type()
810
    {
811
        return 'filepond';
812
    }
813
}
814