Completed
Push — namespace-model ( 9b3f38...c67c40 )
by Sam
16:21 queued 05:15
created

UploadField_SelectHandler::doAttach()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

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

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
222
	 */
223 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...
224
225
		// TODO thats the first thing that came to my head, feel free to change it
226
		$this->addExtraClass('ss-upload'); // class, used by js
227
		$this->addExtraClass('ss-uploadfield'); // class, used by css for uploadfield only
228
229
		$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...
230
231
		parent::__construct($name, $title);
232
233
		if($items) $this->setItems($items);
234
235
		// filter out '' since this would be a regex problem on JS end
236
		$this->getValidator()->setAllowedExtensions(
237
			array_filter(Config::inst()->get('File', 'allowed_extensions'))
238
		);
239
240
		// get the lower max size
241
		$maxUpload = File::ini2bytes(ini_get('upload_max_filesize'));
242
		$maxPost = File::ini2bytes(ini_get('post_max_size'));
243
		$this->getValidator()->setAllowedMaxFileSize(min($maxUpload, $maxPost));
244
	}
245
246
	/**
247
	 * Set name of template used for Buttons on each file (replace, edit, remove, delete) (without path or extension)
248
	 *
249
	 * @param string
250
	 */
251
	public function setTemplateFileButtons($template) {
252
		$this->templateFileButtons = $template;
253
		return $this;
254
	}
255
256
	/**
257
	 * @return string
258
	 */
259
	public function getTemplateFileButtons() {
260
		return $this->templateFileButtons;
261
	}
262
263
	/**
264
	 * Set name of template used for the edit (inline & popup) of a file file (without path or extension)
265
	 *
266
	 * @param string
267
	 */
268
	public function setTemplateFileEdit($template) {
269
		$this->templateFileEdit = $template;
270
		return $this;
271
	}
272
273
	/**
274
	 * @return string
275
	 */
276
	public function getTemplateFileEdit() {
277
		return $this->templateFileEdit;
278
	}
279
280
	/**
281
	 * Determine if the target folder for new uploads in is visible the field UI.
282
	 *
283
	 * @return boolean
284
	 */
285
	public function canPreviewFolder() {
286
		if(!$this->isActive()) return false;
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
		return $this->setConfig('canPreviewFolder', $canPreviewFolder);
301
	}
302
303
	/**
304
	 * Determine if the field should show a warning when overwriting a file.
305
	 * This requires Upload->replaceFile config to be set to true, otherwise
306
	 * files will be renamed instead of overwritten (although the warning will
307
	 * still be displayed)
308
	 *
309
	 * @return boolean
310
	 */
311
	public function getOverwriteWarning() {
312
		return $this->getConfig('overwriteWarning');
313
	}
314
315
	/**
316
	 * Determine if the field should show a warning when overwriting a file.
317
	 * This requires Upload->replaceFile config to be set to true, otherwise
318
	 * files will be renamed instead of overwritten (although the warning will
319
	 * still be displayed)
320
	 *
321
	 * @param boolean $overwriteWarning
322
	 * @return UploadField Self reference
323
	 */
324
	public function setOverwriteWarning($overwriteWarning) {
325
		return $this->setConfig('overwriteWarning', $overwriteWarning);
326
	}
327
328
	/**
329
	 * @param String
330
	 */
331
	public function setDisplayFolderName($name) {
332
		$this->displayFolderName = $name;
333
		return $this;
334
	}
335
336
	/**
337
	 * @return String
338
	 */
339
	public function getDisplayFolderName() {
340
		return $this->displayFolderName;
341
	}
342
343
	/**
344
	 * Force a record to be used as "Parent" for uploaded Files (eg a Page with a has_one to File)
345
	 * @param DataObject $record
346
	 */
347
	public function setRecord($record) {
348
		$this->record = $record;
349
		return $this;
350
	}
351
	/**
352
	 * 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
353
	 * use Form->getRecord() or Form->Controller()->data()
354
	 *
355
	 * @return DataObject
356
	 */
357
	public function getRecord() {
358
		if (!$this->record && $this->form) {
359
			if (($record = $this->form->getRecord()) && ($record instanceof DataObject)) {
360
				$this->record = $record;
361
			} 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...
362
				&& $controller->hasMethod('data')
363
				&& ($record = $controller->data())
364
				&& ($record instanceof DataObject)
365
			) {
366
				$this->record = $record;
367
			}
368
		}
369
		return $this->record;
370
	}
371
372
	/**
373
	 * Loads the related record values into this field. UploadField can be uploaded
374
	 * in one of three ways:
375
	 *
376
	 *  - By passing in a list of file IDs in the $value parameter (an array with a single
377
	 *    key 'Files', with the value being the actual array of IDs).
378
	 *  - By passing in an explicit list of File objects in the $record parameter, and
379
	 *    leaving $value blank.
380
	 *  - By passing in a dataobject in the $record parameter, from which file objects
381
	 *    will be extracting using the field name as the relation field.
382
	 *
383
	 * Each of these methods will update both the items (list of File objects) and the
384
	 * field value (list of file ID values).
385
	 *
386
	 * @param array $value Array of submitted form data, if submitting from a form
387
	 * @param array|DataObject|SS_List $record Full source record, either as a DataObject,
388
	 * SS_List of items, or an array of submitted form data
389
	 * @return $this Self reference
390
	 */
