Completed
Push — webpack ( 4abc02...0465cf )
by Sam
07:36
created

UploadField::Field()   B

Complexity

Conditions 7
Paths 48

Size

Total Lines 62
Code Lines 39

Duplication

Lines 15
Ratio 24.19 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 7
eloc 39
c 2
b 0
f 0
nc 48
nop 1
dl 15
loc 62
rs 7.3333

How to fix   Long Method   

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

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
227
228
		// TODO thats the first thing that came to my head, feel free to change it
229
		$this->addExtraClass('ss-upload'); // class, used by js
230
		$this->addExtraClass('ss-uploadfield'); // class, used by css for uploadfield only
231
232
		$this->ufConfig = self::config()->defaultConfig;
0 ignored issues
show
Documentation introduced by
The property defaultConfig does not exist on object<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...
233
234
		parent::__construct($name, $title);
235
236
		if($items) $this->setItems($items);
237
238
		// filter out '' since this would be a regex problem on JS end
239
		$this->getValidator()->setAllowedExtensions(
240
			array_filter(Config::inst()->get('File', 'allowed_extensions'))
241
		);
242
243
		// get the lower max size
244
		$maxUpload = File::ini2bytes(ini_get('upload_max_filesize'));
245
		$maxPost = File::ini2bytes(ini_get('post_max_size'));
246
		$this->getValidator()->setAllowedMaxFileSize(min($maxUpload, $maxPost));
247
	}
248
249
	/**
250
	 * Set name of template used for Buttons on each file (replace, edit, remove, delete) (without path or extension)
251
	 *
252
	 * @param string $template
253
	 * @return $this
254
	 */
255
	public function setTemplateFileButtons($template) {
256
		$this->templateFileButtons = $template;
257
		return $this;
258
	}
259
260
	/**
261
	 * @return string
262
	 */
263
	public function getTemplateFileButtons() {
264
		return $this->templateFileButtons;
265
	}
266
267
	/**
268
	 * Set name of template used for the edit (inline & popup) of a file file (without path or extension)
269
	 *
270
	 * @param string $template
271
	 * @return $this
272
	 */
273
	public function setTemplateFileEdit($template) {
274
		$this->templateFileEdit = $template;
275
		return $this;
276
	}
277
278
	/**
279
	 * @return string
280
	 */
281
	public function getTemplateFileEdit() {
282
		return $this->templateFileEdit;
283
	}
284
285
	/**
286
	 * Determine if the target folder for new uploads in is visible the field UI.
287
	 *
288
	 * @return boolean
289
	 */
290
	public function canPreviewFolder() {
291
		if(!$this->isActive()) return false;
292
		$can = $this->getConfig('canPreviewFolder');
293
		return (is_bool($can)) ? $can : Permission::check($can);
294
	}
295
296
	/**
297
	 * Determine if the target folder for new uploads in is visible the field UI.
298
	 * Disable to keep the internal filesystem structure hidden from users.
299
	 *
300
	 * @param boolean|string $canPreviewFolder Either a boolean flag, or a
301
	 * required permission code
302
	 * @return UploadField Self reference
303
	 */
304
	public function setCanPreviewFolder($canPreviewFolder) {
305
		return $this->setConfig('canPreviewFolder', $canPreviewFolder);
306
	}
307
308
	/**
309
	 * Determine if the field should show a warning when overwriting a file.
310
	 * This requires Upload->replaceFile config to be set to true, otherwise
311
	 * files will be renamed instead of overwritten (although the warning will
312
	 * still be displayed)
313
	 *
314
	 * @return boolean
315
	 */
316
	public function getOverwriteWarning() {
317
		return $this->getConfig('overwriteWarning');
318
	}
319
320
	/**
321
	 * Determine if the field should show a warning when overwriting a file.
322
	 * This requires Upload->replaceFile config to be set to true, otherwise
323
	 * files will be renamed instead of overwritten (although the warning will
324
	 * still be displayed)
325
	 *
326
	 * @param boolean $overwriteWarning
327
	 * @return UploadField Self reference
328
	 */
329
	public function setOverwriteWarning($overwriteWarning) {
330
		return $this->setConfig('overwriteWarning', $overwriteWarning);
331
	}
332
333
	/**
334
	 * @param string $name
335
	 * @return $this
336
	 */
337
	public function setDisplayFolderName($name) {
338
		$this->displayFolderName = $name;
339
		return $this;
340
	}
341
342
	/**
343
	 * @return String
344
	 */
345
	public function getDisplayFolderName() {
346
		return $this->displayFolderName;
347
	}
348
349
	/**
350
	 * Force a record to be used as "Parent" for uploaded Files (eg a Page with a has_one to File)
351
	 *
352
	 * @param DataObject $record
353
	 * @return $this
354
	 */
355
	public function setRecord($record) {
356
		$this->record = $record;
357
		return $this;
358
	}
359
	/**
360
	 * Get the record to use as "Parent" for uploaded Files (eg a Page with a has_one to File) If none is set, it will
361
	 * use Form->getRecord() or Form->Controller()->data()
362
	 *
363
	 * @return DataObject
364
	 */
365
	public function getRecord() {
366
		if (!$this->record && $this->form) {
367
			if (($record = $this->form->getRecord()) && ($record instanceof DataObject)) {
368
				$this->record = $record;
369
			} elseif (($controller = $this->form->Controller())
0 ignored issues
show
Deprecated Code introduced by
The method Form::Controller() has been deprecated with message: 4.0

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
370
				&& $controller->hasMethod('data')
371
				&& ($record = $controller->data())
372
				&& ($record instanceof DataObject)
373
			) {
374
				$this->record = $record;
375
			}
376
		}
377
		return $this->record;
378
	}
379
380
	/**
381
	 * Loads the related record values into this field. UploadField can be uploaded
382
	 * in one of three ways:
383
	 *
384
	 *  - By passing in a list of file IDs in the $value parameter (an array with a single
385
	 *    key 'Files', with the value being the actual array of IDs).
386
	 *  - By passing in an explicit list of File objects in the $record parameter, and
387
	 *    leaving $value blank.
388
	 *  - By passing in a dataobject in the $record parameter, from which file objects
389
	 *    will be extracting using the field name as the relation field.
390
	 *
391
	 * Each of these methods will update both the items (list of File objects) and the
392
	 * field value (list of file ID values).
393
	 *
394
	 * @param array $value Array of submitted form data, if submitting from a form
395
	 * @param array|DataObject|SS_List $record Full source record, either as a DataObject,
396
	 * SS_List of items, or an array of submitted form data
397
	 * @return $this Self reference
398
	 * @throws ValidationException
399
	 */
