Completed
Push — hash-nonce ( 07e2e8 )
by Sam
08:52
created

UploadField   F

Complexity

Total Complexity 170

Size/Duplication

Total Lines 1283
Duplicated Lines 0.47 %

Coupling/Cohesion

Components 4
Dependencies 29

Importance

Changes 0
Metric Value
dl 6
loc 1283
rs 0.5217
c 0
b 0
f 0
wmc 170
lcom 4
cbo 29

63 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 22 2
A setTemplateFileButtons() 0 4 1
A getTemplateFileButtons() 0 3 1
A setTemplateFileEdit() 0 4 1
A getTemplateFileEdit() 0 3 1
A canPreviewFolder() 0 5 3
A setCanPreviewFolder() 0 3 1
A getOverwriteWarning() 0 3 1
A setOverwriteWarning() 0 3 1
A setDisplayFolderName() 0 4 1
A getDisplayFolderName() 0 3 1
A setRecord() 0 4 1
B getRecord() 0 14 9
C setValue() 0 66 16
A setItems() 0 3 1
A getItems() 0 3 2
A getCustomisedItems() 0 7 2
A getItemIDs() 0 4 2
A Value() 0 4 1
B saveInto() 0 19 8
A customiseFile() 0 12 1
A setConfig() 0 4 1
A getConfig() 0 4 2
A getAutoUpload() 0 3 1
A setAutoUpload() 0 3 1
A getAllowedMaxFileNumber() 0 17 4
A setAllowedMaxFileNumber() 0 3 1
A canUpload() 0 5 3
A setCanUpload() 0 3 1
A canAttachExisting() 0 5 3
A isActive() 0 3 2
A setCanAttachExisting() 0 3 1
A getPreviewMaxWidth() 0 3 1
A setPreviewMaxWidth() 0 3 1
A getPreviewMaxHeight() 0 3 1
A setPreviewMaxHeight() 0 3 1
A getUploadTemplateName() 0 3 1
A setUploadTemplateName() 0 3 1
A getDownloadTemplateName() 0 3 1
A setDownloadTemplateName() 0 3 1
B getFileEditFields() 0 22 6
A setFileEditFields() 0 4 1
A getFileEditActions() 0 19 4
A setFileEditActions() 0 4 1
A getFileEditValidator() 0 14 4
A setFileEditValidator() 0 4 1
B getThumbnailURLForFile() 0 18 8
A getAttributes() 0 6 1
A extraClass() 0 5 3
C Field() 0 83 7
C validate() 6 46 7
A handleItem() 0 3 1
A getItemHandler() 0 3 1
A handleSelect() 0 4 2
C extractUploadedFileData() 0 25 8
B saveTemporaryFile() 0 41 6
A encodeFileAttributes() 0 16 1
C upload() 0 34 7
A attach() 0 14 4
A checkFileExists() 0 17 3
A fileexists() 0 19 3
A performReadonlyTransformation() 0 6 1
B getRelationAutosetClass() 0 15 5

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like UploadField often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use UploadField, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * Field for uploading single or multiple files of all types, including images.
5
 *
6
 * <b>Features (some might not be available to old browsers):</b>
7
 *
8
 * - File Drag&Drop support
9
 * - Progressbar
10
 * - Image thumbnail/file icons even before upload finished
11
 * - Saving into relations on form submit
12
 * - Edit file
13
 * - allowedExtensions is by default File::$allowed_extensions<li>maxFileSize the value of min(upload_max_filesize,
14
 * post_max_size) from php.ini
15
 *
16
 * <>Usage</b>
17
 *
18
 * @example <code>
19
 * $UploadField = new UploadField('AttachedImages', 'Please upload some images <span>(max. 5 files)</span>');
20
 * $UploadField->setAllowedFileCategories('image');
21
 * $UploadField->setAllowedMaxFileNumber(5);
22
 * </code>
23
 *
24
 * @author Zauberfisch
25
 * @package forms
26
 * @subpackages fields-files
27
 */
28
class UploadField extends FileField {
29
30
	/**
31
	 * @var array
32
	 */
33
	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...
34
		'upload',
35
		'attach',
36
		'handleItem',
37
		'handleSelect',
38
		'fileexists'
39
	);
40
41
	/**
42
	 * @var array
43
	 */
44
	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...
45
		'item/$ID' => 'handleItem',
46
		'select' => 'handleSelect',
47
		'$Action!' => '$Action',
48
	);
49
50
	/**
51
	 * Template to use for the file button widget
52
	 *
53
	 * @var string
54
	 */
55
	protected $templateFileButtons = 'UploadField_FileButtons';
56
57
	/**
58
	 * Template to use for the edit form
59
	 *
60
	 * @var string
61
	 */
62
	protected $templateFileEdit = 'UploadField_FileEdit';
63
64
	/**
65
	 * Parent data record. Will be infered from parent form or controller if blank.
66
	 *
67
	 * @var DataObject
68
	 */
69
	protected $record;
70
71
	/**
72
	 * Items loaded into this field. May be a RelationList, or any other SS_List
73
	 *
74
	 * @var SS_List
75
	 */
76
	protected $items;
77
78
	/**
79
	 * Config for this field used in the front-end javascript
80
	 * (will be merged into the config of the javascript file upload plugin).
81
	 * See framework/_config/uploadfield.yml for configuration defaults and documentation.
82
	 *
83
	 * @var array
84
	 */
85
	protected $ufConfig = array(
86
		/**
87
		 * Automatically upload the file once selected
88
		 *
89
		 * @var boolean
90
		 */
91
		'autoUpload' => true,
92
		/**
93
		 * Restriction on number of files that may be set for this field. Set to null to allow
94
		 * unlimited. If record has a has_one and allowedMaxFileNumber is null, it will be set to 1.
95
		 * The resulting value will be set to maxNumberOfFiles
96
		 *
97
		 * @var integer
98
		 */
99
		'allowedMaxFileNumber' => null,
100
		/**
101
		 * Can the user upload new files, or just select from existing files.
102
		 * String values are interpreted as permission codes.
103
		 *
104
		 * @var boolean|string
105
		 */
106
		'canUpload' => true,
107
		/**
108
		 * Can the user attach files from the assets archive on the site?
109
		 * String values are interpreted as permission codes.
110
		 *
111
		 * @var boolean|string
112
		 */
113
		'canAttachExisting' => "CMS_ACCESS_AssetAdmin",
114
		/**
115
		 * Shows the target folder for new uploads in the field UI.
116
		 * Disable to keep the internal filesystem structure hidden from users.
117
		 *
118
		 * @var boolean|string
119
		 */
120
		'canPreviewFolder' => true,
121
		/**
122
		 * Indicate a change event to the containing form if an upload
123
		 * or file edit/delete was performed.
124
		 *
125
		 * @var boolean
126
		 */
127
		'changeDetection' => true,
128
		/**
129
		 * Maximum width of the preview thumbnail
130
		 *
131
		 * @var integer
132
		 */
133
		'previewMaxWidth' => 80,
134
		/**
135
		 * Maximum height of the preview thumbnail
136
		 *
137
		 * @var integer
138
		 */
139
		'previewMaxHeight' => 60,
140
		/**
141
		 * javascript template used to display uploading files
142
		 *
143
		 * @see javascript/UploadField_uploadtemplate.js
144
		 * @var string
145
		 */
146
		'uploadTemplateName' => 'ss-uploadfield-uploadtemplate',
147
		/**
148
		 * javascript template used to display already uploaded files
149
		 *
150
		 * @see javascript/UploadField_downloadtemplate.js
151
		 * @var string
152
		 */
153
		'downloadTemplateName' => 'ss-uploadfield-downloadtemplate',
154
		/**
155
		 * Show a warning when overwriting a file.
156
		 * This requires Upload->replaceFile config to be set to true, otherwise
157
		 * files will be renamed instead of overwritten
158
		 *
159
		 * @see Upload
160
		 * @var boolean
161
		 */
162
		'overwriteWarning' => true
163
	);