391
	public function setValue($value, $record = null) {
392
393
		// If we're not passed a value directly, we can attempt to infer the field
394
		// value from the second parameter by inspecting its relations
395
		$items = new ArrayList();
396
397
		// Determine format of presented data
398
		if(empty($value) && $record) {
399
			// If a record is given as a second parameter, but no submitted values,
400
			// then we should inspect this instead for the form values
401
402
			if(($record instanceof DataObject) && $record->hasMethod($this->getName())) {
403
				// If given a dataobject use reflection to extract details
404
405
				$data = $record->{$this->getName()}();
406
				if($data instanceof DataObject) {
407
					// If has_one, add sole item to default list
408
					$items->push($data);
409
				} elseif($data instanceof SS_List) {
410
					// For many_many and has_many relations we can use the relation list directly
411
					$items = $data;
412
				}
413
			} elseif($record instanceof SS_List) {
414
				// If directly passing a list then save the items directly
415
				$items = $record;
416
			}
417
		} elseif(!empty($value['Files'])) {
418
			// If value is given as an array (such as a posted form), extract File IDs from this
419
			$class = $this->getRelationAutosetClass();
420
			$items = DataObject::get($class)->byIDs($value['Files']);
421
		}
422
423
		// If javascript is disabled, direct file upload (non-html5 style) can
424
		// trigger a single or multiple file submission. Note that this may be
425
		// included in addition to re-submitted File IDs as above, so these
426
		// should be added to the list instead of operated on independently.
427
		if($uploadedFiles = $this->extractUploadedFileData($value)) {
428
			foreach($uploadedFiles as $tempFile) {
429
				$file = $this->saveTemporaryFile($tempFile, $error);
430
				if($file) {
431
					$items->add($file);
432
				} else {
433
					throw new ValidationException($error);
434
				}
435
			}
436
		}
437
438
		// Filter items by what's allowed to be viewed
439
		$filteredItems = new ArrayList();
440
		$fileIDs = array();
441
		foreach($items as $file) {
442
			if($file->exists() && $file->canView()) {
443
				$filteredItems->push($file);
444
				$fileIDs[] = $file->ID;
445
			}
446
		}
447
448
		// Filter and cache updated item list
449
		$this->items = $filteredItems;
450
		// Same format as posted form values for this field. Also ensures that
451
		// $this->setValue($this->getValue()); is non-destructive
452
		$value = $fileIDs ? array('Files' => $fileIDs) : null;
453
454
		// Set value using parent
455
		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...
456
	}
457
458
	/**
459
	 * Sets the items assigned to this field as an SS_List of File objects.
460
	 * Calling setItems will also update the value of this field, as well as
461
	 * updating the internal list of File items.
462
	 *
463
	 * @param SS_List $items
464
	 * @return UploadField self reference
465
	 */
466
	public function setItems(SS_List $items) {
467
		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...
468
	}
469
470
	/**
471
	 * Retrieves the current list of files
472
	 *
473
	 * @return SS_List
474
	 */
475
	public function getItems() {
476
		return $this->items ? $this->items : new ArrayList();
477
	}
478
479
	/**
480
	 * Retrieves a customised list of all File records to ensure they are
481
	 * properly viewable when rendered in the field template.
482
	 *
483
	 * @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...
484
	 */
485
	public function getCustomisedItems() {
486
		$customised = new ArrayList();
487
		foreach($this->getItems() as $file) {
488
			$customised->push($this->customiseFile($file));
489
		}
490
		return $customised;
491
	}
492
493
	/**
494
	 * Retrieves the list of selected file IDs
495
	 *
496
	 * @return array
497
	 */
498
	public function getItemIDs() {
499
		$value = $this->Value();
500
		return empty($value['Files']) ? array() : $value['Files'];
501
	}
502
503
	public function Value() {
504
		// Re-override FileField Value to use data value
505
		return $this->dataValue();
506
	}
507
508
	public function saveInto(DataObjectInterface $record) {
509
		// Check required relation details are available
510
		$fieldname = $this->getName();
511
		if(!$fieldname) return $this;
512
513
		// Get details to save
514
		$idList = $this->getItemIDs();
515
516
		// Check type of relation
517
		$relation = $record->hasMethod($fieldname) ? $record->$fieldname() : null;
518
		if($relation && ($relation instanceof RelationList || $relation instanceof UnsavedRelationList)) {
519
			// has_many or many_many
520
			$relation->setByIDList($idList);
521
		} elseif($record->hasOneComponent($fieldname)) {
522
			// has_one
523
			$record->{"{$fieldname}ID"} = $idList ? reset($idList) : 0;
524
		}
525
		return $this;
526
	}
527
528
	/**
529
	 * Customises a file with additional details suitable for rendering in the
530
	 * UploadField.ss template
531
	 *
532
	 * @param AssetContainer $file
533
	 * @return ViewableData_Customised
534
	 */
535
	protected function customiseFile(AssetContainer $file) {
536
		$file = $file->customise(array(
537
			'UploadFieldThumbnailURL' => $this->getThumbnailURLForFile($file),
538
			'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...
539
			'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...
540
			'UploadField' => $this
541
		));
542
		// we do this in a second customise to have the access to the previous customisations
543
		return $file->customise(array(
544
			'UploadFieldFileButtons' => (string)$file->renderWith($this->getTemplateFileButtons())
545
		));
546
	}
547
548
	/**
549
	 * Assign a front-end config variable for the upload field
550
	 *
551
	 * @see https://github.com/blueimp/jQuery-File-Upload/wiki/Options for the list of front end options available
552
	 *
553
	 * @param string $key
554
	 * @param mixed $val
555
	 * @return UploadField self reference
556
	 */
557
	public function setConfig($key, $val) {
558
		$this->ufConfig[$key] = $val;
559
		return $this;
560
	}
561
562
	/**
563
	 * Gets a front-end config variable for the upload field
564
	 *
565
	 * @see https://github.com/blueimp/jQuery-File-Upload/wiki/Options for the list of front end options available
566
	 *
567
	 * @param string $key
568
	 * @return mixed
569
	 */
570
	public function getConfig($key) {
571
		if(!isset($this->ufConfig[$key])) return null;
572
		return $this->ufConfig[$key];
573
	}
574
575
	/**
576
	 * Determine if the field should automatically upload the file.
577
	 *
578
	 * @return boolean
579
	 */
580
	public function getAutoUpload() {
581
		return $this->getConfig('autoUpload');
582
	}
583
584
	/**
585
	 * Determine if the field should automatically upload the file
586
	 *
587
	 * @param boolean $autoUpload
588
	 * @return UploadField Self reference
589
	 */
590
	public function setAutoUpload($autoUpload) {
591
		return $this->setConfig('autoUpload', $autoUpload);
592
	}
593
594
	/**
595
	 * Determine maximum number of files allowed to be attached
596
	 * Defaults to 1 for has_one and null (unlimited) for
597
	 * many_many and has_many relations.
598
	 *
599
	 * @return integer|null Maximum limit, or null for no limit
600
	 */