400
	public function setValue($value, $record = null) {
401
402
		// If we're not passed a value directly, we can attempt to infer the field
403
		// value from the second parameter by inspecting its relations
404
		$items = new ArrayList();
405
406
		// Determine format of presented data
407
		if(empty($value) && $record) {
408
			// If a record is given as a second parameter, but no submitted values,
409
			// then we should inspect this instead for the form values
410
411
			if(($record instanceof DataObject) && $record->hasMethod($this->getName())) {
412
				// If given a dataobject use reflection to extract details
413
414
				$data = $record->{$this->getName()}();
415
				if($data instanceof DataObject) {
416
					// If has_one, add sole item to default list
417
					$items->push($data);
418
				} elseif($data instanceof SS_List) {
419
					// For many_many and has_many relations we can use the relation list directly
420
					$items = $data;
421
				}
422
			} elseif($record instanceof SS_List) {
423
				// If directly passing a list then save the items directly
424
				$items = $record;
425
			}
426
		} elseif(!empty($value['Files'])) {
427
			// If value is given as an array (such as a posted form), extract File IDs from this
428
			$class = $this->getRelationAutosetClass();
429
			$items = DataObject::get($class)->byIDs($value['Files']);
430
		}
431
432
		// If javascript is disabled, direct file upload (non-html5 style) can
433
		// trigger a single or multiple file submission. Note that this may be
434
		// included in addition to re-submitted File IDs as above, so these
435
		// should be added to the list instead of operated on independently.
436
		if($uploadedFiles = $this->extractUploadedFileData($value)) {
437
			foreach($uploadedFiles as $tempFile) {
438
				$file = $this->saveTemporaryFile($tempFile, $error);
439
				if($file) {
440
					$items->add($file);
441
				} else {
442
					throw new ValidationException($error);
443
				}
444
			}
445
		}
446
447
		// Filter items by what's allowed to be viewed
448
		$filteredItems = new ArrayList();
449
		$fileIDs = array();
450
		foreach($items as $file) {
451
			if($file->exists() && $file->canView()) {
452
				$filteredItems->push($file);
453
				$fileIDs[] = $file->ID;
454
			}
455
		}
456
457
		// Filter and cache updated item list
458
		$this->items = $filteredItems;
459
		// Same format as posted form values for this field. Also ensures that
460
		// $this->setValue($this->getValue()); is non-destructive
461
		$value = $fileIDs ? array('Files' => $fileIDs) : null;
462
463
		// Set value using parent
464
		return parent::setValue($value, $record);
0 ignored issues
show
Unused Code introduced by
The call to FileField::setValue() has too many arguments starting with $record.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
465
	}
466
467
	/**
468
	 * Sets the items assigned to this field as an SS_List of File objects.
469
	 * Calling setItems will also update the value of this field, as well as
470
	 * updating the internal list of File items.
471
	 *
472
	 * @param SS_List $items
473
	 * @return UploadField self reference
474
	 */
475
	public function setItems(SS_List $items) {
476
		return $this->setValue(null, $items);
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
477
	}
478
479
	/**
480
	 * Retrieves the current list of files
481
	 *
482
	 * @return SS_List
483
	 */
484
	public function getItems() {
485
		return $this->items ? $this->items : new ArrayList();
486
	}
487
488
	/**
489
	 * Retrieves a customised list of all File records to ensure they are
490
	 * properly viewable when rendered in the field template.
491
	 *
492
	 * @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...
493
	 */
494
	public function getCustomisedItems() {
495
		$customised = new ArrayList();
496
		foreach($this->getItems() as $file) {
497
			$customised->push($this->customiseFile($file));
498
		}
499
		return $customised;
500
	}
501
502
	/**
503
	 * Retrieves the list of selected file IDs
504
	 *
505
	 * @return array
506
	 */
507
	public function getItemIDs() {
508
		$value = $this->Value();
509
		return empty($value['Files']) ? array() : $value['Files'];
510
	}
511
512
	public function Value() {
513
		// Re-override FileField Value to use data value
514
		return $this->dataValue();
515
	}
516
517
	public function saveInto(DataObjectInterface $record) {
518
		// Check required relation details are available
519
		$fieldname = $this->getName();
520
		if(!$fieldname) return $this;
521
522
		// Get details to save
523
		$idList = $this->getItemIDs();
524
525
		// Check type of relation
526
		$relation = $record->hasMethod($fieldname) ? $record->$fieldname() : null;
527
		if($relation && ($relation instanceof RelationList || $relation instanceof UnsavedRelationList)) {
528
			// has_many or many_many
529
			$relation->setByIDList($idList);
530
		} elseif($record->hasOneComponent($fieldname)) {
531
			// has_one
532
			$record->{"{$fieldname}ID"} = $idList ? reset($idList) : 0;
533
		}
534
		return $this;
535
	}
536
537
	/**
538
	 * Customises a file with additional details suitable for rendering in the
539
	 * UploadField.ss template
540
	 *
541
	 * @param AssetContainer $file
542
	 * @return ViewableData_Customised
543
	 */
544
	protected function customiseFile(AssetContainer $file) {
545
		$file = $file->customise(array(
546
			'UploadFieldThumbnailURL' => $this->getThumbnailURLForFile($file),
547
			'UploadFieldDeleteLink' => $this->getItemHandler($file->ID)->DeleteLink(),
0 ignored issues
show
Bug introduced by
Accessing ID on the interface SilverStripe\Filesystem\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...
548
			'UploadFieldEditLink' => $this->getItemHandler($file->ID)->EditLink(),
0 ignored issues
show
Bug introduced by
Accessing ID on the interface SilverStripe\Filesystem\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...
549
			'UploadField' => $this
550
		));
551
		// we do this in a second customise to have the access to the previous customisations
552
		return $file->customise(array(
553
			'UploadFieldFileButtons' => $file->renderWith($this->getTemplateFileButtons())
554
		));
555
	}
556
557
	/**
558
	 * Assign a front-end config variable for the upload field
559
	 *
560
	 * @see https://github.com/blueimp/jQuery-File-Upload/wiki/Options for the list of front end options available
561
	 *
562
	 * @param string $key
563
	 * @param mixed $val
564
	 * @return UploadField self reference
565
	 */
566
	public function setConfig($key, $val) {
567
		$this->ufConfig[$key] = $val;
568
		return $this;
569
	}
570
571
	/**
572
	 * Gets a front-end config variable for the upload field
573
	 *
574
	 * @see https://github.com/blueimp/jQuery-File-Upload/wiki/Options for the list of front end options available
575
	 *
576
	 * @param string $key
577
	 * @return mixed
578
	 */