164
165
	/**
166
	 * @var String Folder to display in "Select files" list.
167
	 * Defaults to listing all files regardless of folder.
168
	 * The folder path should be relative to the webroot.
169
	 * See {@link FileField->folderName} to set the upload target instead.
170
	 * @example admin/folder/subfolder
171
	 */
172
	protected $displayFolderName;
173
174
	/**
175
	 * FieldList $fields or string $name (of a method on File to provide a fields) for the EditForm
176
	 * @example 'getCMSFields'
177
	 *
178
	 * @var FieldList|string
179
	 */
180
	protected $fileEditFields = null;
181
182
	/**
183
	 * FieldList $actions or string $name (of a method on File to provide a actions) for the EditForm
184
	 * @example 'getCMSActions'
185
	 *
186
	 * @var FieldList|string
187
	 */
188
	protected $fileEditActions = null;
189
190
	/**
191
	 * Validator (eg RequiredFields) or string $name (of a method on File to provide a Validator) for the EditForm
192
	 * @example 'getCMSValidator'
193
	 *
194
	 * @var RequiredFields|string
195
	 */
196
	protected $fileEditValidator = null;
197
198
	/**
199
	 * Construct a new UploadField instance
200
	 *
201
	 * @param string $name The internal field name, passed to forms.
202
	 * @param string $title The field label.
203
	 * @param SS_List $items If no items are defined, the field will try to auto-detect an existing relation on
204
	 *                       @link $record}, with the same name as the field name.
205
	 * @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...
206
	 */
207
	public function __construct($name, $title = null, SS_List $items = null) {
208
209
		// TODO thats the first thing that came to my head, feel free to change it
210
		$this->addExtraClass('ss-upload'); // class, used by js
211
		$this->addExtraClass('ss-uploadfield'); // class, used by css for uploadfield only
212
213
		$this->ufConfig = array_merge($this->ufConfig, self::config()->defaultConfig);
214
215
		parent::__construct($name, $title);
216
217
		if($items) $this->setItems($items);
218
219
		// filter out '' since this would be a regex problem on JS end
220
		$this->getValidator()->setAllowedExtensions(
221
			array_filter(Config::inst()->get('File', 'allowed_extensions'))
222
		);
223
224
		// get the lower max size
225
		$maxUpload = File::ini2bytes(ini_get('upload_max_filesize'));
226
		$maxPost = File::ini2bytes(ini_get('post_max_size'));
227
		$this->getValidator()->setAllowedMaxFileSize(min($maxUpload, $maxPost));
228
	}
229
230
	/**
231
	 * Set name of template used for Buttons on each file (replace, edit, remove, delete) (without path or extension)
232
	 *
233
	 * @param string
234
	 */
235
	public function setTemplateFileButtons($template) {
236
		$this->templateFileButtons = $template;
237
		return $this;
238
	}
239
240
	/**
241
	 * @return string
242
	 */
243
	public function getTemplateFileButtons() {
244
		return $this->templateFileButtons;
245
	}
246
247
	/**
248
	 * Set name of template used for the edit (inline & popup) of a file file (without path or extension)
249
	 *
250
	 * @param string
251
	 */
252
	public function setTemplateFileEdit($template) {
253
		$this->templateFileEdit = $template;
254
		return $this;
255
	}
256
257
	/**
258
	 * @return string
259
	 */
260
	public function getTemplateFileEdit() {
261
		return $this->templateFileEdit;
262
	}
263
264
	/**
265
	 * Determine if the target folder for new uploads in is visible the field UI.
266
	 *
267
	 * @return boolean
268
	 */
269
	public function canPreviewFolder() {
270
		if(!$this->isActive()) return false;
271
		$can = $this->getConfig('canPreviewFolder');
272
		return (is_bool($can)) ? $can : Permission::check($can);
273
	}
274
275
	/**
276
	 * Determine if the target folder for new uploads in is visible the field UI.
277
	 * Disable to keep the internal filesystem structure hidden from users.
278
	 *
279
	 * @param boolean|string $canPreviewFolder Either a boolean flag, or a
280
	 * required permission code
281
	 * @return UploadField Self reference
282
	 */
283
	public function setCanPreviewFolder($canPreviewFolder) {
284
		return $this->setConfig('canPreviewFolder', $canPreviewFolder);
285
	}
286
287
	/**
288
	 * Determine if the field should show a warning when overwriting a file.
289
	 * This requires Upload->replaceFile config to be set to true, otherwise
290
	 * files will be renamed instead of overwritten (although the warning will
291
	 * still be displayed)
292
	 *
293
	 * @return boolean
294
	 */
295
	public function getOverwriteWarning() {
296
		return $this->getConfig('overwriteWarning');
297
	}
298
299
	/**
300
	 * Determine if the field should show a warning when overwriting a file.
301
	 * This requires Upload->replaceFile config to be set to true, otherwise
302
	 * files will be renamed instead of overwritten (although the warning will
303
	 * still be displayed)
304
	 *
305
	 * @param boolean $overwriteWarning
306
	 * @return UploadField Self reference
307
	 */
308
	public function setOverwriteWarning($overwriteWarning) {
309
		return $this->setConfig('overwriteWarning', $overwriteWarning);
310
	}
311
312
	/**
313
	 * @param String
314
	 */
315
	public function setDisplayFolderName($name) {
316
		$this->displayFolderName = $name;
317
		return $this;
318
	}
319
320
	/**
321
	 * @return String
322
	 */
323
	public function getDisplayFolderName() {
324
		return $this->displayFolderName;
325
	}
326
327
	/**
328
	 * Force a record to be used as "Parent" for uploaded Files (eg a Page with a has_one to File)
329
	 * @param DataObject $record
330
	 */
331
	public function setRecord($record) {
332
		$this->record = $record;
333
		return $this;
334
	}
335
	/**
336
	 * 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
337
	 * use Form->getRecord() or Form->Controller()->data()
338
	 *
339
	 * @return DataObject
340
	 */
341
	public function getRecord() {
342
		if (!$this->record && $this->form) {
343
			if (($record = $this->form->getRecord()) && ($record instanceof DataObject)) {
344
				$this->record = $record;
345
			} 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...
346
				&& $controller->hasMethod('data')
347
				&& ($record = $controller->data())
348
				&& ($record instanceof DataObject)
349
			) {
350
				$this->record = $record;
351
			}
352
		}
353
		return $this->record;
354
	}
355
356
	/**
357
	 * Loads the related record values into this field. UploadField can be uploaded
358
	 * in one of three ways:
359
	 *
360
	 *  - By passing in a list of file IDs in the $value parameter (an array with a single
361
	 *    key 'Files', with the value being the actual array of IDs).
362
	 *  - By passing in an explicit list of File objects in the $record parameter, and
363
	 *    leaving $value blank.
364
	 *  - By passing in a dataobject in the $record parameter, from which file objects
365
	 *    will be extracting using the field name as the relation field.
366
	 *
367
	 * Each of these methods will update both the items (list of File objects) and the
368
	 * field value (list of file ID values).
369
	 *
370
	 * @param array $value Array of submitted form data, if submitting from a form
371
	 * @param array|DataObject|SS_List $record Full source record, either as a DataObject,
372
	 * SS_List of items, or an array of submitted form data
373
	 * @return UploadField Self reference
374
	 */