601
	public function getAllowedMaxFileNumber() {
602
		$allowedMaxFileNumber = $this->getConfig('allowedMaxFileNumber');
603
604
		// if there is a has_one relation with that name on the record and
605
		// allowedMaxFileNumber has not been set, it's wanted to be 1
606
		if(empty($allowedMaxFileNumber)) {
607
			$record = $this->getRecord();
608
			$name = $this->getName();
609
			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...
610
				return 1; // Default for has_one
611
			} else {
612
				return null; // Default for has_many and many_many
613
			}
614
		} else {
615
			return $allowedMaxFileNumber;
616
		}
617
	}
618
619
	/**
620
	 * Determine maximum number of files allowed to be attached.
621
	 *
622
	 * @param integer|null $allowedMaxFileNumber Maximum limit. 0 or null will be treated as unlimited
623
	 * @return UploadField Self reference
624
	 */
625
	public function setAllowedMaxFileNumber($allowedMaxFileNumber) {
626
		return $this->setConfig('allowedMaxFileNumber', $allowedMaxFileNumber);
627
	}
628
629
	/**
630
	 * Determine if the user has permission to upload.
631
	 *
632
	 * @return boolean
633
	 */
634
	public function canUpload() {
635
		if(!$this->isActive()) return false;
636
		$can = $this->getConfig('canUpload');
637
		return (is_bool($can)) ? $can : Permission::check($can);
638
	}
639
640
	/**
641
	 * Specify whether the user can upload files.
642
	 * String values will be treated as required permission codes
643
	 *
644
	 * @param boolean|string $canUpload Either a boolean flag, or a required
645
	 * permission code
646
	 * @return UploadField Self reference
647
	 */
648
	public function setCanUpload($canUpload) {
649
		return $this->setConfig('canUpload', $canUpload);
650
	}
651
652
	/**
653
	 * Determine if the user has permission to attach existing files
654
	 * By default returns true if the user has the CMS_ACCESS_AssetAdmin permission
655
	 *
656
	 * @return boolean
657
	 */
658
	public function canAttachExisting() {
659
		if(!$this->isActive()) return false;
660
		$can = $this->getConfig('canAttachExisting');
661
		return (is_bool($can)) ? $can : Permission::check($can);
662
	}
663
664
	/**
665
	 * Returns true if the field is neither readonly nor disabled
666
	 *
667
	 * @return boolean
668
	 */
669
	public function isActive() {
670
		return !$this->isDisabled() && !$this->isReadonly();
671
	}
672
673
	/**
674
	 * Specify whether the user can attach existing files
675
	 * String values will be treated as required permission codes
676
	 *
677
	 * @param boolean|string $canAttachExisting Either a boolean flag, or a
678
	 * required permission code
679
	 * @return UploadField Self reference
680
	 */
681
	public function setCanAttachExisting($canAttachExisting) {
682
		return $this->setConfig('canAttachExisting', $canAttachExisting);
683
	}
684
685
	/**
686
	 * Gets thumbnail width. Defaults to 80
687
	 *
688
	 * @return integer
689
	 */
690
	public function getPreviewMaxWidth() {
691
		return $this->getConfig('previewMaxWidth');
692
	}
693
694
	/**
695
	 * @see UploadField::getPreviewMaxWidth()
696
	 *
697
	 * @param integer $previewMaxWidth
698
	 * @return UploadField Self reference
699
	 */
700
	public function setPreviewMaxWidth($previewMaxWidth) {
701
		return $this->setConfig('previewMaxWidth', $previewMaxWidth);
702
	}
703
704
	/**
705
	 * Gets thumbnail height. Defaults to 60
706
	 *
707
	 * @return integer
708
	 */
709
	public function getPreviewMaxHeight() {
710
		return $this->getConfig('previewMaxHeight');
711
	}
712
713
	/**
714
	 * @see UploadField::getPreviewMaxHeight()
715
	 *
716
	 * @param integer $previewMaxHeight
717
	 * @return UploadField Self reference
718
	 */
719
	public function setPreviewMaxHeight($previewMaxHeight) {
720
		return $this->setConfig('previewMaxHeight', $previewMaxHeight);
721
	}
722
723
	/**
724
	 * javascript template used to display uploading files
725
	 * Defaults to 'ss-uploadfield-uploadtemplate'
726
	 *
727
	 * @see javascript/UploadField_uploadtemplate.js
728
	 * @var string
729
	 */
730
	public function getUploadTemplateName() {
731
		return $this->getConfig('uploadTemplateName');
732
	}
733
734
	/**
735
	 * @see UploadField::getUploadTemplateName()
736
	 *
737
	 * @param string $uploadTemplateName
738
	 * @return UploadField Self reference
739
	 */
740
	public function setUploadTemplateName($uploadTemplateName) {
741
		return $this->setConfig('uploadTemplateName', $uploadTemplateName);
742
	}
743
744
	/**
745
	 * javascript template used to display already uploaded files
746
	 * Defaults to 'ss-downloadfield-downloadtemplate'
747
	 *
748
	 * @see javascript/DownloadField_downloadtemplate.js
749
	 * @var string
750
	 */
751
	public function getDownloadTemplateName() {
752
		return $this->getConfig('downloadTemplateName');
753
	}
754
755
	/**
756
	 * @see Uploadfield::getDownloadTemplateName()
757
	 *
758
	 * @param string $downloadTemplateName
759
	 * @return Uploadfield Self reference
760
	 */
761
	public function setDownloadTemplateName($downloadTemplateName) {
762
		return $this->setConfig('downloadTemplateName', $downloadTemplateName);
763
	}
764
765
	/**
766
	 * FieldList $fields for the EditForm
767
	 * @example 'getCMSFields'
768
	 *
769
	 * @param DataObject $file File context to generate fields for
770
	 * @return FieldList List of form fields
771
	 */
772
	public function getFileEditFields(DataObject $file) {
773
		// Empty actions, generate default
774
		if(empty($this->fileEditFields)) {
775
			$fields = $file->getCMSFields();
776
			// Only display main tab, to avoid overly complex interface
777
			if($fields->hasTabSet() && ($mainTab = $fields->findOrMakeTab('Root.Main'))) {
778
				$fields = $mainTab->Fields();
779
			}
780
			return $fields;
781
		}
782
783
		// Fields instance
784
		if ($this->fileEditFields instanceof FieldList) {
785
			return $this->fileEditFields;
786
		}
787
788
		// Method to call on the given file
789
		if($file->hasMethod($this->fileEditFields)) {
790
			return $file->{$this->fileEditFields}();
791
		}
792
793
		user_error("Invalid value for UploadField::fileEditFields", E_USER_ERROR);
794
	}
