Completed
Pull Request — master (#6325)
by Damian
08:47
created

UploadField::setValue()   C

Complexity

Conditions 16
Paths 91

Size

Total Lines 68
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 16
eloc 31
nc 91
nop 2
dl 0
loc 68
rs 5.7201
c 0
b 0
f 0

4 Methods

Rating   Name   Duplication   Size   Complexity  
A UploadField::setAutoUpload() 0 4 1
A UploadField::getAllowedMaxFileNumber() 0 18 4
A UploadField::setAllowedMaxFileNumber() 0 4 1
A UploadField::canUpload() 0 8 3

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace SilverStripe\Forms;
4
5
use SilverStripe\Assets\Storage\AssetContainer;
6
use SilverStripe\Assets\File;
7
use SilverStripe\Assets\FileNameFilter;
8
use SilverStripe\Assets\Folder;
9
use SilverStripe\Control\HTTPRequest;
10
use SilverStripe\Control\HTTPResponse;
11
use SilverStripe\Core\Convert;
12
use SilverStripe\Core\Object;
13
use SilverStripe\ORM\SS_List;
14
use SilverStripe\ORM\DataObject;
15
use SilverStripe\ORM\ArrayList;
16
use SilverStripe\ORM\ValidationException;
17
use SilverStripe\Security\Permission;
18
use SilverStripe\View\ArrayData;
19
use SilverStripe\View\ViewableData;
20
use SilverStripe\View\ViewableData_Customised;
21
use InvalidArgumentException;
22
use Exception;
23
24
/**
25
 * Field for uploading single or multiple files of all types, including images.
26
 *
27
 * <b>Features (some might not be available to old browsers):</b>
28
 *
29
 * - File Drag&Drop support
30
 * - Progressbar
31
 * - Image thumbnail/file icons even before upload finished
32
 * - Saving into relations on form submit
33
 * - Edit file
34
 * - allowedExtensions is by default File::$allowed_extensions<li>maxFileSize the value of min(upload_max_filesize,
35
 * post_max_size) from php.ini
36
 *
37
 * <>Usage</b>
38
 *
39
 * @example <code>
40
 * $UploadField = new UploadField('AttachedImages', 'Please upload some images <span>(max. 5 files)</span>');
41
 * $UploadField->setAllowedFileCategories('image');
42
 * $UploadField->setAllowedMaxFileNumber(5);
43
 * </code>
44
 *
45
 * Caution: The form field does not include any JavaScript or CSS when used outside of the CMS context,
46
 * since the required frontend dependencies are included through CMS bundling.
47
 */
48
class UploadField extends FormField
49
{
50
    use FileUploadReceiver;
51
52
    /**
53
     * @var array
54
     */
55
    private static $allowed_actions = array(
56
        'upload',
57
        'attach',
58
        'handleItem',
59
        'handleSelect',
60
        'fileexists'
61
    );
62
63
    /**
64
     * @var array
65
     */
66
    private static $url_handlers = array(
67
        'item/$ID' => 'handleItem',
68
        'select' => 'handleSelect',
69
        '$Action!' => '$Action',
70
    );
71
72
    /**
73
     * Template to use for the file button widget
74
     *
75
     * @var string
76
     */
77
    protected $templateFileButtons = null;
78
79
    /**
80
     * Template to use for the edit form
81
     *
82
     * @var string
83
     */
84
    protected $templateFileEdit = null;
85
86
    /**
87
     * Config for this field used in the front-end javascript
88
     * (will be merged into the config of the javascript file upload plugin).
89
     *
90
     * @var array
91
     */
92
    protected $ufConfig = array();
93
94
    /**
95
     * Front end config defaults
96
     *
97
     * @config
98
     * @var array
99
     */
100
    private static $defaultConfig = array(
101
        /**
102
         * Automatically upload the file once selected
103
         *
104
         * @var boolean
105
         */
106
        'autoUpload' => true,
107
        /**
108
         * Restriction on number of files that may be set for this field. Set to null to allow
109
         * unlimited. If record has a has_one and allowedMaxFileNumber is null, it will be set to 1.
110
         * The resulting value will be set to maxNumberOfFiles
111
         *
112
         * @var integer
113
         */
114
        'allowedMaxFileNumber' => null,
115
        /**
116
         * Can the user upload new files, or just select from existing files.
117
         * String values are interpreted as permission codes.
118
         *
119
         * @var boolean|string
120
         */
121
        'canUpload' => true,
122
        /**
123
         * Can the user attach files from the assets archive on the site?
124
         * String values are interpreted as permission codes.
125
         *
126
         * @var boolean|string
127
         */
128
        'canAttachExisting' => "CMS_ACCESS_AssetAdmin",
129
        /**
130
         * Shows the target folder for new uploads in the field UI.
131
         * Disable to keep the internal filesystem structure hidden from users.
132
         *
133
         * @var boolean|string
134
         */
135
        'canPreviewFolder' => true,
136
        /**
137
         * Indicate a change event to the containing form if an upload
138
         * or file edit/delete was performed.
139
         *
140
         * @var boolean
141
         */
142
        'changeDetection' => true,
143
        /**
144
         * Maximum width of the preview thumbnail
145
         *
146
         * @var integer
147
         */
148
        'previewMaxWidth' => 80,
149
        /**
150
         * Maximum height of the preview thumbnail
151
         *
152
         * @var integer
153
         */
154
        'previewMaxHeight' => 60,
155
        /**
156
         * javascript template used to display uploading files
157
         *
158
         * @see javascript/UploadField_uploadtemplate.js
159
         * @var string
160
         */
161
        'uploadTemplateName' => 'ss-uploadfield-uploadtemplate',
162
        /**
163
         * javascript template used to display already uploaded files
164
         *
165
         * @see javascript/UploadField_downloadtemplate.js
166
         * @var string
167
         */
168
        'downloadTemplateName' => 'ss-uploadfield-downloadtemplate',
169
        /**
170
         * Show a warning when overwriting a file.
171
         * This requires Upload->replaceFile config to be set to true, otherwise
172
         * files will be renamed instead of overwritten
173
         *
174
         * @see Upload
175
         * @var boolean
176
         */
177
        'overwriteWarning' => true
178
    );
179
180
    /**
181
     * @var String Folder to display in "Select files" list.
182
     * Defaults to listing all files regardless of folder.
183
     * The folder path should be relative to the webroot.
184
     * See {@link FileField->folderName} to set the upload target instead.
185
     * @example admin/folder/subfolder
186
     */
187
    protected $displayFolderName;
188
189
    /**
190
     * FieldList $fields or string $name (of a method on File to provide a fields) for the EditForm
191
     * @example 'getCMSFields'
192
     *
193
     * @var FieldList|string
194
     */
195
    protected $fileEditFields = null;
196
197
    /**
198
     * FieldList $actions or string $name (of a method on File to provide a actions) for the EditForm
199
     * @example 'getCMSActions'
200
     *
201
     * @var FieldList|string
202
     */
203
    protected $fileEditActions = null;
204
205
    /**
206
     * Validator (eg RequiredFields) or string $name (of a method on File to provide a Validator) for the EditForm
207
     * @example 'getCMSValidator'
208
     *
209
     * @var RequiredFields|string
210
     */
211
    protected $fileEditValidator = null;
212
213
    /**
214
     * Construct a new UploadField instance
215
     *
216
     * @param string $name The internal field name, passed to forms.
217
     * @param string $title The field label.
218
     * @param SS_List $items If no items are defined, the field will try to auto-detect an existing relation on
219
     *                       @link $record}, with the same name as the field name.
220
     */
221
    public function __construct($name, $title = null, SS_List $items = null)
222
    {
223
        // TODO thats the first thing that came to my head, feel free to change it
224
        $this->addExtraClass('ss-upload'); // class, used by js
225
        $this->addExtraClass('ss-uploadfield'); // class, used by css for uploadfield only
226
227
        $this->ufConfig = self::config()->defaultConfig;
0 ignored issues
show
Documentation introduced by
The property defaultConfig does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
228
        $this->constructFileUploadReceiver();
229
230
        parent::__construct($name, $title);
231
232
        if ($items) {
233
            $this->setItems($items);
234
        }
235
    }
236
237
    /**
238
     * Set name of template used for Buttons on each file (replace, edit, remove, delete) (without path or extension)
239
     *
240
     * @param string $template
241
     * @return $this
242
     */
243
    public function setTemplateFileButtons($template)
244
    {
245
        $this->templateFileButtons = $template;
246
        return $this;
247
    }
248
249
    /**
250
     * @return string
251
     */
252
    public function getTemplateFileButtons()
253
    {
254
        return $this->_templates($this->templateFileButtons, '_FileButtons');
255
    }
256
257
    /**
258
     * Set name of template used for the edit (inline & popup) of a file file (without path or extension)
259
     *
260
     * @param string $template
261
     * @return $this
262
     */
263
    public function setTemplateFileEdit($template)
264
    {
265
        $this->templateFileEdit = $template;
266
        return $this;
267
    }
268
269
    /**
270
     * @return string
271
     */
272
    public function getTemplateFileEdit()
273
    {
274
        return $this->_templates($this->templateFileEdit, '_FileEdit');
275
    }
276
277
    /**
278
     * Determine if the target folder for new uploads in is visible the field UI.
279
     *
280
     * @return boolean
281
     */
282
    public function canPreviewFolder()
283
    {
284
        if (!$this->isActive()) {
285
            return false;
286
        }
287
        $can = $this->getConfig('canPreviewFolder');
288
        return (is_bool($can)) ? $can : Permission::check($can);
289
    }
290
291
    /**
292
     * Determine if the target folder for new uploads in is visible the field UI.
293
     * Disable to keep the internal filesystem structure hidden from users.
294
     *
295
     * @param boolean|string $canPreviewFolder Either a boolean flag, or a
296
     * required permission code
297
     * @return UploadField Self reference
298
     */
299
    public function setCanPreviewFolder($canPreviewFolder)
300
    {
301
        return $this->setConfig('canPreviewFolder', $canPreviewFolder);
302
    }
303
304
    /**
305
     * Determine if the field should show a warning when overwriting a file.
306
     * This requires Upload->replaceFile config to be set to true, otherwise
307
     * files will be renamed instead of overwritten (although the warning will
308
     * still be displayed)
309
     *
310
     * @return boolean
311
     */
312
    public function getOverwriteWarning()
313
    {
314
        return $this->getConfig('overwriteWarning');
315
    }
316
317
    /**
318
     * Determine if the field should show a warning when overwriting a file.
319
     * This requires Upload->replaceFile config to be set to true, otherwise
320
     * files will be renamed instead of overwritten (although the warning will
321
     * still be displayed)
322
     *
323
     * @param boolean $overwriteWarning
324
     * @return UploadField Self reference
325
     */
326
    public function setOverwriteWarning($overwriteWarning)
327
    {
328
        return $this->setConfig('overwriteWarning', $overwriteWarning);
329
    }
330
331
    /**
332
     * @param string $name
333
     * @return $this
334
     */
335
    public function setDisplayFolderName($name)
336
    {
337
        $this->displayFolderName = $name;
338
        return $this;
339
    }
340
341
    /**
342
     * @return String
343
     */
344
    public function getDisplayFolderName()
345
    {
346
        return $this->displayFolderName;
347
    }
348
349
350
351
    /**
352
     * Retrieves a customised list of all File records to ensure they are
353
     * properly viewable when rendered in the field template.
354
     *
355
     * @return SS_List[ViewableData_Customised]
0 ignored issues
show
Documentation introduced by
The doc-type SS_List[ViewableData_Customised] could not be parsed: Expected "]" at position 2, but found "ViewableData_Customised". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
356
     */
357
    public function getCustomisedItems()
358
    {
359
        $customised = new ArrayList();
360
        foreach ($this->getItems() as $file) {
361
            $customised->push($this->customiseFile($file));
362
        }
363
        return $customised;
364
    }
365
366
    /**
367
     * Customises a file with additional details suitable for rendering in the
368
     * UploadField.ss template
369
     *
370
     * @param ViewableData|AssetContainer $file
371
     * @return ViewableData_Customised
372
     */
373
    protected function customiseFile(AssetContainer $file)
374
    {
375
        $file = $file->customise(array(
376
            'UploadFieldThumbnailURL' => $this->getThumbnailURLForFile($file),
377
            'UploadFieldDeleteLink' => $this->getItemHandler($file->ID)->DeleteLink(),
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?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
378
            'UploadFieldEditLink' => $this->getItemHandler($file->ID)->EditLink(),
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?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
379
            'UploadField' => $this
380
        ));
381
        // we do this in a second customise to have the access to the previous customisations
382
        return $file->customise(array(
383
            'UploadFieldFileButtons' => $file->renderWith($this->getTemplateFileButtons())
384
        ));
385
    }
386
387
    /**
388
     * Assign a front-end config variable for the upload field
389
     *
390
     * @see https://github.com/blueimp/jQuery-File-Upload/wiki/Options for the list of front end options available
391
     *
392
     * @param string $key
393
     * @param mixed $val
394
     * @return UploadField self reference
395
     */
396
    public function setConfig($key, $val)
397
    {
398
        $this->ufConfig[$key] = $val;
399
        return $this;
400
    }
401
402
    /**
403
     * Gets a front-end config variable for the upload field
404
     *
405
     * @see https://github.com/blueimp/jQuery-File-Upload/wiki/Options for the list of front end options available
406
     *
407
     * @param string $key
408
     * @return mixed
409
     */
410
    public function getConfig($key)
411
    {
412
        if (!isset($this->ufConfig[$key])) {
413
            return null;
414
        }
415
        return $this->ufConfig[$key];
416
    }
417
418
    /**
419
     * Determine if the field should automatically upload the file.
420
     *
421
     * @return boolean
422
     */
423
    public function getAutoUpload()
424
    {
425
        return $this->getConfig('autoUpload');
426
    }
427
428
    /**
429
     * Determine if the field should automatically upload the file
430
     *
431
     * @param boolean $autoUpload
432
     * @return UploadField Self reference
433
     */
434
    public function setAutoUpload($autoUpload)
435
    {
436
        return $this->setConfig('autoUpload', $autoUpload);
437
    }
438
439
    /**
440
     * Determine maximum number of files allowed to be attached
441
     * Defaults to 1 for has_one and null (unlimited) for
442
     * many_many and has_many relations.
443
     *
444
     * @return integer|null Maximum limit, or null for no limit
445
     */
446
    public function getAllowedMaxFileNumber()
447
    {
448
        $allowedMaxFileNumber = $this->getConfig('allowedMaxFileNumber');
449
450
        // if there is a has_one relation with that name on the record and
451
        // allowedMaxFileNumber has not been set, it's wanted to be 1
452
        if (empty($allowedMaxFileNumber)) {
453
            $record = $this->getRecord();
454
            $name = $this->getName();
455
            if ($record && DataObject::getSchema()->hasOneComponent(get_class($record), $name)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression \SilverStripe\ORM\DataOb..._class($record), $name) of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
456
                return 1; // Default for has_one
457
            } else {
458
                return null; // Default for has_many and many_many
459
            }
460
        } else {
461
            return $allowedMaxFileNumber;
462
        }
463
    }
464
465
    /**
466
     * Determine maximum number of files allowed to be attached.
467
     *
468
     * @param integer|null $allowedMaxFileNumber Maximum limit. 0 or null will be treated as unlimited
469
     * @return UploadField Self reference
470
     */
471
    public function setAllowedMaxFileNumber($allowedMaxFileNumber)
472
    {
473
        return $this->setConfig('allowedMaxFileNumber', $allowedMaxFileNumber);
474
    }
475
476
    /**
477
     * Determine if the user has permission to upload.
478
     *
479
     * @return boolean
480
     */
481
    public function canUpload()
482
    {
483
        if (!$this->isActive()) {
484
            return false;
485
        }
486
        $can = $this->getConfig('canUpload');
487
        return (is_bool($can)) ? $can : Permission::check($can);
488
    }
489
490
    /**
491
     * Specify whether the user can upload files.
492
     * String values will be treated as required permission codes
493
     *
494
     * @param boolean|string $canUpload Either a boolean flag, or a required
495
     * permission code
496
     * @return UploadField Self reference
497
     */
498
    public function setCanUpload($canUpload)
499
    {
500
        return $this->setConfig('canUpload', $canUpload);
501
    }
502
503
    /**
504
     * Determine if the user has permission to attach existing files
505
     * By default returns true if the user has the CMS_ACCESS_AssetAdmin permission
506
     *
507
     * @return boolean
508
     */
509
    public function canAttachExisting()
510
    {
511
        if (!$this->isActive()) {
512
            return false;
513
        }
514
        $can = $this->getConfig('canAttachExisting');
515
        return (is_bool($can)) ? $can : Permission::check($can);
516
    }
517
518
    /**
519
     * Returns true if the field is neither readonly nor disabled
520
     *
521
     * @return boolean
522
     */
523
    public function isActive()
524
    {
525
        return !$this->isDisabled() && !$this->isReadonly();
526
    }
527
528
    /**
529
     * Specify whether the user can attach existing files
530
     * String values will be treated as required permission codes
531
     *
532
     * @param boolean|string $canAttachExisting Either a boolean flag, or a
533
     * required permission code
534
     * @return UploadField Self reference
535
     */
536
    public function setCanAttachExisting($canAttachExisting)
537
    {
538
        return $this->setConfig('canAttachExisting', $canAttachExisting);
539
    }
540
541
    /**
542
     * Gets thumbnail width. Defaults to 80
543
     *
544
     * @return integer
545
     */
546
    public function getPreviewMaxWidth()
547
    {
548
        return $this->getConfig('previewMaxWidth');
549
    }
550
551
    /**
552
     * @see UploadField::getPreviewMaxWidth()
553
     *
554
     * @param integer $previewMaxWidth
555
     * @return UploadField Self reference
556
     */
557
    public function setPreviewMaxWidth($previewMaxWidth)
558
    {
559
        return $this->setConfig('previewMaxWidth', $previewMaxWidth);
560
    }
561
562
    /**
563
     * Gets thumbnail height. Defaults to 60
564
     *
565
     * @return integer
566
     */
567
    public function getPreviewMaxHeight()
568
    {
569
        return $this->getConfig('previewMaxHeight');
570
    }
571
572
    /**
573
     * @see UploadField::getPreviewMaxHeight()
574
     *
575
     * @param integer $previewMaxHeight
576
     * @return UploadField Self reference
577
     */
578
    public function setPreviewMaxHeight($previewMaxHeight)
579
    {
580
        return $this->setConfig('previewMaxHeight', $previewMaxHeight);
581
    }
582
583
    /**
584
     * javascript template used to display uploading files
585
     * Defaults to 'ss-uploadfield-uploadtemplate'
586
     *
587
     * @see javascript/UploadField_uploadtemplate.js
588
     * @return string
589
     */
590
    public function getUploadTemplateName()
591
    {
592
        return $this->getConfig('uploadTemplateName');
593
    }
594
595
    /**
596
     * @see UploadField::getUploadTemplateName()
597
     *
598
     * @param string $uploadTemplateName
599
     * @return UploadField Self reference
600
     */
601
    public function setUploadTemplateName($uploadTemplateName)
602
    {
603
        return $this->setConfig('uploadTemplateName', $uploadTemplateName);
604
    }
605
606
    /**
607
     * javascript template used to display already uploaded files
608
     * Defaults to 'ss-downloadfield-downloadtemplate'
609
     *
610
     * @see javascript/DownloadField_downloadtemplate.js
611
     * @return string
612
     */
613
    public function getDownloadTemplateName()
614
    {
615
        return $this->getConfig('downloadTemplateName');
616
    }
617
618
    /**
619
     * @see Uploadfield::getDownloadTemplateName()
620
     *
621
     * @param string $downloadTemplateName
622
     * @return Uploadfield Self reference
623
     */
624
    public function setDownloadTemplateName($downloadTemplateName)
625
    {
626
        return $this->setConfig('downloadTemplateName', $downloadTemplateName);
627
    }
628
629
    /**
630
     * FieldList $fields for the EditForm
631
     * @example 'getCMSFields'
632
     *
633
     * @param DataObject $file File context to generate fields for
634
     * @return FieldList List of form fields
635
     */
636
    public function getFileEditFields(DataObject $file)
637
    {
638
        // Empty actions, generate default
639
        if (empty($this->fileEditFields)) {
640
            $fields = $file->getCMSFields();
641
            // Only display main tab, to avoid overly complex interface
642
            if ($fields->hasTabSet() && ($mainTab = $fields->findOrMakeTab('Root.Main'))) {
643
                $fields = $mainTab->Fields();
644
            }
645
            return $fields;
646
        }
647
648
        // Fields instance
649
        if ($this->fileEditFields instanceof FieldList) {
650
            return $this->fileEditFields;
651
        }
652
653
        // Method to call on the given file
654
        if ($file->hasMethod($this->fileEditFields)) {
655
            return $file->{$this->fileEditFields}();
656
        }
657
658
        throw new InvalidArgumentException("Invalid value for UploadField::fileEditFields");
659
    }
660
661
    /**
662
     * FieldList $fields or string $name (of a method on File to provide a fields) for the EditForm
663
     * @example 'getCMSFields'
664
     *
665
     * @param FieldList|string
666
     * @return Uploadfield Self reference
667
     */
668
    public function setFileEditFields($fileEditFields)
669
    {
670
        $this->fileEditFields = $fileEditFields;
671
        return $this;
672
    }
673
674
    /**
675
     * FieldList $actions or string $name (of a method on File to provide a actions) for the EditForm
676
     * @example 'getCMSActions'
677
     *
678
     * @param DataObject $file File context to generate form actions for
679
     * @return FieldList Field list containing FormAction
680
     */
681
    public function getFileEditActions(DataObject $file)
682
    {
683
        // Empty actions, generate default
684
        if (empty($this->fileEditActions)) {
685
            $actions = new FieldList($saveAction = new FormAction('doEdit', _t('UploadField.DOEDIT', 'Save')));
686
            $saveAction->addExtraClass('ss-ui-action-constructive icon-accept');
687
            return $actions;
688
        }
689
690
        // Actions instance
691
        if ($this->fileEditActions instanceof FieldList) {
692
            return $this->fileEditActions;
693
        }
694
695
        // Method to call on the given file
696
        if ($file->hasMethod($this->fileEditActions)) {
697
            return $file->{$this->fileEditActions}();
698
        }
699
700
        throw new InvalidArgumentException("Invalid value for UploadField::fileEditActions");
701
    }
702
703
    /**
704
     * FieldList $actions or string $name (of a method on File to provide a actions) for the EditForm
705
     * @example 'getCMSActions'
706
     *
707
     * @param FieldList|string
708
     * @return Uploadfield Self reference
709
     */
710
    public function setFileEditActions($fileEditActions)
711
    {
712
        $this->fileEditActions = $fileEditActions;
713
        return $this;
714
    }
715
716
    /**
717
     * Determines the validator to use for the edit form
718
     * @example 'getCMSValidator'
719
     *
720
     * @param DataObject $file File context to generate validator from
721
     * @return Validator Validator object
722
     */
723
    public function getFileEditValidator(DataObject $file)
724
    {
725
        // Empty validator
726
        if (empty($this->fileEditValidator)) {
727
            return null;
728
        }
729
730
        // Validator instance
731
        if ($this->fileEditValidator instanceof Validator) {
732
            return $this->fileEditValidator;
733
        }
734
735
        // Method to call on the given file
736
        if ($file->hasMethod($this->fileEditValidator)) {
737
            return $file->{$this->fileEditValidator}();
738
        }
739
740
        throw new InvalidArgumentException("Invalid value for UploadField::fileEditValidator");
741
    }
742
743
    /**
744
     * Validator (eg RequiredFields) or string $name (of a method on File to provide a Validator) for the EditForm
745
     * @example 'getCMSValidator'
746
     *
747
     * @param Validator|string
748
     * @return Uploadfield Self reference
749
     */
750
    public function setFileEditValidator($fileEditValidator)
751
    {
752
        $this->fileEditValidator = $fileEditValidator;
753
        return $this;
754
    }
755
756
    /**
757
     *
758
     * @param File|AssetContainer $file
759
     * @return string URL to thumbnail
760
     */
761
    protected function getThumbnailURLForFile(AssetContainer $file)
762
    {
763
        if (!$file->exists()) {
764
            return null;
765
        }
766
767
        // Attempt to generate image at given size
768
        $width = $this->getPreviewMaxWidth();
769
        $height = $this->getPreviewMaxHeight();
770
        if ($file->hasMethod('ThumbnailURL')) {
771
            return $file->ThumbnailURL($width, $height);
772
        }
773
        if ($file->hasMethod('Thumbnail')) {
774
            return $file->Thumbnail($width, $height)->getURL();
775
        }
776
        if ($file->hasMethod('Fit')) {
777
            return $file->Fit($width, $height)->getURL();
778
        }
779
780
        // Check if unsized icon is available
781
        if ($file->hasMethod('getIcon')) {
782
            return $file->getIcon();
783
        }
784
        return null;
785
    }
786
787
    public function getAttributes()
788
    {
789
        return array_merge(
790
            parent::getAttributes(),
791
            array(
792
                'type' => 'file',
793
                'data-selectdialog-url' => $this->Link('select')
794
            )
795
        );
796
    }
797
798
    public function extraClass()
799
    {
800
        if ($this->isDisabled()) {
801
            $this->addExtraClass('disabled');
802
        }
803
        if ($this->isReadonly()) {
804
            $this->addExtraClass('readonly');
805
        }
806
807
        return parent::extraClass();
808
    }
809
810
    public function Field($properties = array())
811
    {
812
        // Calculated config as per jquery.fileupload-ui.js
813
        $allowedMaxFileNumber = $this->getAllowedMaxFileNumber();
814
        $config = array(
815
            'url' => $this->Link('upload'),
816
            'urlSelectDialog' => $this->Link('select'),
817
            'urlAttach' => $this->Link('attach'),
818
            'urlFileExists' => $this->Link('fileexists'),
819
            'acceptFileTypes' => '.+$',
820
            // Fileupload treats maxNumberOfFiles as the max number of _additional_ items allowed
821
            'maxNumberOfFiles' => $allowedMaxFileNumber ? ($allowedMaxFileNumber - count($this->getItemIDs())) : null,
822
            'replaceFile' => $this->getUpload()->getReplaceFile(),
823
        );
824
825
        // Validation: File extensions
826
        if ($allowedExtensions = $this->getAllowedExtensions()) {
827
            $config['acceptFileTypes'] = '(\.|\/)(' . implode('|', $allowedExtensions) . ')$';
828
            $config['errorMessages']['acceptFileTypes'] = _t(
829
                'File.INVALIDEXTENSIONSHORT',
830
                'Extension is not allowed'
831
            );
832
        }
833
834
        // Validation: File size
835
        if ($allowedMaxFileSize = $this->getValidator()->getAllowedMaxFileSize()) {
836
            $config['maxFileSize'] = $allowedMaxFileSize;
837
            $config['errorMessages']['maxFileSize'] = _t(
838
                'File.TOOLARGESHORT',
839
                'File size exceeds {size}',
840
                array('size' => File::format_size($config['maxFileSize']))
841
            );
842
        }
843
844
        // Validation: Number of files
845
        if ($allowedMaxFileNumber) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $allowedMaxFileNumber of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

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
Loading history...
846
            if ($allowedMaxFileNumber > 1) {
847
                $config['errorMessages']['maxNumberOfFiles'] = _t(
848
                    'UploadField.MAXNUMBEROFFILESSHORT',
849
                    'Can only upload {count} files',
850
                    array('count' => $allowedMaxFileNumber)
851
                );
852
            } else {
853
                $config['errorMessages']['maxNumberOfFiles'] = _t(
854
                    'UploadField.MAXNUMBEROFFILESONE',
855
                    'Can only upload one file'
856
                );
857
            }
858
        }
859
860
        // add overwrite warning error message to the config object sent to Javascript
861
        if ($this->getOverwriteWarning()) {
862
            $config['errorMessages']['overwriteWarning'] =
863
                _t('UploadField.OVERWRITEWARNING', 'File with the same name already exists');
864
        }
865
866
        $mergedConfig = array_merge($config, $this->ufConfig);
867
        return parent::Field(array(
868
            'configString' => Convert::raw2json($mergedConfig),
869
            'config' => new ArrayData($mergedConfig),
870
            'multiple' => $allowedMaxFileNumber !== 1
871
        ));
872
    }