375
	public function setValue($value, $record = null) {
376
377
		// If we're not passed a value directly, we can attempt to infer the field
378
		// value from the second parameter by inspecting its relations
379
		$items = new ArrayList();
380
381
		// Determine format of presented data
382
		if(empty($value) && $record) {
383
			// If a record is given as a second parameter, but no submitted values,
384
			// then we should inspect this instead for the form values
385
386
			if(($record instanceof DataObject) && $record->hasMethod($this->getName())) {
387
				// If given a dataobject use reflection to extract details
388
389
				$data = $record->{$this->getName()}();
390
				if($data instanceof DataObject) {
391
					// If has_one, add sole item to default list
392
					$items->push($data);
393
				} elseif($data instanceof SS_List) {
394
					// For many_many and has_many relations we can use the relation list directly
395
					$items = $data;
396
				}
397
			} elseif($record instanceof SS_List) {
398
				// If directly passing a list then save the items directly
399
				$items = $record;
400
			}
401
		} elseif(!empty($value['Files'])) {
402
			// If value is given as an array (such as a posted form), extract File IDs from this
403
			$class = $this->getRelationAutosetClass();
404
			$items = DataObject::get($class)->byIDs($value['Files']);
405
		}
406
407
		// If javascript is disabled, direct file upload (non-html5 style) can
408
		// trigger a single or multiple file submission. Note that this may be
409
		// included in addition to re-submitted File IDs as above, so these
410
		// should be added to the list instead of operated on independently.
411
		if($uploadedFiles = $this->extractUploadedFileData($value)) {
412
			foreach($uploadedFiles as $tempFile) {
413
				$file = $this->saveTemporaryFile($tempFile, $error);
414
				if($file) {
415
					$items->add($file);
416
				} else {
417
					throw new ValidationException($error);
418
				}
419
			}
420
		}
421
422
		// Filter items by what's allowed to be viewed
423
		$filteredItems = new ArrayList();
424
		$fileIDs = array();
425
		foreach($items as $file) {
426
			if($file->exists() && $file->canView()) {
427
				$filteredItems->push($file);
428
				$fileIDs[] = $file->ID;
429
			}
430
		}
431
432
		// Filter and cache updated item list
433
		$this->items = $filteredItems;
434
		// Same format as posted form values for this field. Also ensures that
435
		// $this->setValue($this->getValue()); is non-destructive
436
		$value = $fileIDs ? array('Files' => $fileIDs) : null;
437
438
		// Set value using parent
439
		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...
440
	}
441
442
	/**
443
	 * Sets the items assigned to this field as an SS_List of File objects.
444
	 * Calling setItems will also update the value of this field, as well as
445
	 * updating the internal list of File items.
446
	 *
447
	 * @param SS_List $items
448
	 * @return UploadField self reference
449
	 */
450
	public function setItems(SS_List $items) {
451
		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...
452
	}
453
454
	/**
455
	 * Retrieves the current list of files
456
	 *
457
	 * @return SS_List
458
	 */
459
	public function getItems() {
460
		return $this->items ? $this->items : new ArrayList();
461
	}
462
463
	/**
464
	 * Retrieves a customised list of all File records to ensure they are
465
	 * properly viewable when rendered in the field template.
466
	 *
467
	 * @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...
468
	 */
469
	public function getCustomisedItems() {
470
		$customised = new ArrayList();
471
		foreach($this->getItems() as $file) {
472
			$customised->push($this->customiseFile($file));
473
		}
474
		return $customised;
475
	}
476
477
	/**
478
	 * Retrieves the list of selected file IDs
479
	 *
480
	 * @return array
481
	 */
482
	public function getItemIDs() {
483
		$value = $this->Value();
484
		return empty($value['Files']) ? array() : $value['Files'];
485
	}
486
487
	public function Value() {
488
		// Re-override FileField Value to use data value
489
		return $this->dataValue();
490
	}
491
492
	public function saveInto(DataObjectInterface $record) {
493
		// Check required relation details are available
494
		$fieldname = $this->getName();
495
		if(!$fieldname) return $this;
496
497
		// Get details to save
498
		$idList = $this->getItemIDs();
499
500
		// Check type of relation
501
		$relation = $record->hasMethod($fieldname) ? $record->$fieldname() : null;
502
		if($relation && ($relation instanceof RelationList || $relation instanceof UnsavedRelationList)) {
503
			// has_many or many_many
504
			$relation->setByIDList($idList);
505
		} elseif($record->hasOneComponent($fieldname)) {
506
			// has_one
507
			$record->{"{$fieldname}ID"} = $idList ? reset($idList) : 0;
508
		}
509
		return $this;
510
	}
511
512
	/**
513
	 * Customises a file with additional details suitable for rendering in the
514
	 * UploadField.ss template
515
	 *
516
	 * @param File $file
517
	 * @return ViewableData_Customised
518
	 */
519
	protected function customiseFile(File $file) {
520
		$file = $file->customise(array(
521
			'UploadFieldThumbnailURL' => $this->getThumbnailURLForFile($file),
522
			'UploadFieldDeleteLink' => $this->getItemHandler($file->ID)->DeleteLink(),
523
			'UploadFieldEditLink' => $this->getItemHandler($file->ID)->EditLink(),
524
			'UploadField' => $this
525
		));
526
		// we do this in a second customise to have the access to the previous customisations
527
		return $file->customise(array(
528
			'UploadFieldFileButtons' => (string)$file->renderWith($this->getTemplateFileButtons())
529
		));
530
	}
531
532
	/**
533
	 * Assign a front-end config variable for the upload field
534
	 *
535
	 * @see https://github.com/blueimp/jQuery-File-Upload/wiki/Options for the list of front end options available
536
	 *
537
	 * @param string $key
538
	 * @param mixed $val
539
	 * @return UploadField self reference
540
	 */
541
	public function setConfig($key, $val) {
542
		$this->ufConfig[$key] = $val;
543
		return $this;
544
	}
545
546
	/**
547
	 * Gets a front-end config variable for the upload field
548
	 *
549
	 * @see https://github.com/blueimp/jQuery-File-Upload/wiki/Options for the list of front end options available
550
	 *
551
	 * @param string $key
552
	 * @return mixed
553
	 */
554
	public function getConfig($key) {
555
		if(!isset($this->ufConfig[$key])) return null;
556
		return $this->ufConfig[$key];
557
	}
558
559
	/**
560
	 * Determine if the field should automatically upload the file.
561
	 *
562
	 * @return boolean
563
	 */
564
	public function getAutoUpload() {
565
		return $this->getConfig('autoUpload');
566
	}
567
568
	/**
569
	 * Determine if the field should automatically upload the file
570
	 *
571
	 * @param boolean $autoUpload
572
	 * @return UploadField Self reference
573
	 */
574
	public function setAutoUpload($autoUpload) {
575
		return $this->setConfig('autoUpload', $autoUpload);
576
	}
577
578
	/**
579
	 * Determine maximum number of files allowed to be attached
580
	 * Defaults to 1 for has_one and null (unlimited) for
581
	 * many_many and has_many relations.
582
	 *
583
	 * @return integer|null Maximum limit, or null for no limit
584
	 */