795
796
	/**
797
	 * FieldList $fields or string $name (of a method on File to provide a fields) for the EditForm
798
	 * @example 'getCMSFields'
799
	 *
800
	 * @param FieldList|string
801
	 * @return Uploadfield Self reference
802
	 */
803
	public function setFileEditFields($fileEditFields) {
804
		$this->fileEditFields = $fileEditFields;
805
		return $this;
806
	}
807
808
	/**
809
	 * FieldList $actions or string $name (of a method on File to provide a actions) for the EditForm
810
	 * @example 'getCMSActions'
811
	 *
812
	 * @param DataObject $file File context to generate form actions for
813
	 * @return FieldList Field list containing FormAction
814
	 */
815
	public function getFileEditActions(DataObject $file) {
816
		// Empty actions, generate default
817
		if(empty($this->fileEditActions)) {
818
			$actions = new FieldList($saveAction = new FormAction('doEdit', _t('UploadField.DOEDIT', 'Save')));
819
			$saveAction->addExtraClass('ss-ui-action-constructive icon-accept');
820
			return $actions;
821
		}
822
823
		// Actions instance
824
		if ($this->fileEditActions instanceof FieldList) {
825
			return $this->fileEditActions;
826
		}
827
828
		// Method to call on the given file
829
		if($file->hasMethod($this->fileEditActions)) {
830
			return $file->{$this->fileEditActions}();
831
		}
832
833
		user_error("Invalid value for UploadField::fileEditActions", E_USER_ERROR);
834
	}
835
836
	/**
837
	 * FieldList $actions or string $name (of a method on File to provide a actions) for the EditForm
838
	 * @example 'getCMSActions'
839
	 *
840
	 * @param FieldList|string
841
	 * @return Uploadfield Self reference
842
	 */
843
	public function setFileEditActions($fileEditActions) {
844
		$this->fileEditActions = $fileEditActions;
845
		return $this;
846
	}
847
848
	/**
849
	 * Determines the validator to use for the edit form
850
	 * @example 'getCMSValidator'
851
	 *
852
	 * @param DataObject $file File context to generate validator from
853
	 * @return Validator Validator object
854
	 */
855
	public function getFileEditValidator(DataObject $file) {
856
		// Empty validator
857
		if(empty($this->fileEditValidator)) {
858
			return null;
859
		}
860
861
		// Validator instance
862
		if($this->fileEditValidator instanceof Validator) {
863
			return $this->fileEditValidator;
864
		}
865
866
		// Method to call on the given file
867
		if($file->hasMethod($this->fileEditValidator)) {
868
			return $file->{$this->fileEditValidator}();
869
		}
870
871
		user_error("Invalid value for UploadField::fileEditValidator", E_USER_ERROR);
872
	}
873
874
	/**
875
	 * Validator (eg RequiredFields) or string $name (of a method on File to provide a Validator) for the EditForm
876
	 * @example 'getCMSValidator'
877
	 *
878
	 * @param Validator|string
879
	 * @return Uploadfield Self reference
880
	 */
881
	public function setFileEditValidator($fileEditValidator) {
882
		$this->fileEditValidator = $fileEditValidator;
883
		return $this;
884
	}
885
886
	/**
887
	 *
888
	 * @param AssetContainer $file
889
	 * @return string URL to thumbnail
890
	 */
891
	protected function getThumbnailURLForFile(AssetContainer $file) {
892
		if (!$file->exists()) {
893
			return null;
894
		}
895
896
		// Attempt to generate image at given size
897
		$width = $this->getPreviewMaxWidth();
898
		$height = $this->getPreviewMaxHeight();
899
		if ($file->hasMethod('ThumbnailURL')) {
900
			return $file->ThumbnailURL($width, $height);
901
		}
902
		if ($file->hasMethod('Thumbnail')) {
903
			return $file->Thumbnail($width, $height)->getURL();
904
		}
905
		if ($file->hasMethod('Fit')) {
906
			return $file->Fit($width, $height)->getURL();
907
		}
908
909
		// Check if unsized icon is available
910
		if($file->hasMethod('getIcon')) {
911
			return $file->getIcon();
912
		}
913
	}
914
915
	public function getAttributes() {
916
		return array_merge(
917
			parent::getAttributes(),
918
			array('data-selectdialog-url', $this->Link('select'))
919
		);
920
	}
921
922
	public function extraClass() {
923
		if($this->isDisabled()) {
924
			$this->addExtraClass('disabled');
925
		}
926
		if($this->isReadonly()) {
927
			$this->addExtraClass('readonly');
928
		}
929
930
		return parent::extraClass();
931
	}
932
933
	public function Field($properties = array()) {
934
		Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.js');
935
		Requirements::javascript(THIRDPARTY_DIR . '/jquery-ui/jquery-ui.js');
936
		Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js');
937
		Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/client/dist/js/ssui.core.js');
938
		Requirements::add_i18n_javascript(FRAMEWORK_DIR . '/javascript/lang');
939
940
		Requirements::combine_files('uploadfield.js', array(
941
			// @todo jquery templates is a project no longer maintained and should be retired at some point.
942
			THIRDPARTY_DIR . '/javascript-templates/tmpl.js',
943
			THIRDPARTY_DIR . '/javascript-loadimage/load-image.js',
944
			THIRDPARTY_DIR . '/jquery-fileupload/jquery.iframe-transport.js',
945
			THIRDPARTY_DIR . '/jquery-fileupload/cors/jquery.xdr-transport.js',
946
			THIRDPARTY_DIR . '/jquery-fileupload/jquery.fileupload.js',
947
			THIRDPARTY_DIR . '/jquery-fileupload/jquery.fileupload-ui.js',
948
			FRAMEWORK_DIR . '/client/dist/js/UploadField_uploadtemplate.js',
949
			FRAMEWORK_DIR . '/client/dist/js/UploadField_downloadtemplate.js',
950
			FRAMEWORK_DIR . '/client/dist/js/UploadField.js',
951
		));
952
		Requirements::css(THIRDPARTY_DIR . '/jquery-ui-themes/smoothness/jquery-ui.css'); // TODO hmmm, remove it?
953
		Requirements::css(FRAMEWORK_DIR . '/client/dist/styles/UploadField.css');
954
955
		// Calculated config as per jquery.fileupload-ui.js
956
		$allowedMaxFileNumber = $this->getAllowedMaxFileNumber();
957
		$config = array(
958
			'url' => $this->Link('upload'),
959
			'urlSelectDialog' => $this->Link('select'),
960
			'urlAttach' => $this->Link('attach'),
961
			'urlFileExists' => $this->link('fileexists'),
962
			'acceptFileTypes' => '.+$',
963
			// Fileupload treats maxNumberOfFiles as the max number of _additional_ items allowed
964
			'maxNumberOfFiles' => $allowedMaxFileNumber ? ($allowedMaxFileNumber - count($this->getItemIDs())) : null,
965
			'replaceFile' => $this->getUpload()->getReplaceFile(),
966
		);
967
968
		// Validation: File extensions
969 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...
970
			$config['acceptFileTypes'] = '(\.|\/)(' . implode('|', $allowedExtensions) . ')$';
971
			$config['errorMessages']['acceptFileTypes'] = _t(
972
				'File.INVALIDEXTENSIONSHORT',
973
				'Extension is not allowed'
974
			);
975
		}