873
874
    /**
875
     * Validation method for this field, called when the entire form is validated
876
     *
877
     * @param Validator $validator
878
     * @return boolean
879
     */
880
    public function validate($validator)
881
    {
882
        $name = $this->getName();
883
        $files = $this->getItems();
884
885
        // If there are no files then quit
886
        if ($files->count() == 0) {
887
            return true;
888
        }
889
890
        // Check max number of files
891
        $maxFiles = $this->getAllowedMaxFileNumber();
892
        if ($maxFiles && ($files->count() > $maxFiles)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $maxFiles of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

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
Loading history...
893
            $validator->validationError(
894
                $name,
895
                _t(
896
                    'UploadField.MAXNUMBEROFFILES',
897
                    'Max number of {count} file(s) exceeded.',
898
                    array('count' => $maxFiles)
899
                ),
900
                "validation"
901
            );
902
            return false;
903
        }
904
905
        // Revalidate each file against nested validator
906
        $this->upload->clearErrors();
907
        foreach ($files as $file) {
908
            // Generate $_FILES style file attribute array for upload validator
909
            $tmpFile = array(
910
                'name' => $file->Name,
911
                'type' => null, // Not used for type validation
912
                'size' => $file->AbsoluteSize,
913
                'tmp_name' => null, // Should bypass is_uploaded_file check
914
                'error' => UPLOAD_ERR_OK,
915
            );
916
            $this->upload->validate($tmpFile);
917
        }
918
919
        // Check all errors
920
        if ($errors = $this->upload->getErrors()) {
921
            foreach ($errors as $error) {
922
                $validator->validationError($name, $error, "validation");
923
            }
924
            return false;
925
        }
926
927
        return true;
928
    }