585
	public function getAllowedMaxFileNumber() {
586
		$allowedMaxFileNumber = $this->getConfig('allowedMaxFileNumber');
587
588
		// if there is a has_one relation with that name on the record and
589
		// allowedMaxFileNumber has not been set, it's wanted to be 1
590
		if(empty($allowedMaxFileNumber)) {
591
			$record = $this->getRecord();
592
			$name = $this->getName();
593
			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...
594
				return 1; // Default for has_one
595
			} else {
596
				return null; // Default for has_many and many_many
597
			}
598
		} else {
599
			return $allowedMaxFileNumber;
600
		}
601
	}
602
603
	/**
604
	 * Determine maximum number of files allowed to be attached.
605
	 *
606
	 * @param integer|null $allowedMaxFileNumber Maximum limit. 0 or null will be treated as unlimited
607
	 * @return UploadField Self reference
608
	 */
609
	public function setAllowedMaxFileNumber($allowedMaxFileNumber) {
610
		return $this->setConfig('allowedMaxFileNumber', $allowedMaxFileNumber);
611
	}
612
613
	/**
614
	 * Determine if the user has permission to upload.
615
	 *
616
	 * @return boolean
617
	 */
618
	public function canUpload() {
619
		if(!$this->isActive()) return false;
620
		$can = $this->getConfig('canUpload');
621
		return (is_bool($can)) ? $can : Permission::check($can);
622
	}
623
624
	/**
625
	 * Specify whether the user can upload files.
626
	 * String values will be treated as required permission codes
627
	 *
628
	 * @param boolean|string $canUpload Either a boolean flag, or a required
629
	 * permission code
630
	 * @return UploadField Self reference
631
	 */
632
	public function setCanUpload($canUpload) {
633
		return $this->setConfig('canUpload', $canUpload);
634
	}
635
636
	/**
637
	 * Determine if the user has permission to attach existing files
638
	 * By default returns true if the user has the CMS_ACCESS_AssetAdmin permission
639
	 *
640
	 * @return boolean
641
	 */
642
	public function canAttachExisting() {
643
		if(!$this->isActive()) return false;
644
		$can = $this->getConfig('canAttachExisting');
645
		return (is_bool($can)) ? $can : Permission::check($can);
646
	}
647
648
	/**
649
	 * Returns true if the field is neither readonly nor disabled
650
	 *
651
	 * @return boolean
652
	 */
653
	public function isActive() {
654
		return !$this->isDisabled() && !$this->isReadonly();
655
	}
656
657
	/**
658
	 * Specify whether the user can attach existing files
659
	 * String values will be treated as required permission codes
660
	 *
661
	 * @param boolean|string $canAttachExisting Either a boolean flag, or a
662
	 * required permission code
663
	 * @return UploadField Self reference
664
	 */
665
	public function setCanAttachExisting($canAttachExisting) {
666
		return $this->setConfig('canAttachExisting', $canAttachExisting);
667
	}
668
669
	/**
670
	 * Gets thumbnail width. Defaults to 80
671
	 *
672
	 * @return integer
673
	 */
674
	public function getPreviewMaxWidth() {
675
		return $this->getConfig('previewMaxWidth');
676
	}
677
678
	/**
679
	 * @see UploadField::getPreviewMaxWidth()
680
	 *
681
	 * @param integer $previewMaxWidth
682
	 * @return UploadField Self reference
683
	 */
684
	public function setPreviewMaxWidth($previewMaxWidth) {
685
		return $this->setConfig('previewMaxWidth', $previewMaxWidth);
686
	}
687
688
	/**
689
	 * Gets thumbnail height. Defaults to 60
690
	 *
691
	 * @return integer
692
	 */
693
	public function getPreviewMaxHeight() {
694
		return $this->getConfig('previewMaxHeight');
695
	}
696
697
	/**
698
	 * @see UploadField::getPreviewMaxHeight()
699
	 *
700
	 * @param integer $previewMaxHeight
701
	 * @return UploadField Self reference
702
	 */
703
	public function setPreviewMaxHeight($previewMaxHeight) {
704
		return $this->setConfig('previewMaxHeight', $previewMaxHeight);
705
	}
706
707
	/**
708
	 * javascript template used to display uploading files
709
	 * Defaults to 'ss-uploadfield-uploadtemplate'
710
	 *
711
	 * @see javascript/UploadField_uploadtemplate.js
712
	 * @var string
713
	 */
714
	public function getUploadTemplateName() {
715
		return $this->getConfig('uploadTemplateName');
716
	}
717
718
	/**
719
	 * @see UploadField::getUploadTemplateName()
720
	 *
721
	 * @param string $uploadTemplateName
722
	 * @return UploadField Self reference
723
	 */
724
	public function setUploadTemplateName($uploadTemplateName) {
725
		return $this->setConfig('uploadTemplateName', $uploadTemplateName);
726
	}
727
728
	/**
729
	 * javascript template used to display already uploaded files
730
	 * Defaults to 'ss-downloadfield-downloadtemplate'
731
	 *
732
	 * @see javascript/DownloadField_downloadtemplate.js
733
	 * @var string
734
	 */
735
	public function getDownloadTemplateName() {
736
		return $this->getConfig('downloadTemplateName');
737
	}
738
739
	/**
740
	 * @see Uploadfield::getDownloadTemplateName()
741
	 *
742
	 * @param string $downloadTemplateName
743
	 * @return Uploadfield Self reference
744
	 */
745
	public function setDownloadTemplateName($downloadTemplateName) {
746
		return $this->setConfig('downloadTemplateName', $downloadTemplateName);
747
	}
748
749
	/**
750
	 * FieldList $fields for the EditForm
751
	 * @example 'getCMSFields'
752
	 *
753
	 * @param File $file File context to generate fields for
754
	 * @return FieldList List of form fields
755
	 */
756
	public function getFileEditFields(File $file) {
757
758
		// Empty actions, generate default
759
		if(empty($this->fileEditFields)) {
760
			$fields = $file->getCMSFields();
761
			// Only display main tab, to avoid overly complex interface
762
			if($fields->hasTabSet() && ($mainTab = $fields->findOrMakeTab('Root.Main'))) {
763
				$fields = $mainTab->Fields();
764
			}
765
			return $fields;
766
		}
767
768
		// Fields instance
769
		if ($this->fileEditFields instanceof FieldList) return $this->fileEditFields;
770
771
		// Method to call on the given file
772
		if($file->hasMethod($this->fileEditFields)) {
773
			return $file->{$this->fileEditFields}();
774
		}
775
776
		user_error("Invalid value for UploadField::fileEditFields", E_USER_ERROR);
777
	}
778
779
	/**
780
	 * FieldList $fields or string $name (of a method on File to provide a fields) for the EditForm
781
	 * @example 'getCMSFields'
782
	 *
783
	 * @param FieldList|string
784
	 * @return Uploadfield Self reference
785
	 */
786
	public function setFileEditFields($fileEditFields) {
787
		$this->fileEditFields = $fileEditFields;
788
		return $this;
789
	}
790
791
	/**
792
	 * FieldList $actions or string $name (of a method on File to provide a actions) for the EditForm
793
	 * @example 'getCMSActions'
794
	 *
795
	 * @param File $file File context to generate form actions for
796
	 * @return FieldList Field list containing FormAction
797
	 */