976
977
		// Validation: File size
978 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...
979
			$config['maxFileSize'] = $allowedMaxFileSize;
980
			$config['errorMessages']['maxFileSize'] = _t(
981
				'File.TOOLARGESHORT',
982
				'Filesize exceeds {size}',
983
				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...
984
			);
985
		}
986
987
		// Validation: Number of files
988
		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...
989
			if($allowedMaxFileNumber > 1) {
990
				$config['errorMessages']['maxNumberOfFiles'] = _t(
991
					'UploadField.MAXNUMBEROFFILESSHORT',
992
					'Can only upload {count} files',
993
					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...
994
				);
995
			} else {
996
				$config['errorMessages']['maxNumberOfFiles'] = _t(
997
					'UploadField.MAXNUMBEROFFILESONE',
998
					'Can only upload one file'
999
				);
1000
			}
1001
		}
1002
1003
		// add overwrite warning error message to the config object sent to Javascript
1004
		if ($this->getOverwriteWarning()) {
1005
			$config['errorMessages']['overwriteWarning'] =
1006
				_t('UploadField.OVERWRITEWARNING', 'File with the same name already exists');
1007
		}
1008
1009
		$mergedConfig = array_merge($config, $this->ufConfig);
1010
		return $this->customise(array(
1011
			'configString' => str_replace('"', "&quot;", Convert::raw2json($mergedConfig)),
1012
			'config' => new ArrayData($mergedConfig),
1013
			'multiple' => $allowedMaxFileNumber !== 1
1014
		))->renderWith($this->getTemplates());
1015
	}
1016
1017
	/**
1018
	 * Validation method for this field, called when the entire form is validated
1019
	 *
1020
	 * @param Validator $validator
1021
	 * @return boolean
1022
	 */
1023
	public function validate($validator) {
1024
		$name = $this->getName();
1025
		$files = $this->getItems();
1026
1027
		// If there are no files then quit
1028
		if($files->count() == 0) return true;
1029
1030
		// Check max number of files
1031
		$maxFiles = $this->getAllowedMaxFileNumber();
1032
		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...
1033
			$validator->validationError(
1034
				$name,
1035
				_t(
1036
					'UploadField.MAXNUMBEROFFILES',
1037
					'Max number of {count} file(s) exceeded.',
1038
					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...
1039
				),
1040
				"validation"
1041
			);
1042
			return false;
1043
		}
1044
1045
		// Revalidate each file against nested validator
1046
		$this->upload->clearErrors();
1047
		foreach($files as $file) {
1048
			// Generate $_FILES style file attribute array for upload validator
1049
			$tmpFile = array(
1050
				'name' => $file->Name,
1051
				'type' => null, // Not used for type validation
1052
				'size' => $file->AbsoluteSize,
1053
				'tmp_name' => null, // Should bypass is_uploaded_file check
1054
				'error' => UPLOAD_ERR_OK,
1055
			);
1056
			$this->upload->validate($tmpFile);
1057
		}
1058
1059
		// Check all errors
1060 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...
1061
			foreach($errors as $error) {
1062
				$validator->validationError($name, $error, "validation");
1063
			}
1064
			return false;
1065
		}
1066
1067
		return true;
1068
	}
1069
1070
	/**
1071
	 * @param SS_HTTPRequest $request
1072
	 * @return UploadField_ItemHandler
1073
	 */
1074
	public function handleItem(SS_HTTPRequest $request) {
1075
		return $this->getItemHandler($request->param('ID'));
1076
	}
1077
1078
	/**
1079
	 * @param int $itemID
1080
	 * @return UploadField_ItemHandler
1081
	 */
1082
	public function getItemHandler($itemID) {
1083
		return UploadField_ItemHandler::create($this, $itemID);
1084
	}
1085
1086
	/**
1087
	 * @param SS_HTTPRequest $request
1088
	 * @return UploadField_ItemHandler
1089
	 */
1090
	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...
1091
		if(!$this->canAttachExisting()) return $this->httpError(403);
1092
		return UploadField_SelectHandler::create($this, $this->getFolderName());
1093
	}
1094
1095
	/**
1096
	 * Given an array of post variables, extract all temporary file data into an array
1097
	 *
1098
	 * @param array $postVars Array of posted form data
1099
	 * @return array List of temporary file data
1100
	 */
1101
	protected function extractUploadedFileData($postVars) {
1102
1103
		// Note: Format of posted file parameters in php is a feature of using
1104
		// <input name='{$Name}[Uploads][]' /> for multiple file uploads
1105
		$tmpFiles = array();
1106
		if(	!empty($postVars['tmp_name'])
1107
			&& is_array($postVars['tmp_name'])
1108
			&& !empty($postVars['tmp_name']['Uploads'])
1109
		) {
1110
			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...
1111
				// Skip if "empty" file
1112
				if(empty($postVars['tmp_name']['Uploads'][$i])) continue;
1113
				$tmpFile = array();
1114 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...
1115
					$tmpFile[$field] = $postVars[$field]['Uploads'][$i];
1116
				}
1117
				$tmpFiles[] = $tmpFile;
1118
			}