579
	public function getConfig($key) {
580
		if(!isset($this->ufConfig[$key])) return null;
581
		return $this->ufConfig[$key];
582
	}
583
584
	/**
585
	 * Determine if the field should automatically upload the file.
586
	 *
587
	 * @return boolean
588
	 */
589
	public function getAutoUpload() {
590
		return $this->getConfig('autoUpload');
591
	}
592
593
	/**
594
	 * Determine if the field should automatically upload the file
595
	 *
596
	 * @param boolean $autoUpload
597
	 * @return UploadField Self reference
598
	 */
599
	public function setAutoUpload($autoUpload) {
600
		return $this->setConfig('autoUpload', $autoUpload);
601
	}
602
603
	/**
604
	 * Determine maximum number of files allowed to be attached
605
	 * Defaults to 1 for has_one and null (unlimited) for
606
	 * many_many and has_many relations.
607
	 *
608
	 * @return integer|null Maximum limit, or null for no limit
609
	 */
610
	public function getAllowedMaxFileNumber() {
611
		$allowedMaxFileNumber = $this->getConfig('allowedMaxFileNumber');
612
613
		// if there is a has_one relation with that name on the record and
614
		// allowedMaxFileNumber has not been set, it's wanted to be 1
615
		if(empty($allowedMaxFileNumber)) {
616
			$record = $this->getRecord();
617
			$name = $this->getName();
618
			if($record && $record->hasOneComponent($name)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $record->hasOneComponent($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...
619
				return 1; // Default for has_one
620
			} else {
621
				return null; // Default for has_many and many_many
622
			}
623
		} else {
624
			return $allowedMaxFileNumber;
625
		}
626
	}
627
628
	/**
629
	 * Determine maximum number of files allowed to be attached.
630
	 *
631
	 * @param integer|null $allowedMaxFileNumber Maximum limit. 0 or null will be treated as unlimited
632
	 * @return UploadField Self reference
633
	 */
634
	public function setAllowedMaxFileNumber($allowedMaxFileNumber) {
635
		return $this->setConfig('allowedMaxFileNumber', $allowedMaxFileNumber);
636
	}
637
638
	/**
639
	 * Determine if the user has permission to upload.
640
	 *
641
	 * @return boolean
642
	 */
643
	public function canUpload() {
644
		if(!$this->isActive()) return false;
645
		$can = $this->getConfig('canUpload');
646
		return (is_bool($can)) ? $can : Permission::check($can);
647
	}
648
649
	/**
650
	 * Specify whether the user can upload files.
651
	 * String values will be treated as required permission codes
652
	 *
653
	 * @param boolean|string $canUpload Either a boolean flag, or a required
654
	 * permission code
655
	 * @return UploadField Self reference
656
	 */
657
	public function setCanUpload($canUpload) {
658
		return $this->setConfig('canUpload', $canUpload);
659
	}
660
661
	/**
662
	 * Determine if the user has permission to attach existing files
663
	 * By default returns true if the user has the CMS_ACCESS_AssetAdmin permission
664
	 *
665
	 * @return boolean
666
	 */
667
	public function canAttachExisting() {
668
		if(!$this->isActive()) return false;
669
		$can = $this->getConfig('canAttachExisting');
670
		return (is_bool($can)) ? $can : Permission::check($can);
671
	}
672
673
	/**
674
	 * Returns true if the field is neither readonly nor disabled
675
	 *
676
	 * @return boolean
677
	 */
678
	public function isActive() {
679
		return !$this->isDisabled() && !$this->isReadonly();
680
	}
681
682
	/**
683
	 * Specify whether the user can attach existing files
684
	 * String values will be treated as required permission codes
685
	 *
686
	 * @param boolean|string $canAttachExisting Either a boolean flag, or a
687
	 * required permission code
688
	 * @return UploadField Self reference
689
	 */
690
	public function setCanAttachExisting($canAttachExisting) {
691
		return $this->setConfig('canAttachExisting', $canAttachExisting);
692
	}
693
694
	/**
695
	 * Gets thumbnail width. Defaults to 80
696
	 *
697
	 * @return integer
698
	 */
699
	public function getPreviewMaxWidth() {
700
		return $this->getConfig('previewMaxWidth');
701
	}
702
703
	/**
704
	 * @see UploadField::getPreviewMaxWidth()
705
	 *
706
	 * @param integer $previewMaxWidth
707
	 * @return UploadField Self reference
708
	 */
709
	public function setPreviewMaxWidth($previewMaxWidth) {
710
		return $this->setConfig('previewMaxWidth', $previewMaxWidth);
711
	}
712
713
	/**
714
	 * Gets thumbnail height. Defaults to 60
715
	 *
716
	 * @return integer
717
	 */
718
	public function getPreviewMaxHeight() {
719
		return $this->getConfig('previewMaxHeight');
720
	}
721
722
	/**
723
	 * @see UploadField::getPreviewMaxHeight()
724
	 *
725
	 * @param integer $previewMaxHeight
726
	 * @return UploadField Self reference
727
	 */
728
	public function setPreviewMaxHeight($previewMaxHeight) {
729
		return $this->setConfig('previewMaxHeight', $previewMaxHeight);
730
	}
731
732
	/**
733
	 * javascript template used to display uploading files
734
	 * Defaults to 'ss-uploadfield-uploadtemplate'
735
	 *
736
	 * @see javascript/UploadField_uploadtemplate.js
737
	 * @return string
738
	 */
739
	public function getUploadTemplateName() {
740
		return $this->getConfig('uploadTemplateName');
741
	}
742
743
	/**
744
	 * @see UploadField::getUploadTemplateName()
745
	 *
746
	 * @param string $uploadTemplateName
747
	 * @return UploadField Self reference
748
	 */
749
	public function setUploadTemplateName($uploadTemplateName) {
750
		return $this->setConfig('uploadTemplateName', $uploadTemplateName);
751
	}
752
753
	/**
754
	 * javascript template used to display already uploaded files
755
	 * Defaults to 'ss-downloadfield-downloadtemplate'
756
	 *
757
	 * @see javascript/DownloadField_downloadtemplate.js
758
	 * @return string
759
	 */
760
	public function getDownloadTemplateName() {
761
		return $this->getConfig('downloadTemplateName');
762
	}
763
764
	/**
765
	 * @see Uploadfield::getDownloadTemplateName()
766
	 *
767
	 * @param string $downloadTemplateName
768
	 * @return Uploadfield Self reference
769
	 */
770
	public function setDownloadTemplateName($downloadTemplateName) {
771
		return $this->setConfig('downloadTemplateName', $downloadTemplateName);
772
	}
773
774
	/**
775
	 * FieldList $fields for the EditForm
776
	 * @example 'getCMSFields'
777
	 *
778
	 * @param DataObject $file File context to generate fields for
779
	 * @return FieldList List of form fields
780
	 */
781
	public function getFileEditFields(DataObject $file) {
782
		// Empty actions, generate default
783
		if(empty($this->fileEditFields)) {
784
			$fields = $file->getCMSFields();
785
			// Only display main tab, to avoid overly complex interface
786
			if($fields->hasTabSet() && ($mainTab = $fields->findOrMakeTab('Root.Main'))) {
787
				$fields = $mainTab->Fields();
788
			}
789
			return $fields;
790
		}
791
792
		// Fields instance
793
		if ($this->fileEditFields instanceof FieldList) {
794
			return $this->fileEditFields;
795
		}
796
797
		// Method to call on the given file
798
		if($file->hasMethod($this->fileEditFields)) {
799
			return $file->{$this->fileEditFields}();
800
		}
801
802
		user_error("Invalid value for UploadField::fileEditFields", E_USER_ERROR);
803
	}
804
805
	/**
806
	 * FieldList $fields or string $name (of a method on File to provide a fields) for the EditForm
807
	 * @example 'getCMSFields'
808
	 *
809
	 * @param FieldList|string
810
	 * @return Uploadfield Self reference
811
	 */
812
	public function setFileEditFields($fileEditFields) {
813
		$this->fileEditFields = $fileEditFields;
814
		return $this;
815
	}
816
817
	/**
818
	 * FieldList $actions or string $name (of a method on File to provide a actions) for the EditForm
819
	 * @example 'getCMSActions'
820
	 *
821
	 * @param DataObject $file File context to generate form actions for
822
	 * @return FieldList Field list containing FormAction
823
	 */
824
	public function getFileEditActions(DataObject $file) {
825
		// Empty actions, generate default
826
		if(empty($this->fileEditActions)) {
827
			$actions = new FieldList($saveAction = new FormAction('doEdit', _t('UploadField.DOEDIT', 'Save')));
828
			$saveAction->addExtraClass('ss-ui-action-constructive icon-accept');
829
			return $actions;
830
		}
831
832
		// Actions instance
833
		if ($this->fileEditActions instanceof FieldList) {
834
			return $this->fileEditActions;
835
		}
836
837
		// Method to call on the given file
838
		if($file->hasMethod($this->fileEditActions)) {
839
			return $file->{$this->fileEditActions}();
840
		}
841
842
		user_error("Invalid value for UploadField::fileEditActions", E_USER_ERROR);
843
	}
844
845
	/**
846
	 * FieldList $actions or string $name (of a method on File to provide a actions) for the EditForm
847
	 * @example 'getCMSActions'
848
	 *
849
	 * @param FieldList|string
850
	 * @return Uploadfield Self reference
851
	 */
852
	public function setFileEditActions($fileEditActions) {
853
		$this->fileEditActions = $fileEditActions;
854
		return $this;
855
	}
856
857
	/**
858
	 * Determines the validator to use for the edit form
859
	 * @example 'getCMSValidator'
860
	 *
861
	 * @param DataObject $file File context to generate validator from
862
	 * @return Validator Validator object
863
	 */
864
	public function getFileEditValidator(DataObject $file) {
865
		// Empty validator
866
		if(empty($this->fileEditValidator)) {
867
			return null;
868
		}
869
870
		// Validator instance
871
		if($this->fileEditValidator instanceof Validator) {
872
			return $this->fileEditValidator;
873
		}
874
875
		// Method to call on the given file
876
		if($file->hasMethod($this->fileEditValidator)) {
877
			return $file->{$this->fileEditValidator}();
878
		}
879
880
		user_error("Invalid value for UploadField::fileEditValidator", E_USER_ERROR);
881
	}
882
883
	/**
884
	 * Validator (eg RequiredFields) or string $name (of a method on File to provide a Validator) for the EditForm
885
	 * @example 'getCMSValidator'
886
	 *
887
	 * @param Validator|string
888
	 * @return Uploadfield Self reference
889
	 */
890
	public function setFileEditValidator($fileEditValidator) {
891
		$this->fileEditValidator = $fileEditValidator;
892
		return $this;
893
	}
894
895
	/**
896
	 *
897
	 * @param AssetContainer $file
898
	 * @return string URL to thumbnail
899
	 */
900
	protected function getThumbnailURLForFile(AssetContainer $file) {
901
		if (!$file->exists()) {
902
			return null;
903
		}
904
905
		// Attempt to generate image at given size
906
		$width = $this->getPreviewMaxWidth();
907
		$height = $this->getPreviewMaxHeight();
908
		if ($file->hasMethod('ThumbnailURL')) {
909
			return $file->ThumbnailURL($width, $height);
910
		}
911
		if ($file->hasMethod('Thumbnail')) {
912
			return $file->Thumbnail($width, $height)->getURL();
913
		}
914
		if ($file->hasMethod('Fit')) {
915
			return $file->Fit($width, $height)->getURL();
916
		}
917
918
		// Check if unsized icon is available
919
		if($file->hasMethod('getIcon')) {
920
			return $file->getIcon();
921
		}
922
	}
923
924
	public function getAttributes() {
925
		return array_merge(
926
			parent::getAttributes(),
927
			array('data-selectdialog-url', $this->Link('select'))
928
		);
929
	}
930
931
	public function extraClass() {
932
		if($this->isDisabled()) {
933
			$this->addExtraClass('disabled');
934
		}
935
		if($this->isReadonly()) {
936
			$this->addExtraClass('readonly');
937
		}
938
939
		return parent::extraClass();
940
	}
941
942
	public function Field($properties = array()) {
943
		// Calculated config as per jquery.fileupload-ui.js
944
		$allowedMaxFileNumber = $this->getAllowedMaxFileNumber();
945
		$config = array(
946
			'url' => $this->Link('upload'),
947
			'urlSelectDialog' => $this->Link('select'),
948
			'urlAttach' => $this->Link('attach'),
949
			'urlFileExists' => $this->link('fileexists'),
950
			'acceptFileTypes' => '.+$',
951
			// Fileupload treats maxNumberOfFiles as the max number of _additional_ items allowed
952
			'maxNumberOfFiles' => $allowedMaxFileNumber ? ($allowedMaxFileNumber - count($this->getItemIDs())) : null,
953
			'replaceFile' => $this->getUpload()->getReplaceFile(),
954
		);
955
956
		// Validation: File extensions
957 View Code Duplication
		if ($allowedExtensions = $this->getAllowedExtensions()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
958
			$config['acceptFileTypes'] = '(\.|\/)(' . implode('|', $allowedExtensions) . ')$';
959
			$config['errorMessages']['acceptFileTypes'] = _t(
960
				'File.INVALIDEXTENSIONSHORT',
961
				'Extension is not allowed'
962
			);
963
		}
964
965
		// Validation: File size
966 View Code Duplication
		if ($allowedMaxFileSize = $this->getValidator()->getAllowedMaxFileSize()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
967
			$config['maxFileSize'] = $allowedMaxFileSize;
968
			$config['errorMessages']['maxFileSize'] = _t(
969
				'File.TOOLARGESHORT',
970
				'File size exceeds {size}',
971
				array('size' => File::format_size($config['maxFileSize']))
0 ignored issues
show
Documentation introduced by
array('size' => \File::f...config['maxFileSize'])) is of type array<string,string,{"size":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
972
			);
973
		}
974
975
		// Validation: Number of files
976
		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...
977
			if($allowedMaxFileNumber > 1) {
978
				$config['errorMessages']['maxNumberOfFiles'] = _t(
979
					'UploadField.MAXNUMBEROFFILESSHORT',
980
					'Can only upload {count} files',
981
					array('count' => $allowedMaxFileNumber)
0 ignored issues
show
Documentation introduced by
array('count' => $allowedMaxFileNumber) is of type array<string,integer,{"count":"integer"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
982
				);
983
			} else {
984
				$config['errorMessages']['maxNumberOfFiles'] = _t(
985
					'UploadField.MAXNUMBEROFFILESONE',
986
					'Can only upload one file'
987
				);
988
			}
989
		}
990
991
		// add overwrite warning error message to the config object sent to Javascript
992
		if ($this->getOverwriteWarning()) {
993
			$config['errorMessages']['overwriteWarning'] =
994
				_t('UploadField.OVERWRITEWARNING', 'File with the same name already exists');
995
		}
996
997
		$mergedConfig = array_merge($config, $this->ufConfig);
998
		return parent::Field(array(
999
			'configString' => Convert::raw2json($mergedConfig),
1000
			'config' => new ArrayData($mergedConfig),
1001
			'multiple' => $allowedMaxFileNumber !== 1
1002
		));
1003
	}
1004
1005
	/**
1006
	 * Validation method for this field, called when the entire form is validated
1007
	 *
1008
	 * @param Validator $validator
1009
	 * @return boolean
1010
	 */
1011
	public function validate($validator) {
1012
		$name = $this->getName();
1013
		$files = $this->getItems();
1014
1015
		// If there are no files then quit
1016
		if($files->count() == 0) return true;
1017
1018
		// Check max number of files
1019
		$maxFiles = $this->getAllowedMaxFileNumber();
1020
		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...
1021
			$validator->validationError(
1022
				$name,
1023
				_t(
1024
					'UploadField.MAXNUMBEROFFILES',
1025
					'Max number of {count} file(s) exceeded.',
1026
					array('count' => $maxFiles)
0 ignored issues
show
Documentation introduced by
array('count' => $maxFiles) is of type array<string,integer,{"count":"integer"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1027
				),
1028
				"validation"
1029
			);
1030
			return false;
1031
		}
1032
1033
		// Revalidate each file against nested validator
1034
		$this->upload->clearErrors();
1035
		foreach($files as $file) {
1036
			// Generate $_FILES style file attribute array for upload validator
1037
			$tmpFile = array(
1038
				'name' => $file->Name,
1039
				'type' => null, // Not used for type validation
1040
				'size' => $file->AbsoluteSize,
1041
				'tmp_name' => null, // Should bypass is_uploaded_file check
1042
				'error' => UPLOAD_ERR_OK,
1043
			);
1044
			$this->upload->validate($tmpFile);
1045
		}
1046
1047
		// Check all errors
1048 View Code Duplication
		if($errors = $this->upload->getErrors()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1049
			foreach($errors as $error) {
1050
				$validator->validationError($name, $error, "validation");
1051
			}
1052
			return false;
1053
		}
1054
1055
		return true;
1056
	}
1057
1058
	/**
1059
	 * @param SS_HTTPRequest $request
1060
	 * @return UploadField_ItemHandler
1061
	 */
1062
	public function handleItem(SS_HTTPRequest $request) {
1063
		return $this->getItemHandler($request->param('ID'));
1064
	}
1065
1066
	/**
1067
	 * @param int $itemID
1068
	 * @return UploadField_ItemHandler
1069
	 */
1070
	public function getItemHandler($itemID) {
1071
		return UploadField_ItemHandler::create($this, $itemID);
1072
	}
1073
1074
	/**
1075
	 * @param SS_HTTPRequest $request
1076
	 * @return UploadField_ItemHandler
1077
	 */
1078
	public function handleSelect(SS_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...
1079
		if(!$this->canAttachExisting()) return $this->httpError(403);
1080
		return UploadField_SelectHandler::create($this, $this->getFolderName());
1081
	}
1082
1083
	/**
1084
	 * Given an array of post variables, extract all temporary file data into an array
1085
	 *
1086
	 * @param array $postVars Array of posted form data
1087
	 * @return array List of temporary file data
1088
	 */
1089
	protected function extractUploadedFileData($postVars) {
1090
1091
		// Note: Format of posted file parameters in php is a feature of using
1092
		// <input name='{$Name}[Uploads][]' /> for multiple file uploads
1093
		$tmpFiles = array();
1094
		if(	!empty($postVars['tmp_name'])
1095
			&& is_array($postVars['tmp_name'])
1096
			&& !empty($postVars['tmp_name']['Uploads'])
1097
		) {
1098
			for($i = 0; $i < count($postVars['tmp_name']['Uploads']); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
1099
				// Skip if "empty" file
1100
				if(empty($postVars['tmp_name']['Uploads'][$i])) continue;
1101
				$tmpFile = array();
1102 View Code Duplication
				foreach(array('name', 'type', 'tmp_name', 'error', 'size') as $field) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1103
					$tmpFile[$field] = $postVars[$field]['Uploads'][$i];
1104
				}
1105
				$tmpFiles[] = $tmpFile;
1106
			}
1107
		} elseif(!empty($postVars['tmp_name'])) {
1108
			// Fallback to allow single file uploads (method used by AssetUploadField)
1109
			$tmpFiles[] = $postVars;
1110
		}
1111
1112
		return $tmpFiles;
1113
	}