798
	public function getFileEditActions(File $file) {
799
800
		// Empty actions, generate default
801
		if(empty($this->fileEditActions)) {
802
			$actions = new FieldList($saveAction = new FormAction('doEdit', _t('UploadField.DOEDIT', 'Save')));
803
			$saveAction->addExtraClass('ss-ui-action-constructive icon-accept');
804
			return $actions;
805
		}
806
807
		// Actions instance
808
		if ($this->fileEditActions instanceof FieldList) return $this->fileEditActions;
809
810
		// Method to call on the given file
811
		if($file->hasMethod($this->fileEditActions)) {
812
			return $file->{$this->fileEditActions}();
813
		}
814
815
		user_error("Invalid value for UploadField::fileEditActions", E_USER_ERROR);
816
	}
817
818
	/**
819
	 * FieldList $actions or string $name (of a method on File to provide a actions) for the EditForm
820
	 * @example 'getCMSActions'
821
	 *
822
	 * @param FieldList|string
823
	 * @return Uploadfield Self reference
824
	 */
825
	public function setFileEditActions($fileEditActions) {
826
		$this->fileEditActions = $fileEditActions;
827
		return $this;
828
	}
829
830
	/**
831
	 * Determines the validator to use for the edit form
832
	 * @example 'getCMSValidator'
833
	 *
834
	 * @param File $file File context to generate validator from
835
	 * @return Validator Validator object
836
	 */
837
	public function getFileEditValidator(File $file) {
838
		// Empty validator
839
		if(empty($this->fileEditValidator)) return null;
840
841
		// Validator instance
842
		if($this->fileEditValidator instanceof Validator) return $this->fileEditValidator;
843
844
		// Method to call on the given file
845
		if($file->hasMethod($this->fileEditValidator)) {
846
			return $file->{$this->fileEditValidator}();
847
		}
848
849
		user_error("Invalid value for UploadField::fileEditValidator", E_USER_ERROR);
850
	}
851
852
	/**
853
	 * Validator (eg RequiredFields) or string $name (of a method on File to provide a Validator) for the EditForm
854
	 * @example 'getCMSValidator'
855
	 *
856
	 * @param Validator|string
857
	 * @return Uploadfield Self reference
858
	 */
859
	public function setFileEditValidator($fileEditValidator) {
860
		$this->fileEditValidator = $fileEditValidator;
861
		return $this;
862
	}
863
864
	/**
865
	 * @param File $file
866
	 * @return string
867
	 */
868
	protected function getThumbnailURLForFile(File $file) {
869
		if ($file->exists() && file_exists(Director::baseFolder() . '/' . $file->getFilename())) {
870
			$width = $this->getPreviewMaxWidth();
871
			$height = $this->getPreviewMaxHeight();
872
			if ($file->hasMethod('getThumbnail')) {
873
				$r = $file->getThumbnail($width, $height);
874
				if ($r) return $r->getURL();
875
			} elseif ($file->hasMethod('getThumbnailURL')) {
876
				return $file->getThumbnailURL($width, $height);
877
			} elseif ($file->hasMethod('Fit')) {
878
				$r = $file->Fit($width, $height);
879
				if ($r) return $r->getURL();
880
			} else {
881
				return $file->Icon();
882
			}
883
		}
884
		return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by UploadField::getThumbnailURLForFile of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
885
	}
886
887
	public function getAttributes() {
888
		return array_merge(
889
			parent::getAttributes(),
890
			array('data-selectdialog-url', $this->Link('select'))
891
		);
892
	}
893
894
	public function extraClass() {
895
		if($this->isDisabled()) $this->addExtraClass('disabled');
896
		if($this->isReadonly()) $this->addExtraClass('readonly');
897
		return parent::extraClass();
898
	}
899
900
	public function Field($properties = array()) {
901
		Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.js');
902
		Requirements::javascript(THIRDPARTY_DIR . '/jquery-ui/jquery-ui.js');
903
		Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js');
904
		Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/javascript/ssui.core.js');
905
		Requirements::add_i18n_javascript(FRAMEWORK_DIR . '/javascript/lang');
906
907
		Requirements::combine_files('uploadfield.js', array(
908
			// @todo jquery templates is a project no longer maintained and should be retired at some point.
909
			THIRDPARTY_DIR . '/javascript-templates/tmpl.js',
910
			THIRDPARTY_DIR . '/javascript-loadimage/load-image.js',
911
			THIRDPARTY_DIR . '/jquery-fileupload/jquery.iframe-transport.js',
912
			THIRDPARTY_DIR . '/jquery-fileupload/cors/jquery.xdr-transport.js',
913
			THIRDPARTY_DIR . '/jquery-fileupload/jquery.fileupload.js',
914
			THIRDPARTY_DIR . '/jquery-fileupload/jquery.fileupload-ui.js',
915
			FRAMEWORK_DIR . '/javascript/UploadField_uploadtemplate.js',
916
			FRAMEWORK_DIR . '/javascript/UploadField_downloadtemplate.js',
917
			FRAMEWORK_DIR . '/javascript/UploadField.js',
918
		));
919
		Requirements::css(THIRDPARTY_DIR . '/jquery-ui-themes/smoothness/jquery-ui.css'); // TODO hmmm, remove it?
920
		Requirements::css(FRAMEWORK_DIR . '/css/UploadField.css');
921
922
		// Calculated config as per jquery.fileupload-ui.js
923
		$allowedMaxFileNumber = $this->getAllowedMaxFileNumber();
924
		$config = array(
925
			'url' => $this->Link('upload'),
926
			'urlSelectDialog' => $this->Link('select'),
927
			'urlAttach' => $this->Link('attach'),
928
			'urlFileExists' => $this->link('fileexists'),
929
			'acceptFileTypes' => '.+$',
930
			// Fileupload treats maxNumberOfFiles as the max number of _additional_ items allowed
931
			'maxNumberOfFiles' => $allowedMaxFileNumber ? ($allowedMaxFileNumber - count($this->getItemIDs())) : null,
932
			'replaceFile' => $this->getUpload()->getReplaceFile(),
933
		);
934
935
		// Validation: File extensions
936
		if ($allowedExtensions = $this->getAllowedExtensions()) {
937
			$config['acceptFileTypes'] = '(\.|\/)(' . implode('|', $allowedExtensions) . ')$';
938
			$config['errorMessages']['acceptFileTypes'] = _t(
939
				'File.INVALIDEXTENSIONSHORT',
940
				'Extension is not allowed'
941
			);
942
		}
943
944
		// Validation: File size
945
		if ($allowedMaxFileSize = $this->getValidator()->getAllowedMaxFileSize()) {
946
			$config['maxFileSize'] = $allowedMaxFileSize;
947
			$config['errorMessages']['maxFileSize'] = _t(
948
				'File.TOOLARGESHORT',
949
				'File size exceeds {size}',
950
				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...
951
			);
952
		}
953
954
		// Validation: Number of files
955
		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...
956
			if($allowedMaxFileNumber > 1) {
957
				$config['errorMessages']['maxNumberOfFiles'] = _t(
958
					'UploadField.MAXNUMBEROFFILESSHORT',
959
					'Can only upload {count} files',
960
					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...
961
				);
962
			} else {
963
				$config['errorMessages']['maxNumberOfFiles'] = _t(
964
					'UploadField.MAXNUMBEROFFILESONE',
965
					'Can only upload one file'
966
				);
967
			}
968
		}
969
970
		// add overwrite warning error message to the config object sent to Javascript
971
		if ($this->getOverwriteWarning()) {
972
			$config['errorMessages']['overwriteWarning'] =
973
				_t('UploadField.OVERWRITEWARNING', 'File with the same name already exists');
974
		}
975
976
		$mergedConfig = array_merge($config, $this->ufConfig);