1119
		} elseif(!empty($postVars['tmp_name'])) {
1120
			// Fallback to allow single file uploads (method used by AssetUploadField)
1121
			$tmpFiles[] = $postVars;
1122
		}
1123
1124
		return $tmpFiles;
1125
	}
1126
1127
	/**
1128
	 * Loads the temporary file data into a File object
1129
	 *
1130
	 * @param array $tmpFile Temporary file data
1131
	 * @param string $error Error message
1132
	 * @return AssetContainer File object, or null if error
1133
	 */
1134
	protected function saveTemporaryFile($tmpFile, &$error = null) {
1135
		// Determine container object
1136
		$error = null;
1137
		$fileObject = null;
1138
1139
		if (empty($tmpFile)) {
1140
			$error = _t('UploadField.FIELDNOTSET', 'File information not found');
1141
			return null;
1142
		}
1143
1144
		if($tmpFile['error']) {
1145
			$error = $tmpFile['error'];
1146
			return null;
1147
		}
1148
1149
		// Search for relations that can hold the uploaded files, but don't fallback
1150
		// to default if there is no automatic relation
1151
		if ($relationClass = $this->getRelationAutosetClass(null)) {
1152
			// Allow File to be subclassed
1153
			if($relationClass === 'File' && isset($tmpFile['name'])) {
1154
				$relationClass = File::get_class_for_file_extension(
1155
					File::get_file_extension($tmpFile['name'])
1156
				);
1157
			}
1158
			// Create new object explicitly. Otherwise rely on Upload::load to choose the class.
1159
			$fileObject = Object::create($relationClass);
1160
			if(! ($fileObject instanceof DataObject) || !($fileObject instanceof AssetContainer)) {
1161
				throw new InvalidArgumentException("Invalid asset container $relationClass");
1162
			}
1163
		}
1164
1165
		// Get the uploaded file into a new file object.
1166
		try {
1167
			$this->upload->loadIntoFile($tmpFile, $fileObject, $this->getFolderName());
1168
		} catch (Exception $e) {
1169
			// we shouldn't get an error here, but just in case
1170
			$error = $e->getMessage();
1171
			return null;
1172
		}
1173
1174
		// Check if upload field has an error
1175 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...
1176
			$error = implode(' ' . PHP_EOL, $this->upload->getErrors());
1177
			return null;
1178
		}
1179
1180
		// return file
1181
		return $this->upload->getFile();
1182
	}
1183
1184
	/**
1185
	 * Safely encodes the File object with all standard fields required
1186
	 * by the front end
1187
	 *
1188
	 * @param AssetContainer $file Object which contains a file
1189
	 * @return array Array encoded list of file attributes
1190
	 */
1191
	protected function encodeFileAttributes(AssetContainer $file) {
1192
		// Collect all output data.
1193
		$customised =  $this->customiseFile($file);
1194
		return array(
1195
			'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...
1196
			'name' => basename($file->getFilename()),
1197
			'url' => $file->getURL(),
1198
			'thumbnail_url' => $customised->UploadFieldThumbnailURL,
1199
			'edit_url' => $customised->UploadFieldEditLink,
1200
			'size' => $file->getAbsoluteSize(),
1201
			'type' => File::get_file_type($file->getFilename()),
1202
			'buttons' => $customised->UploadFieldFileButtons,
1203
			'fieldname' => $this->getName()
1204
		);
1205
	}
1206
1207
	/**
1208
	 * Action to handle upload of a single file
1209
	 *
1210
	 * @param SS_HTTPRequest $request
1211
	 * @return SS_HTTPResponse
1212
	 * @return SS_HTTPResponse
1213
	 */
1214
	public function upload(SS_HTTPRequest $request) {
1215
		if($this->isDisabled() || $this->isReadonly() || !$this->canUpload()) {
1216
			return $this->httpError(403);
1217
		}
1218
1219
		// Protect against CSRF on destructive action
1220
		$token = $this->getForm()->getSecurityToken();
1221
		if(!$token->checkRequest($request)) return $this->httpError(400);
1222
1223
		// Get form details
1224
		$name = $this->getName();
1225
		$postVars = $request->postVar($name);
1226
1227
		// Extract uploaded files from Form data
1228
		$uploadedFiles = $this->extractUploadedFileData($postVars);
1229
		$return = array();
1230
1231
		// Save the temporary files into a File objects
1232
		// and save data/error on a per file basis
1233
		foreach ($uploadedFiles as $tempFile) {
1234
			$file = $this->saveTemporaryFile($tempFile, $error);
1235
			if(empty($file)) {
1236
				array_push($return, array('error' => $error));
1237
			} else {
1238
				array_push($return, $this->encodeFileAttributes($file));
1239
			}
1240
			$this->upload->clearErrors();
1241
		}
1242
1243
		// Format response with json
1244
		$response = new SS_HTTPResponse(Convert::raw2json($return));
1245
		$response->addHeader('Content-Type', 'text/plain');
1246
		return $response;
1247
	}
1248
1249
	/**
1250
	 * Retrieves details for files that this field wishes to attache to the
1251
	 * client-side form
1252
	 *
1253
	 * @param SS_HTTPRequest $request
1254
	 * @return SS_HTTPResponse
1255
	 */
1256
	public function attach(SS_HTTPRequest $request) {
1257
		if(!$request->isPOST()) return $this->httpError(403);
1258
		if(!$this->canAttachExisting()) return $this->httpError(403);
1259
1260
		// Retrieve file attributes required by front end
1261
		$return = array();
1262
		$files = File::get()->byIDs($request->postVar('ids'));
1263
		foreach($files as $file) {
1264
			$return[] = $this->encodeFileAttributes($file);
1265
		}
1266
		$response = new SS_HTTPResponse(Convert::raw2json($return));
1267
		$response->addHeader('Content-Type', 'application/json');
1268
		return $response;
1269
	}
1270
1271
	/**
1272
	 * Check if file exists, both checking filtered filename and exact filename
1273
	 *
1274
	 * @param string $originalFile Filename
1275
	 * @return bool
1276
	 */