1114
1115
	/**
1116
	 * Loads the temporary file data into a File object
1117
	 *
1118
	 * @param array $tmpFile Temporary file data
1119
	 * @param string $error Error message
1120
	 * @return AssetContainer File object, or null if error
1121
	 */
1122
	protected function saveTemporaryFile($tmpFile, &$error = null) {
1123
		// Determine container object
1124
		$error = null;
1125
		$fileObject = null;
1126
1127
		if (empty($tmpFile)) {
1128
			$error = _t('UploadField.FIELDNOTSET', 'File information not found');
1129
			return null;
1130
		}
1131
1132
		if($tmpFile['error']) {
1133
			$error = $tmpFile['error'];
1134
			return null;
1135
		}
1136
1137
		// Search for relations that can hold the uploaded files, but don't fallback
1138
		// to default if there is no automatic relation
1139
		if ($relationClass = $this->getRelationAutosetClass(null)) {
1140
			// Allow File to be subclassed
1141
			if($relationClass === 'File' && isset($tmpFile['name'])) {
1142
				$relationClass = File::get_class_for_file_extension(
1143
					File::get_file_extension($tmpFile['name'])
1144
				);
1145
			}
1146
			// Create new object explicitly. Otherwise rely on Upload::load to choose the class.
1147
			$fileObject = Object::create($relationClass);
1148
			if(! ($fileObject instanceof DataObject) || !($fileObject instanceof AssetContainer)) {
1149
				throw new InvalidArgumentException("Invalid asset container $relationClass");
1150
			}
1151
		}
1152
1153
		// Get the uploaded file into a new file object.
1154
		try {
1155
			$this->upload->loadIntoFile($tmpFile, $fileObject, $this->getFolderName());
1156
		} catch (Exception $e) {
1157
			// we shouldn't get an error here, but just in case
1158
			$error = $e->getMessage();
1159
			return null;
1160
		}
1161
1162
		// Check if upload field has an error
1163 View Code Duplication
		if ($this->upload->isError()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1164
			$error = implode(' ' . PHP_EOL, $this->upload->getErrors());
1165
			return null;
1166
		}
1167
1168
		// return file
1169
		return $this->upload->getFile();
1170
	}