977
		return $this->customise(array(
978
			'configString' => str_replace('"', "&quot;", Convert::raw2json($mergedConfig)),
979
			'config' => new ArrayData($mergedConfig),
980
			'multiple' => $allowedMaxFileNumber !== 1
981
		))->renderWith($this->getTemplates());
982
	}
983
984
	/**
985
	 * Validation method for this field, called when the entire form is validated
986
	 *
987
	 * @param Validator $validator
988
	 * @return boolean
989
	 */
990
	public function validate($validator) {
991
		$name = $this->getName();
992
		$files = $this->getItems();
993
994
		// If there are no files then quit
995
		if($files->count() == 0) return true;
996
997
		// Check max number of files
998
		$maxFiles = $this->getAllowedMaxFileNumber();
999
		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...
1000
			$validator->validationError(
1001
				$name,
1002
				_t(
1003
					'UploadField.MAXNUMBEROFFILES',
1004
					'Max number of {count} file(s) exceeded.',
1005
					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...
1006
				),
1007
				"validation"
1008
			);
1009
			return false;
1010
		}
1011
1012
		// Revalidate each file against nested validator
1013
		$this->upload->clearErrors();
1014
		foreach($files as $file) {
1015
			// Generate $_FILES style file attribute array for upload validator
1016
			$tmpFile = array(
1017
				'name' => $file->Name,
1018
				'type' => null, // Not used for type validation
1019
				'size' => $file->AbsoluteSize,
1020
				'tmp_name' => null, // Should bypass is_uploaded_file check
1021
				'error' => UPLOAD_ERR_OK,
1022
			);
1023
			$this->upload->validate($tmpFile);
1024
		}
1025
1026
		// Check all errors
1027 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...
1028
			foreach($errors as $error) {
1029
				$validator->validationError($name, $error, "validation");
1030
			}
1031
			return false;
1032
		}
1033
1034
		return true;
1035
	}
1036
1037
	/**
1038
	 * @param SS_HTTPRequest $request
1039
	 * @return UploadField_ItemHandler
1040
	 */
1041
	public function handleItem(SS_HTTPRequest $request) {
1042
		return $this->getItemHandler($request->param('ID'));
1043
	}
1044
1045
	/**
1046
	 * @param int $itemID
1047
	 * @return UploadField_ItemHandler
1048
	 */
1049
	public function getItemHandler($itemID) {
1050
		return UploadField_ItemHandler::create($this, $itemID);
1051
	}
1052
1053
	/**
1054
	 * @param SS_HTTPRequest $request
1055
	 * @return UploadField_ItemHandler
1056
	 */
1057
	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...
1058
		if(!$this->canAttachExisting()) return $this->httpError(403);
1059
		return UploadField_SelectHandler::create($this, $this->getFolderName());
1060
	}
1061
1062
	/**
1063
	 * Given an array of post variables, extract all temporary file data into an array
1064
	 *
1065
	 * @param array $postVars Array of posted form data
1066
	 * @return array List of temporary file data
1067
	 */
1068
	protected function extractUploadedFileData($postVars) {
1069
1070
		// Note: Format of posted file parameters in php is a feature of using
1071
		// <input name='{$Name}[Uploads][]' /> for multiple file uploads
1072
		$tmpFiles = array();
1073
		if(	!empty($postVars['tmp_name'])
1074
			&& is_array($postVars['tmp_name'])
1075
			&& !empty($postVars['tmp_name']['Uploads'])
1076
		) {
1077
			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...
1078
				// Skip if "empty" file
1079
				if(empty($postVars['tmp_name']['Uploads'][$i])) continue;
1080
				$tmpFile = array();
1081
				foreach(array('name', 'type', 'tmp_name', 'error', 'size') as $field) {
1082
					$tmpFile[$field] = $postVars[$field]['Uploads'][$i];
1083
				}
1084
				$tmpFiles[] = $tmpFile;
1085
			}
1086
		} elseif(!empty($postVars['tmp_name'])) {
1087
			// Fallback to allow single file uploads (method used by AssetUploadField)
1088
			$tmpFiles[] = $postVars;
1089
		}
1090
1091
		return $tmpFiles;
1092
	}
1093
1094
	/**
1095
	 * Loads the temporary file data into a File object
1096
	 *
1097
	 * @param array $tmpFile Temporary file data
1098
	 * @param string $error Error message
1099
	 * @return File File object, or null if error
1100
	 */
1101
	protected function saveTemporaryFile($tmpFile, &$error = null) {
1102
1103
		// Determine container object
1104
		$error = null;
1105
		$fileObject = null;
1106
1107
		if (empty($tmpFile)) {
1108
			$error = _t('UploadField.FIELDNOTSET', 'File information not found');
1109
			return null;
1110
		}
1111
1112
		if($tmpFile['error']) {
1113
			$error = $tmpFile['error'];
1114
			return null;
1115
		}
1116
1117
		// Search for relations that can hold the uploaded files, but don't fallback
1118
		// to default if there is no automatic relation
1119
		if ($relationClass = $this->getRelationAutosetClass(null)) {
1120
			// Create new object explicitly. Otherwise rely on Upload::load to choose the class.
1121
			$fileObject = Object::create($relationClass);
1122
		}
1123
1124
		// Get the uploaded file into a new file object.
1125
		try {
1126
			$this->upload->loadIntoFile($tmpFile, $fileObject, $this->getFolderName());
0 ignored issues
show
Documentation introduced by
$fileObject is of type this<UploadField>|null, but the function expects a object<File>.

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...
1127
		} catch (Exception $e) {
1128
			// we shouldn't get an error here, but just in case
1129
			$error = $e->getMessage();
1130
			return null;
1131
		}
1132
1133
		// Check if upload field has an error
1134
		if ($this->upload->isError()) {
1135
			$error = implode(' ' . PHP_EOL, $this->upload->getErrors());
1136
			return null;
1137
		}
1138
1139
		// return file
1140
		return $this->upload->getFile();
1141
	}
1142
1143
	/**
1144
	 * Safely encodes the File object with all standard fields required
1145
	 * by the front end
1146
	 *
1147
	 * @param File $file
1148
	 * @return array Array encoded list of file attributes
1149
	 */
1150
	protected function encodeFileAttributes(File $file) {
1151
1152
		// Collect all output data.
1153
		$file =  $this->customiseFile($file);
1154
		return array(
1155
			'id' => $file->ID,
1156
			'name' => $file->Name,
1157
			'url' => $file->URL,
1158
			'thumbnail_url' => $file->UploadFieldThumbnailURL,
1159
			'edit_url' => $file->UploadFieldEditLink,
1160
			'size' => $file->AbsoluteSize,
1161
			'type' => $file->FileType,
1162
			'buttons' => $file->UploadFieldFileButtons,
1163
			'fieldname' => $this->getName()
1164
		);
1165
	}
1166
1167
	/**
1168
	 * Action to handle upload of a single file
1169
	 *
1170
	 * @param SS_HTTPRequest $request
1171
	 * @return SS_HTTPResponse
1172
	 * @return SS_HTTPResponse
1173
	 */