1277
	protected function checkFileExists($originalFile) {
1278
1279
		// Check both original and safely filtered filename
1280
		$nameFilter = FileNameFilter::create();
1281
		$filteredFile = $nameFilter->filter($originalFile);
1282
1283
		// Resolve expected folder name
1284
		$folderName = $this->getFolderName();
1285
		$folder = Folder::find_or_make($folderName);
1286
		$parentPath = $folder ? $folder->getFilename() : '';
1287
1288
		// check if either file exists
1289
		return File::find($parentPath.$originalFile) || File::find($parentPath.$filteredFile);
1290
	}
1291
1292
	/**
1293
	 * Determines if a specified file exists
1294
	 *
1295
	 * @param SS_HTTPRequest $request
1296
	 */
1297
	public function fileexists(SS_HTTPRequest $request) {
1298
		// Assert that requested filename doesn't attempt to escape the directory
1299
		$originalFile = $request->requestVar('filename');
1300
		if($originalFile !== basename($originalFile)) {
1301
			$return = array(
1302
				'error' => _t('File.NOVALIDUPLOAD', 'File is not a valid upload')
1303
			);
1304
		} else {
1305
			$return = array(
1306
				'exists' => $this->checkFileExists($originalFile)
1307
			);
1308
		}
1309
1310
		// Encode and present response
1311
		$response = new SS_HTTPResponse(Convert::raw2json($return));
1312
		$response->addHeader('Content-Type', 'application/json');
1313
		if (!empty($return['error'])) $response->setStatusCode(400);
1314
		return $response;
1315
	}
1316
1317
	public function performReadonlyTransformation() {
1318
		$clone = clone $this;
1319
		$clone->addExtraClass('readonly');
1320
		$clone->setReadonly(true);
1321
		return $clone;
1322
	}
1323
1324
	/**
1325
	 * Gets the foreign class that needs to be created, or 'File' as default if there
1326
	 * is no relationship, or it cannot be determined.
1327
	 *
1328
	 * @param $default Default value to return if no value could be calculated
1329
	 * @return string Foreign class name.
1330
	 */
1331 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...
1332
1333
		// Don't autodetermine relation if no relationship between parent record
1334
		if(!$this->relationAutoSetting) return $default;
1335
1336
		// Check record and name
1337
		$name = $this->getName();
1338
		$record = $this->getRecord();
1339
		if(empty($name) || empty($record)) {
1340
			return $default;
1341
		} else {
1342
			$class = $record->getRelationClass($name);
1343
			return empty($class) ? $default : $class;
1344
		}
1345
	}
1346
1347
}
1348
1349
/**
1350
 * RequestHandler for actions (edit, remove, delete) on a single item (File) of the UploadField
1351
 *
1352
 * @author Zauberfisch
1353
 * @package forms
1354
 * @subpackages fields-files
1355
 */
1356
class UploadField_ItemHandler extends RequestHandler {
1357
1358
	/**
1359
	 * @var UploadFIeld
1360
	 */
1361
	protected $parent;
1362
1363
	/**
1364
	 * @var int FileID
1365
	 */
1366
	protected $itemID;
1367
1368
	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...
1369
		'$Action!' => '$Action',
1370
		'' => 'index',
1371
	);
1372
1373
	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...
1374
		'delete',
1375
		'edit',
1376
		'EditForm',
1377
	);
1378
1379
	/**
1380
	 * @param UploadFIeld $parent
1381
	 * @param int $itemID
1382
	 */
1383
	public function __construct($parent, $itemID) {
1384
		$this->parent = $parent;
1385
		$this->itemID = $itemID;
1386
1387
		parent::__construct();
1388
	}
1389
1390
	/**
1391
	 * @return File
1392
	 */
1393
	public function getItem() {
1394
		return DataObject::get_by_id('File', $this->itemID);
1395
	}
1396
1397
	/**
1398
	 * @param string $action
1399
	 * @return string
1400
	 */
1401
	public function Link($action = null) {
1402
		return Controller::join_links($this->parent->Link(), '/item/', $this->itemID, $action);
1403
	}
1404
1405
	/**
1406
	 * @return string
1407
	 */
1408
	public function DeleteLink() {
1409
		$token = $this->parent->getForm()->getSecurityToken();
1410
		return $token->addToUrl($this->Link('delete'));
1411
	}
1412
1413
	/**
1414
	 * @return string
1415
	 */
1416
	public function EditLink() {
1417
		return $this->Link('edit');
1418
	}
1419
1420
	/**
1421
	 * Action to handle deleting of a single file
1422
	 *
1423
	 * @param SS_HTTPRequest $request
1424
	 * @return SS_HTTPResponse
1425
	 */
1426
	public function delete(SS_HTTPRequest $request) {
1427
		// Check form field state
1428
		if($this->parent->isDisabled() || $this->parent->isReadonly()) return $this->httpError(403);
1429
1430
		// Protect against CSRF on destructive action
1431
		$token = $this->parent->getForm()->getSecurityToken();
1432
		if(!$token->checkRequest($request)) return $this->httpError(400);
1433
1434
		// Check item permissions
1435
		$item = $this->getItem();
1436
		if(!$item) return $this->httpError(404);
1437
		if($item instanceof Folder) return $this->httpError(403);
1438
		if(!$item->canDelete()) return $this->httpError(403);
1439
1440
		// Delete the file from the filesystem. The file will be removed
1441
		// from the relation on save
1442
		// @todo Investigate if references to deleted files (if unsaved) is dangerous
1443
		$item->delete();
1444
	}
1445
1446
	/**
1447
	 * Action to handle editing of a single file
1448
	 *
1449
	 * @param SS_HTTPRequest $request
1450
	 * @return ViewableData_Customised
1451
	 */
1452
	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...
1453
		// Check form field state
1454
		if($this->parent->isDisabled() || $this->parent->isReadonly()) return $this->httpError(403);
1455
1456
		// Check item permissions
1457
		$item = $this->getItem();
1458
		if(!$item) return $this->httpError(404);
1459
		if($item instanceof Folder) return $this->httpError(403);
1460
		if(!$item->canEdit()) return $this->httpError(403);
1461
1462
		Requirements::css(FRAMEWORK_DIR . '/client/dist/styles/UploadField.css');
1463
1464
		return $this->customise(array(
1465
			'Form' => $this->EditForm()
1466
		))->renderWith($this->parent->getTemplateFileEdit());
1467
	}
1468
1469
	/**
1470
	 * @return Form
1471
	 */