1171
1172
	/**
1173
	 * Safely encodes the File object with all standard fields required
1174
	 * by the front end
1175
	 *
1176
	 * @param AssetContainer $file Object which contains a file
1177
	 * @return array Array encoded list of file attributes
1178
	 */
1179
	protected function encodeFileAttributes(AssetContainer $file) {
1180
		// Collect all output data.
1181
		$customised =  $this->customiseFile($file);
1182
		return array(
1183
			'id' => $file->ID,
0 ignored issues
show
Bug introduced by
Accessing ID on the interface SilverStripe\Filesystem\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...
1184
			'name' => basename($file->getFilename()),
1185
			'url' => $file->getURL(),
1186
			'thumbnail_url' => $customised->UploadFieldThumbnailURL,
1187
			'edit_url' => $customised->UploadFieldEditLink,
1188
			'size' => $file->getAbsoluteSize(),
1189
			'type' => File::get_file_type($file->getFilename()),
1190
			'buttons' => (string)$customised->UploadFieldFileButtons,
1191
			'fieldname' => $this->getName()
1192
		);
1193
	}
1194
1195
	/**
1196
	 * Action to handle upload of a single file
1197
	 *
1198
	 * @param SS_HTTPRequest $request
1199
	 * @return SS_HTTPResponse
1200
	 * @return SS_HTTPResponse
1201
	 */