1174
	public function upload(SS_HTTPRequest $request) {
1175
		if($this->isDisabled() || $this->isReadonly() || !$this->canUpload()) {
1176
			return $this->httpError(403);
1177
		}
1178
1179
		// Protect against CSRF on destructive action
1180
		$token = $this->getForm()->getSecurityToken();
1181
		if(!$token->checkRequest($request)) return $this->httpError(400);
1182
1183
		// Get form details
1184
		$name = $this->getName();
1185
		$postVars = $request->postVar($name);
1186
1187
		// Extract uploaded files from Form data
1188
		$uploadedFiles = $this->extractUploadedFileData($postVars);
1189
		$return = array();
1190
1191
		// Save the temporary files into a File objects
1192
		// and save data/error on a per file basis
1193
		foreach ($uploadedFiles as $tempFile) {
1194
			$file = $this->saveTemporaryFile($tempFile, $error);
1195
			if(empty($file)) {
1196
				array_push($return, array('error' => $error));
1197
			} else {
1198
				array_push($return, $this->encodeFileAttributes($file));
1199
			}
1200
			$this->upload->clearErrors();
1201
		}
1202
1203
		// Format response with json
1204
		$response = new SS_HTTPResponse(Convert::raw2json($return));
1205
		$response->addHeader('Content-Type', 'text/plain');
1206
		return $response;
1207
	}
1208
1209
	/**
1210
	 * Retrieves details for files that this field wishes to attache to the
1211
	 * client-side form
1212
	 *
1213
	 * @param SS_HTTPRequest $request
1214
	 * @return SS_HTTPResponse
1215
	 */
1216
	public function attach(SS_HTTPRequest $request) {
1217
		if(!$request->isPOST()) return $this->httpError(403);
1218
		if(!$this->canAttachExisting()) return $this->httpError(403);
1219
1220
		// Retrieve file attributes required by front end
1221
		$return = array();
1222
		$files = File::get()->byIDs($request->postVar('ids'));
1223
		foreach($files as $file) {
1224
			$return[] = $this->encodeFileAttributes($file);
1225
		}
1226
		$response = new SS_HTTPResponse(Convert::raw2json($return));
1227
		$response->addHeader('Content-Type', 'application/json');
1228
		return $response;
1229
	}
1230
1231
	/**
1232
	 * Check if file exists, both checking filtered filename and exact filename
1233
	 *
1234
	 * @param string $originalFile Filename
1235
	 * @return bool
1236
	 */
1237
	protected function checkFileExists($originalFile) {
1238
1239
		// Check both original and safely filtered filename
1240
		$nameFilter = FileNameFilter::create();
1241
		$filteredFile = $nameFilter->filter($originalFile);
1242
1243
		// Resolve expected folder name
1244
		$folderName = $this->getFolderName();
1245
		$folder = Folder::find_or_make($folderName);
1246
		$parentPath = $folder
1247
			? BASE_PATH."/".$folder->getFilename()
1248
			: ASSETS_PATH."/";
1249
1250
		// check if either file exists
1251
		return file_exists($parentPath.$originalFile)
1252
			|| file_exists($parentPath.$filteredFile);
1253
	}
1254
1255
	/**
1256
	 * Determines if a specified file exists
1257
	 *
1258
	 * @param SS_HTTPRequest $request
1259
	 */
1260
	public function fileexists(SS_HTTPRequest $request) {
1261
		// Assert that requested filename doesn't attempt to escape the directory
1262
		$originalFile = $request->requestVar('filename');
1263
		if($originalFile !== basename($originalFile)) {
1264
			$return = array(
1265
				'error' => _t('File.NOVALIDUPLOAD', 'File is not a valid upload')
1266
			);
1267
		} else {
1268
			$return = array(
1269
				'exists' => $this->checkFileExists($originalFile)
1270
			);
1271
		}
1272
1273
		// Encode and present response
1274
		$response = new SS_HTTPResponse(Convert::raw2json($return));
1275
		$response->addHeader('Content-Type', 'application/json');
1276
		if (!empty($return['error'])) $response->setStatusCode(400);
1277
		return $response;
1278
	}
1279
1280
	public function performReadonlyTransformation() {
1281
		$clone = clone $this;
1282
		$clone->addExtraClass('readonly');
1283
		$clone->setReadonly(true);
1284
		return $clone;
1285
	}
1286
1287
	/**
1288
	 * Gets the foreign class that needs to be created, or 'File' as default if there
1289
	 * is no relationship, or it cannot be determined.
1290
	 *
1291
	 * @param $default Default value to return if no value could be calculated
1292
	 * @return string Foreign class name.
1293
	 */
1294
	public function getRelationAutosetClass($default = 'File') {
1295
1296
		// Don't autodetermine relation if no relationship between parent record
1297
		if(!$this->relationAutoSetting) return $default;
1298
1299
		// Check record and name
1300
		$name = $this->getName();
1301
		$record = $this->getRecord();
1302
		if(empty($name) || empty($record)) {
1303
			return $default;
1304
		} else {
1305
			$class = $record->getRelationClass($name);
1306
			return empty($class) ? $default : $class;
1307
		}
1308
	}
1309
1310
}
1311
1312
/**
1313
 * RequestHandler for actions (edit, remove, delete) on a single item (File) of the UploadField
1314
 *
1315
 * @author Zauberfisch
1316
 * @package forms
1317
 * @subpackages fields-files
1318
 */
1319
class UploadField_ItemHandler extends RequestHandler {
1320
1321
	/**
1322
	 * @var UploadFIeld
1323
	 */
1324
	protected $parent;
1325
1326
	/**
1327
	 * @var int FileID
1328
	 */
1329
	protected $itemID;
1330
1331
	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...
1332
		'$Action!' => '$Action',
1333
		'' => 'index',
1334
	);
1335
1336
	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...
1337
		'delete',
1338
		'edit',
1339
		'EditForm',
1340
	);
1341
1342
	/**
1343
	 * @param UploadFIeld $parent
1344
	 * @param int $item
0 ignored issues
show
Bug introduced by
There is no parameter named $item. 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...
1345
	 */
1346
	public function __construct($parent, $itemID) {
1347
		$this->parent = $parent;
1348
		$this->itemID = $itemID;
1349
1350
		parent::__construct();
1351
	}
1352
1353
	/**
1354
	 * @return File
1355
	 */
1356
	public function getItem() {
1357
		return DataObject::get_by_id('File', $this->itemID);
1358
	}
1359
1360
	/**
1361
	 * @param string $action
1362
	 * @return string
1363
	 */
1364
	public function Link($action = null) {
1365
		return Controller::join_links($this->parent->Link(), '/item/', $this->itemID, $action);
1366
	}
1367
1368
	/**
1369
	 * @return string
1370
	 */
1371
	public function DeleteLink() {
1372
		$token = $this->parent->getForm()->getSecurityToken();
1373
		return $token->addToUrl($this->Link('delete'));
1374
	}
1375
1376
	/**
1377
	 * @return string
1378
	 */
1379
	public function EditLink() {
1380
		return $this->Link('edit');
1381
	}
1382
1383
	/**
1384
	 * Action to handle deleting of a single file
1385
	 *
1386
	 * @param SS_HTTPRequest $request
1387
	 * @return SS_HTTPResponse
1388
	 */
1389
	public function delete(SS_HTTPRequest $request) {
1390
		// Check form field state
1391
		if($this->parent->isDisabled() || $this->parent->isReadonly()) return $this->httpError(403);
1392
1393
		// Protect against CSRF on destructive action
1394
		$token = $this->parent->getForm()->getSecurityToken();
1395
		if(!$token->checkRequest($request)) return $this->httpError(400);
1396
1397
		// Check item permissions
1398
		$item = $this->getItem();
1399
		if(!$item) return $this->httpError(404);
1400
		if($item instanceof Folder) return $this->httpError(403);
1401
		if(!$item->canDelete()) return $this->httpError(403);
1402
1403
		// Delete the file from the filesystem. The file will be removed
1404
		// from the relation on save
1405
		// @todo Investigate if references to deleted files (if unsaved) is dangerous
1406
		$item->delete();
1407
	}