1472
	public function EditForm() {
1473
		$file = $this->getItem();
1474
		if(!$file) return $this->httpError(404);
1475
		if($file instanceof Folder) return $this->httpError(403);
1476
		if(!$file->canEdit()) return $this->httpError(403);
1477
1478
		// Get form components
1479
		$fields = $this->parent->getFileEditFields($file);
1480
		$actions = $this->parent->getFileEditActions($file);
1481
		$validator = $this->parent->getFileEditValidator($file);
1482
		$form = new Form(
1483
			$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...
1484
			__FUNCTION__,
1485
			$fields,
1486
			$actions,
1487
			$validator
1488
		);
1489
		$form->loadDataFrom($file);
1490
		$form->addExtraClass('small');
1491
1492
		return $form;
1493
	}
1494
1495
	/**
1496
	 * @param array $data
1497
	 * @param Form $form
1498
	 * @param SS_HTTPRequest $request
1499
	 */
1500
	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...
1501
		// Check form field state
1502
		if($this->parent->isDisabled() || $this->parent->isReadonly()) return $this->httpError(403);
1503
1504
		// Check item permissions
1505
		$item = $this->getItem();
1506
		if(!$item) return $this->httpError(404);
1507
		if($item instanceof Folder) return $this->httpError(403);
1508
		if(!$item->canEdit()) return $this->httpError(403);
1509
1510
		$form->saveInto($item);
1511
		$item->write();
1512
1513
		$form->sessionMessage(_t('UploadField.Saved', 'Saved'), 'good');
1514
1515
		return $this->edit($request);
1516
	}
1517
1518
}
1519
1520
/**
1521
 * File selection popup for attaching existing files.
1522
 *
1523
 * @package forms
1524
 * @subpackages fields-files
1525
 */
1526
class UploadField_SelectHandler extends RequestHandler {
1527
1528
	/**
1529
	 * @var UploadField
1530
	 */
1531
	protected $parent;
1532
1533
	/**
1534
	 * @var string
1535
	 */
1536
	protected $folderName;
1537
1538
	/**
1539
	 * Set pagination quantity for file list field
1540
	 *
1541
	 * @config
1542
	 * @var int
1543
	 */
1544
	private static $page_size = 11;
1545
1546
	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...
1547
		'$Action!' => '$Action',
1548
		'' => 'index',
1549
	);
1550
1551
	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...
1552
		'Form'
1553
	);
1554
1555
	public function __construct($parent, $folderName = null) {
1556
		$this->parent = $parent;
1557
		$this->folderName = $folderName;
1558
1559
		parent::__construct();
1560
	}
1561
1562
	public function index() {
1563
		// Requires a separate JS file, because we can't reach into the iframe with entwine.
1564
		Requirements::javascript(FRAMEWORK_DIR . '/client/dist/js/UploadField_select.js');
1565
		return $this->renderWith('CMSDialog');
1566
	}
1567
1568
	/**
1569
	 * @param string $action
1570
	 * @return string
1571
	 */
1572
	public function Link($action = null) {
1573
		return Controller::join_links($this->parent->Link(), '/select/', $action);
1574
	}
1575
1576
	/**
1577
	 * Build the file selection form.
1578
	 *
1579
	 * @return Form
1580
	 */
1581
	public function Form() {
1582
		// Find out the requested folder ID.
1583
		$folderID = $this->parent->getRequest()->requestVar('ParentID');
1584
		if ($folderID === null && $this->parent->getDisplayFolderName()) {
1585
			$folder = Folder::find_or_make($this->parent->getDisplayFolderName());
1586
			$folderID = $folder ? $folder->ID : 0;
1587
		}
1588
1589
		// Construct the form
1590
		$action = new FormAction('doAttach', _t('UploadField.AttachFile', 'Attach file(s)'));
1591
		$action->addExtraClass('ss-ui-action-constructive icon-accept');
1592
		$form = new Form(
1593
			$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...
1594
			'Form',
1595
			new FieldList($this->getListField($folderID)),
1596
			new FieldList($action)
1597
		);
1598
1599
		// Add a class so we can reach the form from the frontend.
1600
		$form->addExtraClass('uploadfield-form');
1601
1602
		return $form;
1603
	}
1604
1605
	/**
1606
	 * @param $folderID The ID of the folder to display.
1607
	 * @return FormField
1608
	 */
1609
	protected function getListField($folderID) {
1610
		// Generate the folder selection field.
1611
		$folderField = new TreeDropdownField('ParentID', _t('HtmlEditorField.FOLDER', 'Folder'), 'Folder');
1612
		$folderField->setValue($folderID);
1613
1614
		// Generate the file list field.
1615
		$config = GridFieldConfig::create();
1616
		$config->addComponent(new GridFieldSortableHeader());
1617
		$config->addComponent(new GridFieldFilterHeader());
1618
		$config->addComponent($colsComponent = new GridFieldDataColumns());
1619
		$colsComponent->setDisplayFields(array(
1620
			'StripThumbnail' => '',
1621
			'Title' => singleton('File')->fieldLabel('Title'),
1622
			'Created' => singleton('File')->fieldLabel('Created'),
1623
			'Size' => singleton('File')->fieldLabel('Size')
1624
		));
1625
		$colsComponent->setFieldCasting(array(
1626
			'Created' => 'SS_Datetime->Nice'
1627
		));
1628
1629
 		// Set configurable pagination for file list field
1630
		$pageSize = Config::inst()->get(get_class($this), 'page_size');
1631
		$config->addComponent(new GridFieldPaginator($pageSize));
1632
1633
		// If relation is to be autoset, we need to make sure we only list compatible objects.
1634
		$baseClass = $this->parent->getRelationAutosetClass();
1635
1636
		// Create the data source for the list of files within the current directory.
1637
		$files = DataList::create($baseClass)->exclude('ClassName', 'Folder');
1638
		if($folderID) $files = $files->filter('ParentID', $folderID);
1639
1640
		$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...
1641
		$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...
1642
		if($this->parent->getAllowedMaxFileNumber() !== 1) {
1643
			$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...
1644
		}
1645
1646
		$selectComposite = new CompositeField(
1647
			$folderField,
1648
			$fileField
1649
		);
1650
1651
		return $selectComposite;
1652
	}
1653
1654
	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...
1655
		// Popup-window attach does not require server side action, as it is implemented via JS
1656
	}
1657
1658
}
1659