1202
	public function upload(SS_HTTPRequest $request) {
1203
		if($this->isDisabled() || $this->isReadonly() || !$this->canUpload()) {
1204
			return $this->httpError(403);
1205
		}
1206
1207
		// Protect against CSRF on destructive action
1208
		$token = $this->getForm()->getSecurityToken();
1209
		if(!$token->checkRequest($request)) return $this->httpError(400);
1210
1211
		// Get form details
1212
		$name = $this->getName();
1213
		$postVars = $request->postVar($name);
1214
1215
		// Extract uploaded files from Form data
1216
		$uploadedFiles = $this->extractUploadedFileData($postVars);
1217
		$return = array();
1218
1219
		// Save the temporary files into a File objects
1220
		// and save data/error on a per file basis
1221
		foreach ($uploadedFiles as $tempFile) {
1222
			$file = $this->saveTemporaryFile($tempFile, $error);
1223
			if(empty($file)) {
1224
				array_push($return, array('error' => $error));
1225
			} else {
1226
				array_push($return, $this->encodeFileAttributes($file));
1227
			}
1228
			$this->upload->clearErrors();
1229
		}
1230
1231
		// Format response with json
1232
		$response = new SS_HTTPResponse(Convert::raw2json($return));
1233
		$response->addHeader('Content-Type', 'text/plain');
1234
		return $response;
1235
	}
1236
1237
	/**
1238
	 * Retrieves details for files that this field wishes to attache to the
1239
	 * client-side form
1240
	 *
1241
	 * @param SS_HTTPRequest $request
1242
	 * @return SS_HTTPResponse
1243
	 */
1244
	public function attach(SS_HTTPRequest $request) {
1245
		if(!$request->isPOST()) return $this->httpError(403);
1246
		if(!$this->canAttachExisting()) return $this->httpError(403);
1247
1248
		// Retrieve file attributes required by front end
1249
		$return = array();
1250
		$files = File::get()->byIDs($request->postVar('ids'));
1251
		foreach($files as $file) {
1252
			$return[] = $this->encodeFileAttributes($file);
1253
		}
1254
		$response = new SS_HTTPResponse(Convert::raw2json($return));
1255
		$response->addHeader('Content-Type', 'application/json');
1256
		return $response;
1257
	}
1258
1259
	/**
1260
	 * Check if file exists, both checking filtered filename and exact filename
1261
	 *
1262
	 * @param string $originalFile Filename
1263
	 * @return bool
1264
	 */
1265
	protected function checkFileExists($originalFile) {
1266
1267
		// Check both original and safely filtered filename
1268
		$nameFilter = FileNameFilter::create();
1269
		$filteredFile = $nameFilter->filter($originalFile);
1270
1271
		// Resolve expected folder name
1272
		$folderName = $this->getFolderName();
1273
		$folder = Folder::find_or_make($folderName);
1274
		$parentPath = $folder ? $folder->getFilename() : '';
0 ignored issues
show
Bug introduced by
The method getFilename does only exist in Folder, but not in SilverStripe\ORM\DataObject.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1275
1276
		// check if either file exists
1277
		return File::find($parentPath.$originalFile) || File::find($parentPath.$filteredFile);
1278
	}