1408
1409
	/**
1410
	 * Action to handle editing of a single file
1411
	 *
1412
	 * @param SS_HTTPRequest $request
1413
	 * @return ViewableData_Customised
1414
	 */
1415
	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...
1416
		// Check form field state
1417
		if($this->parent->isDisabled() || $this->parent->isReadonly()) return $this->httpError(403);
1418
1419
		// Check item permissions
1420
		$item = $this->getItem();
1421
		if(!$item) return $this->httpError(404);
1422
		if($item instanceof Folder) return $this->httpError(403);
1423
		if(!$item->canEdit()) return $this->httpError(403);
1424
1425
		Requirements::css(FRAMEWORK_DIR . '/css/UploadField.css');
1426
1427
		return $this->customise(array(
1428
			'Form' => $this->EditForm()
1429
		))->renderWith($this->parent->getTemplateFileEdit());
1430
	}
1431
1432
	/**
1433
	 * @return Form
1434
	 */
1435
	public function EditForm() {
1436
		$file = $this->getItem();
1437
		if(!$file) return $this->httpError(404);
1438
		if($file instanceof Folder) return $this->httpError(403);
1439
		if(!$file->canEdit()) return $this->httpError(403);
1440
1441
		// Get form components
1442
		$fields = $this->parent->getFileEditFields($file);
1443
		$actions = $this->parent->getFileEditActions($file);
1444
		$validator = $this->parent->getFileEditValidator($file);
1445
		$form = new Form(
1446
			$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...
1447
			__FUNCTION__,
1448
			$fields,
1449
			$actions,
1450
			$validator
1451
		);
1452
		$form->loadDataFrom($file);
1453
		$form->addExtraClass('small');
1454
1455
		return $form;
1456
	}
1457
1458
	/**
1459
	 * @param array $data
1460
	 * @param Form $form
1461
	 * @param SS_HTTPRequest $request
1462
	 */
1463
	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...
1464
		// Check form field state
1465
		if($this->parent->isDisabled() || $this->parent->isReadonly()) return $this->httpError(403);
1466
1467
		// Check item permissions
1468
		$item = $this->getItem();
1469
		if(!$item) return $this->httpError(404);
1470
		if($item instanceof Folder) return $this->httpError(403);
1471
		if(!$item->canEdit()) return $this->httpError(403);
1472
1473
		$form->saveInto($item);
1474
		$item->write();
1475
1476
		$form->sessionMessage(_t('UploadField.Saved', 'Saved'), 'good');
1477
1478
		return $this->edit($request);
1479
	}
1480
1481
}
1482
1483
/**
1484
 * File selection popup for attaching existing files.
1485
 *
1486
 * @package forms
1487
 * @subpackages fields-files
1488
 */
1489
class UploadField_SelectHandler extends RequestHandler {
1490
1491
	/**
1492
	 * @var UploadField
1493
	 */
1494
	protected $parent;
1495
1496
	/**
1497
	 * @var string
1498
	 */
1499
	protected $folderName;
1500
1501
	/**
1502
	 * Set pagination quantity for file list field
1503
	 *
1504
	 * @config
1505
	 * @var int
1506
	 */
1507
	private static $page_size = 11; 
1508
1509
	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...
1510
		'$Action!' => '$Action',
1511
		'' => 'index',
1512
	);
1513
1514
	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...
1515
		'Form'
1516
	);
1517
1518
	public function __construct($parent, $folderName = null) {
1519
		$this->parent = $parent;
1520
		$this->folderName = $folderName;
1521
1522
		parent::__construct();
1523
	}
1524
1525
	public function index() {
1526
		// Requires a separate JS file, because we can't reach into the iframe with entwine.
1527
		Requirements::javascript(FRAMEWORK_DIR . '/javascript/UploadField_select.js');
1528
		return $this->renderWith('CMSDialog');
1529
	}
1530
1531
	/**
1532
	 * @param string $action
1533
	 * @return string
1534
	 */
1535
	public function Link($action = null) {
1536
		return Controller::join_links($this->parent->Link(), '/select/', $action);
1537
	}
1538
1539
	/**
1540
	 * Build the file selection form.
1541
	 *
1542
	 * @return Form
1543
	 */
1544
	public function Form() {
1545
		// Find out the requested folder ID.
1546
		$folderID = $this->parent->getRequest()->requestVar('ParentID');
1547
		if ($folderID === null && $this->parent->getDisplayFolderName()) {
1548
			$folder = Folder::find_or_make($this->parent->getDisplayFolderName());
1549
			$folderID = $folder ? $folder->ID : 0;
1550
		}
1551
1552
		// Construct the form
1553
		$action = new FormAction('doAttach', _t('UploadField.AttachFile', 'Attach file(s)'));
1554
		$action->addExtraClass('ss-ui-action-constructive icon-accept');
1555
		$form = new Form(
1556
			$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...
1557
			'Form',
1558
			new FieldList($this->getListField($folderID)),
1559
			new FieldList($action)
1560
		);
1561
1562
		// Add a class so we can reach the form from the frontend.
1563
		$form->addExtraClass('uploadfield-form');
1564
1565
		return $form;
1566
	}
1567
1568
	/**
1569
	 * @param $folderID The ID of the folder to display.
1570
	 * @return FormField
1571
	 */
1572
	protected function getListField($folderID) {
1573
		// Generate the folder selection field.
1574
		$folderField = new TreeDropdownField('ParentID', _t('HtmlEditorField.FOLDER', 'Folder'), 'Folder');
1575
		$folderField->setValue($folderID);
1576
1577
		// Generate the file list field.
1578
		$config = GridFieldConfig::create();
1579
		$config->addComponent(new GridFieldSortableHeader());
1580
		$config->addComponent(new GridFieldFilterHeader());
1581
		$config->addComponent($colsComponent = new GridFieldDataColumns());
1582
		$colsComponent->setDisplayFields(array(
1583
			'StripThumbnail' => '',
1584
			'Title' => singleton('File')->fieldLabel('Title'),
1585
			'Created' => singleton('File')->fieldLabel('Created'),
1586
			'Size' => singleton('File')->fieldLabel('Size')
1587
		));
1588
		$colsComponent->setFieldCasting(array(
1589
			'Created' => 'SS_Datetime->Nice'
1590
		));
1591
1592
 		// Set configurable pagination for file list field  
1593
		$pageSize = Config::inst()->get(get_class($this), 'page_size');
1594
		$config->addComponent(new GridFieldPaginator($pageSize));
1595
1596
		// If relation is to be autoset, we need to make sure we only list compatible objects.
1597
		$baseClass = $this->parent->getRelationAutosetClass();
1598
1599
		// Create the data source for the list of files within the current directory.
1600
		$files = DataList::create($baseClass)->exclude('ClassName', 'Folder');
1601
		if($folderID) $files = $files->filter('ParentID', $folderID);
1602
1603
		$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...
1604
		$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...
1605
		if($this->parent->getAllowedMaxFileNumber() !== 1) {
1606
			$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...
1607
		}
1608
1609
		$selectComposite = new CompositeField(
1610
			$folderField,
1611
			$fileField
1612
		);
1613
1614
		return $selectComposite;
1615
	}
1616
1617
	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...
1618
		// Popup-window attach does not require server side action, as it is implemented via JS
1619
	}
1620
1621
}
1622