929
930
    /**
931
     * @param HTTPRequest $request
932
     * @return UploadField_ItemHandler
933
     */
934
    public function handleItem(HTTPRequest $request)
935
    {
936
        return $this->getItemHandler($request->param('ID'));
937
    }
938
939
    /**
940
     * @param int $itemID
941
     * @return UploadField_ItemHandler
942
     */
943
    public function getItemHandler($itemID)
944
    {
945
        return UploadField_ItemHandler::create($this, $itemID);
946
    }
947
948
    /**
949
     * @param HTTPRequest $request
950
     * @return UploadField_SelectHandler
951
     */
952
    public function handleSelect(HTTPRequest $request)
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
953
    {
954
        if (!$this->canAttachExisting()) {
955
            return $this->httpError(403);
956
        }
957
        return UploadField_SelectHandler::create($this, $this->getFolderName());
958
    }
959
960
    /**
961
     * Safely encodes the File object with all standard fields required
962
     * by the front end
963
     *
964
     * @param File|AssetContainer $file Object which contains a file
965
     * @return array Array encoded list of file attributes
966
     */
967
    protected function encodeFileAttributes(AssetContainer $file)
968
    {
969
        // Collect all output data.
970
        $customised =  $this->customiseFile($file);
971
        return array(
972
            'id' => $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?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
973
            'name' => basename($file->getFilename()),
974
            'url' => $file->getURL(),
975
            'thumbnail_url' => $customised->UploadFieldThumbnailURL,
976
            'edit_url' => $customised->UploadFieldEditLink,
977
            'size' => $file->getAbsoluteSize(),
978
            'type' => File::get_file_type($file->getFilename()),
979
            'buttons' => (string)$customised->UploadFieldFileButtons,
980
            'fieldname' => $this->getName()
981
        );
982
    }