1279
1280
	/**
1281
	 * Determines if a specified file exists
1282
	 *
1283
	 * @param SS_HTTPRequest $request
1284
	 * @return SS_HTTPResponse
1285
	 */
1286
	public function fileexists(SS_HTTPRequest $request) {
1287
		// Assert that requested filename doesn't attempt to escape the directory
1288
		$originalFile = $request->requestVar('filename');
1289
		if($originalFile !== basename($originalFile)) {
1290
			$return = array(
1291
				'error' => _t('File.NOVALIDUPLOAD', 'File is not a valid upload')
1292
			);
1293
		} else {
1294
			$return = array(
1295
				'exists' => $this->checkFileExists($originalFile)
1296
			);
1297
		}
1298
1299
		// Encode and present response
1300
		$response = new SS_HTTPResponse(Convert::raw2json($return));
1301
		$response->addHeader('Content-Type', 'application/json');
1302
		if (!empty($return['error'])) $response->setStatusCode(400);
1303
		return $response;
1304
	}
1305
1306
	public function performReadonlyTransformation() {
1307
		$clone = clone $this;
1308
		$clone->addExtraClass('readonly');
1309
		$clone->setReadonly(true);
1310
		return $clone;
1311
	}
1312
1313
	/**
1314
	 * Gets the foreign class that needs to be created, or 'File' as default if there
1315
	 * is no relationship, or it cannot be determined.
1316
	 *
1317
	 * @param string $default Default value to return if no value could be calculated
1318
	 * @return string Foreign class name.
1319
	 */
1320 View Code Duplication
	public function getRelationAutosetClass($default = 'File') {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1321
1322
		// Don't autodetermine relation if no relationship between parent record
1323
		if(!$this->relationAutoSetting) return $default;
1324
1325
		// Check record and name
1326
		$name = $this->getName();
1327
		$record = $this->getRecord();
1328
		if(empty($name) || empty($record)) {
1329
			return $default;
1330
		} else {
1331
			$class = $record->getRelationClass($name);
1332
			return empty($class) ? $default : $class;
1333
		}
1334
	}
1335
1336
}
1337
1338
/**
1339
 * RequestHandler for actions (edit, remove, delete) on a single item (File) of the UploadField
1340
 *
1341
 * @author Zauberfisch
1342
 * @package forms
1343
 * @subpackages fields-files
1344
 */
1345
class UploadField_ItemHandler extends RequestHandler {
1346
1347
	/**
1348
	 * @var UploadFIeld
1349
	 */
1350
	protected $parent;
1351
1352
	/**
1353
	 * @var int FileID
1354
	 */
1355
	protected $itemID;
1356
1357
	private static $url_handlers = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
1358
		'$Action!' => '$Action',
1359
		'' => 'index',
1360
	);
1361
1362
	private static $allowed_actions = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
1363
		'delete',
1364
		'edit',
1365
		'EditForm',
1366
	);
1367
1368
	/**
1369
	 * @param UploadFIeld $parent
1370
	 * @param int $itemID
1371
	 */
1372
	public function __construct($parent, $itemID) {
1373
		$this->parent = $parent;
1374
		$this->itemID = $itemID;
1375
1376
		parent::__construct();
1377
	}
1378
1379
	/**
1380
	 * @return File
1381
	 */
1382
	public function getItem() {
1383
		return DataObject::get_by_id('File', $this->itemID);
1384
	}
1385
1386
	/**
1387
	 * @param string $action
1388
	 * @return string
1389
	 */
1390
	public function Link($action = null) {
1391
		return Controller::join_links($this->parent->Link(), '/item/', $this->itemID, $action);
1392
	}
1393
1394
	/**
1395
	 * @return string
1396
	 */
1397
	public function DeleteLink() {
1398
		$token = $this->parent->getForm()->getSecurityToken();
1399
		return $token->addToUrl($this->Link('delete'));
1400
	}
1401
1402
	/**
1403
	 * @return string
1404
	 */
1405
	public function EditLink() {
1406
		return $this->Link('edit');
1407
	}
1408
1409
	/**
1410
	 * Action to handle deleting of a single file
1411
	 *
1412
	 * @param SS_HTTPRequest $request
1413
	 * @return SS_HTTPResponse
1414
	 */
1415
	public function delete(SS_HTTPRequest $request) {
1416
		// Check form field state
1417
		if($this->parent->isDisabled() || $this->parent->isReadonly()) return $this->httpError(403);
1418
1419
		// Protect against CSRF on destructive action
1420
		$token = $this->parent->getForm()->getSecurityToken();
1421
		if(!$token->checkRequest($request)) return $this->httpError(400);
1422
1423
		// Check item permissions
1424
		$item = $this->getItem();
1425
		if(!$item) return $this->httpError(404);
1426
		if($item instanceof Folder) return $this->httpError(403);
1427
		if(!$item->canDelete()) return $this->httpError(403);
1428
1429
		// Delete the file from the filesystem. The file will be removed
1430
		// from the relation on save
1431
		// @todo Investigate if references to deleted files (if unsaved) is dangerous
1432
		$item->delete();
1433
	}
1434
1435
	/**
1436
	 * Action to handle editing of a single file
1437
	 *
1438
	 * @param SS_HTTPRequest $request
1439
	 * @return ViewableData_Customised
1440
	 */
1441
	public function edit(SS_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...
1442
		// Check form field state
1443
		if($this->parent->isDisabled() || $this->parent->isReadonly()) return $this->httpError(403);
1444
1445
		// Check item permissions
1446
		$item = $this->getItem();
1447
		if(!$item) return $this->httpError(404);
1448
		if($item instanceof Folder) return $this->httpError(403);
1449
		if(!$item->canEdit()) return $this->httpError(403);
1450
1451
		Requirements::css(FRAMEWORK_DIR . '/client/dist/styles/UploadField.css');
1452
1453
		return $this->customise(array(
1454
			'Form' => $this->EditForm()
1455
		))->renderWith($this->parent->getTemplateFileEdit());
1456
	}
1457
1458
	/**
1459
	 * @return Form
1460
	 */
1461
	public function EditForm() {
1462
		$file = $this->getItem();
1463
		if(!$file) return $this->httpError(404);
1464
		if($file instanceof Folder) return $this->httpError(403);
1465
		if(!$file->canEdit()) return $this->httpError(403);
1466
1467
		// Get form components
1468
		$fields = $this->parent->getFileEditFields($file);
1469
		$actions = $this->parent->getFileEditActions($file);
1470
		$validator = $this->parent->getFileEditValidator($file);
1471
		$form = new Form(
1472
			$this,
0 ignored issues
show
Documentation introduced by
$this is of type this<UploadField_ItemHandler>, but the function expects a object<Controller>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1473
			__FUNCTION__,
1474
			$fields,
1475
			$actions,
1476
			$validator
1477
		);
1478
		$form->loadDataFrom($file);
1479
		$form->addExtraClass('small');
1480
1481
		return $form;
1482
	}