983
984
    /**
985
     * Action to handle upload of a single file
986
     *
987
     * @param HTTPRequest $request
988
     * @return HTTPResponse
989
     * @return HTTPResponse
990
     */
991
    public function upload(HTTPRequest $request)
992
    {
993
        if ($this->isDisabled() || $this->isReadonly() || !$this->canUpload()) {
994
            return $this->httpError(403);
995
        }
996
997
        // Protect against CSRF on destructive action
998
        $token = $this->getForm()->getSecurityToken();
999
        if (!$token->checkRequest($request)) {
1000
            return $this->httpError(400);
1001
        }
1002
1003
        // Get form details
1004
        $name = $this->getName();
1005
        $postVars = $request->postVar($name);
1006
1007
        // Extract uploaded files from Form data
1008
        $uploadedFiles = $this->extractUploadedFileData($postVars);
1009
        $return = array();
1010
1011
        // Save the temporary files into a File objects
1012
        // and save data/error on a per file basis
1013
        foreach ($uploadedFiles as $tempFile) {
1014
            $file = $this->saveTemporaryFile($tempFile, $error);
1015
            if (empty($file)) {
1016
                array_push($return, array('error' => $error));
1017
            } else {
1018
                array_push($return, $this->encodeFileAttributes($file));
1019
            }
1020
            $this->upload->clearErrors();
1021
        }
1022
1023
        // Format response with json
1024
        $response = new HTTPResponse(Convert::raw2json($return));
1025
        $response->addHeader('Content-Type', 'text/plain');
1026
        return $response;
1027
    }
1028
1029
    /**
1030
     * Retrieves details for files that this field wishes to attache to the
1031
     * client-side form
1032
     *
1033
     * @param HTTPRequest $request
1034
     * @return HTTPResponse
1035
     */
1036
    public function attach(HTTPRequest $request)
1037
    {
1038
        if (!$request->isPOST()) {
1039
            return $this->httpError(403);
1040
        }
1041
        if (!$this->canAttachExisting()) {
1042
            return $this->httpError(403);
1043
        }
1044
1045
        // Retrieve file attributes required by front end
1046
        $return = array();
1047
        $files = File::get()->byIDs($request->postVar('ids'));
1048
        foreach ($files as $file) {
1049
            $return[] = $this->encodeFileAttributes($file);
1050
        }
1051
        $response = new HTTPResponse(Convert::raw2json($return));
1052
        $response->addHeader('Content-Type', 'application/json');
1053
        return $response;
1054
    }
1055
1056
    /**
1057
     * Check if file exists, both checking filtered filename and exact filename
1058
     *
1059
     * @param string $originalFile Filename
1060
     * @return bool
1061
     */
1062
    protected function checkFileExists($originalFile)
1063
    {
1064
1065
        // Check both original and safely filtered filename
1066
        $nameFilter = FileNameFilter::create();
1067
        $filteredFile = $nameFilter->filter($originalFile);
1068
1069
        // Resolve expected folder name
1070
        $folderName = $this->getFolderName();
1071
        $folder = Folder::find_or_make($folderName);
1072
        $parentPath = $folder ? $folder->getFilename() : '';
1073
1074
        // check if either file exists
1075
        return File::find($parentPath.$originalFile) || File::find($parentPath.$filteredFile);
1076
    }
1077
1078
    /**
1079
     * Determines if a specified file exists
1080
     *
1081
     * @param HTTPRequest $request
1082
     * @return HTTPResponse
1083
     */
1084
    public function fileexists(HTTPRequest $request)
1085
    {
1086
        // Assert that requested filename doesn't attempt to escape the directory
1087
        $originalFile = $request->requestVar('filename');
1088
        if ($originalFile !== basename($originalFile)) {
1089
            $return = array(
1090
                'error' => _t('File.NOVALIDUPLOAD', 'File is not a valid upload')
1091
            );
1092
        } else {
1093
            $return = array(
1094
                'exists' => $this->checkFileExists($originalFile)
1095
            );
1096
        }
1097
1098
        // Encode and present response
1099
        $response = new HTTPResponse(Convert::raw2json($return));
1100
        $response->addHeader('Content-Type', 'application/json');
1101
        if (!empty($return['error'])) {
1102
            $response->setStatusCode(400);
1103
        }
1104
        return $response;
1105
    }
1106
1107
    public function performReadonlyTransformation()
1108
    {
1109
        $clone = clone $this;
1110
        $clone->addExtraClass('readonly');
1111
        $clone->setReadonly(true);
1112
        return $clone;
1113
    }
1114
}
1115