1483
1484
	/**
1485
	 * @param array $data
1486
	 * @param Form $form
1487
	 * @param SS_HTTPRequest $request
1488
	 */
1489
	public function doEdit(array $data, Form $form, SS_HTTPRequest $request) {
0 ignored issues
show
Unused Code introduced by
The parameter $data 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...
1490
		// Check form field state
1491
		if($this->parent->isDisabled() || $this->parent->isReadonly()) return $this->httpError(403);
1492
1493
		// Check item permissions
1494
		$item = $this->getItem();
1495
		if(!$item) return $this->httpError(404);
1496
		if($item instanceof Folder) return $this->httpError(403);
1497
		if(!$item->canEdit()) return $this->httpError(403);
1498
1499
		$form->saveInto($item);
1500
		$item->write();
1501
1502
		$form->sessionMessage(_t('UploadField.Saved', 'Saved'), 'good');
1503
1504
		return $this->edit($request);
1505
	}
1506
1507
}
1508
1509
/**
1510
 * File selection popup for attaching existing files.
1511
 *
1512
 * @package forms
1513
 * @subpackages fields-files
1514
 */
1515
class UploadField_SelectHandler extends RequestHandler {
1516
1517
	/**
1518
	 * @var UploadField
1519
	 */
1520
	protected $parent;
1521
1522
	/**
1523
	 * @var string
1524
	 */
1525
	protected $folderName;
1526
1527
	/**
1528
	 * Set pagination quantity for file list field
1529
	 *
1530
	 * @config
1531
	 * @var int
1532
	 */
1533
	private static $page_size = 11;
1534
1535
	private static $url_handlers = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
1536
		'$Action!' => '$Action',
1537
		'' => 'index',
1538
	);
1539
1540
	private static $allowed_actions = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
1541
		'Form'
1542
	);
1543
1544
	public function __construct($parent, $folderName = null) {
1545
		$this->parent = $parent;
1546
		$this->folderName = $folderName;
1547
1548
		parent::__construct();
1549
	}
1550
1551
	public function index() {
1552
		// Requires a separate JS file, because we can't reach into the iframe with entwine.
1553
		Requirements::javascript(FRAMEWORK_DIR . '/client/dist/js/UploadField_select.js');
1554
		return $this->renderWith('SilverStripe\\Admin\\CMSDialog');
1555
	}
1556
1557
	/**
1558
	 * @param string $action
1559
	 * @return string
1560
	 */
1561
	public function Link($action = null) {
1562
		return Controller::join_links($this->parent->Link(), '/select/', $action);
1563
	}
1564
1565
	/**
1566
	 * Build the file selection form.
1567
	 *
1568
	 * @return Form
1569
	 */
1570
	public function Form() {
1571
		// Find out the requested folder ID.
1572
		$folderID = $this->parent->getRequest()->requestVar('ParentID');
1573
		if ($folderID === null && $this->parent->getDisplayFolderName()) {
1574
			$folder = Folder::find_or_make($this->parent->getDisplayFolderName());
1575
			$folderID = $folder ? $folder->ID : 0;
1576
		}
1577
1578
		// Construct the form
1579
		$action = new FormAction('doAttach', _t('UploadField.AttachFile', 'Attach file(s)'));
1580
		$action->addExtraClass('ss-ui-action-constructive icon-accept');
1581
		$form = new Form(
1582
			$this,
0 ignored issues
show
Documentation introduced by
$this is of type this<UploadField_SelectHandler>, but the function expects a object<Controller>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1583
			'Form',
1584
			new FieldList($this->getListField($folderID)),
1585
			new FieldList($action)
1586
		);
1587
1588
		// Add a class so we can reach the form from the frontend.
1589
		$form->addExtraClass('uploadfield-form');
1590
1591
		return $form;
1592
	}
1593
1594
	/**
1595
	 * @param int $folderID The ID of the folder to display.
1596
	 * @return FormField
1597
	 */
1598
	protected function getListField($folderID) {
1599
		// Generate the folder selection field.
1600
		$folderField = new TreeDropdownField('ParentID', _t('HTMLEditorField.FOLDER', 'Folder'), 'Folder');
1601
		$folderField->setValue($folderID);
1602
1603
		// Generate the file list field.
1604
		$config = GridFieldConfig::create();
1605
		$config->addComponent(new GridFieldSortableHeader());
1606
		$config->addComponent(new GridFieldFilterHeader());
1607
		$config->addComponent($colsComponent = new GridFieldDataColumns());
1608
		$colsComponent->setDisplayFields(array(
1609
			'StripThumbnail' => '',
1610
			'Title' => singleton('File')->fieldLabel('Title'),
1611
			'Created' => singleton('File')->fieldLabel('Created'),
1612
			'Size' => singleton('File')->fieldLabel('Size')
1613
		));
1614
		$colsComponent->setFieldCasting(array(
1615
			'Created' => 'DBDatetime->Nice'
1616
		));
1617
1618
 		// Set configurable pagination for file list field
1619
		$pageSize = Config::inst()->get(get_class($this), 'page_size');
1620
		$config->addComponent(new GridFieldPaginator($pageSize));
1621
1622
		// If relation is to be autoset, we need to make sure we only list compatible objects.
1623
		$baseClass = $this->parent->getRelationAutosetClass();
1624
1625
		// Create the data source for the list of files within the current directory.
1626
		$files = DataList::create($baseClass)->exclude('ClassName', 'Folder');
1627
		if($folderID) $files = $files->filter('ParentID', $folderID);
1628
1629
		$fileField = new GridField('Files', false, $files, $config);
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1630
		$fileField->setAttribute('data-selectable', true);
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1631
		if($this->parent->getAllowedMaxFileNumber() !== 1) {
1632
			$fileField->setAttribute('data-multiselect', true);
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1633
		}
1634
1635
		$selectComposite = new CompositeField(
1636
			$folderField,
1637
			$fileField
1638
		);
1639
1640
		return $selectComposite;
1641
	}
1642
1643
	public function doAttach($data, $form) {
0 ignored issues
show
Unused Code introduced by
The parameter $data 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...
Unused Code introduced by
The parameter $form 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...
1644
		// Popup-window attach does not require server side action, as it is implemented via JS
1645
	}
1646
1647
}
1648