/
FileUploadReceiver.php
424 lines (381 loc) · 14.1 KB
/
FileUploadReceiver.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
<?php
namespace SilverStripe\Forms;
use Exception;
use InvalidArgumentException;
use SilverStripe\Assets\File;
use SilverStripe\Assets\Storage\AssetContainer;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DataObjectInterface;
use SilverStripe\ORM\RelationList;
use SilverStripe\ORM\SS_List;
use SilverStripe\ORM\UnsavedRelationList;
use SilverStripe\ORM\ValidationException;
/**
* Provides operations for reading and writing uploaded files to/from
* {@see File} dataobject instances.
* Allows writing to a parent record with the following relation types:
* - has_one
* - has_many
* - many_many
* Additionally supports writing directly to the File table not attached
* to any parent record.
*
* Note that this trait expects to be applied to a {@see FormField} class
*
* @mixin FormField
*/
trait FileUploadReceiver
{
use UploadReceiver;
/**
* Flag to automatically determine and save a has_one-relationship
* on the saved record (e.g. a "Player" has_one "PlayerImage" would
* trigger saving the ID of newly created file into "PlayerImageID"
* on the record).
*
* @var boolean
*/
public $relationAutoSetting = true;
/**
* Parent data record. Will be inferred from parent form or controller if blank.
*
* @var ?DataObject
*/
protected $record;
/**
* Items loaded into this field. May be a RelationList, or any other SS_List
*
* @var SS_List
*/
protected $items;
protected function constructFileUploadReceiver()
{
$this->constructUploadReceiver();
}
/**
* Force a record to be used as "Parent" for uploaded Files (eg a Page with a has_one to File)
*
* @param DataObject $record
* @return $this
*/
public function setRecord($record)
{
$this->record = $record;
return $this;
}
/**
* 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
* use Form->getRecord() or Form->Controller()->data()
*
* @return ?DataObject
*/
public function getRecord()
{
if ($this->record) {
return $this->record;
}
if (!$this->getForm()) {
return null;
}
// Get record from form
$record = $this->getForm()->getRecord();
if ($record && ($record instanceof DataObject)) {
$this->record = $record;
return $record;
}
// Get record from controller
$controller = $this->getForm()->getController();
if ($controller
&& $controller->hasMethod('data')
&& ($record = $controller->data())
&& ($record instanceof DataObject)
) {
$this->record = $record;
return $record;
}
return null;
}
/**
* Loads the related record values into this field. This can be uploaded
* in one of three ways:
*
* - By passing in a list of file IDs in the $value parameter (an array with a single
* key 'Files', with the value being the actual array of IDs).
* - By passing in an explicit list of File objects in the $record parameter, and
* leaving $value blank.
* - By passing in a dataobject in the $record parameter, from which file objects
* will be extracting using the field name as the relation field.
*
* Each of these methods will update both the items (list of File objects) and the
* field value (list of file ID values).
*
* @param array $value Array of submitted form data, if submitting from a form
* @param array|DataObject|SS_List $record Full source record, either as a DataObject,
* SS_List of items, or an array of submitted form data
* @return $this Self reference
* @throws ValidationException
*/
public function setValue($value, $record = null)
{
// If we're not passed a value directly, we can attempt to infer the field
// value from the second parameter by inspecting its relations
$items = new ArrayList();
// Determine format of presented data
if ($value instanceof File) {
$items = ArrayList::create([$value]);
$value = null;
} elseif ($value instanceof SS_List) {
$items = $value;
$value = null;
} elseif (empty($value) && $record) {
// If a record is given as a second parameter, but no submitted values,
// then we should inspect this instead for the form values
if (($record instanceof DataObject) && $record->hasMethod($this->getName())) {
// If given a dataobject use reflection to extract details
$data = $record->{$this->getName()}();
if ($data instanceof DataObject) {
// If has_one, add sole item to default list
$items->push($data);
} elseif ($data instanceof SS_List) {
// For many_many and has_many relations we can use the relation list directly
$items = $data;
}
} elseif ($record instanceof SS_List) {
// If directly passing a list then save the items directly
$items = $record;
}
} elseif (is_array($value) && !empty($value['Files'])) {
// If value is given as an array (such as a posted form), extract File IDs from this
$class = $this->getRelationAutosetClass();
$items = DataObject::get($class)->byIDs($value['Files']);
}
// If javascript is disabled, direct file upload (non-html5 style) can
// trigger a single or multiple file submission. Note that this may be
// included in addition to re-submitted File IDs as above, so these
// should be added to the list instead of operated on independently.
if ($uploadedFiles = $this->extractUploadedFileData($value)) {
foreach ($uploadedFiles as $tempFile) {
$file = $this->saveTemporaryFile($tempFile, $error);
if ($file) {
$items->add($file);
} else {
throw new ValidationException($error);
}
}
}
// Filter items by what's allowed to be viewed
$filteredItems = new ArrayList();
$fileIDs = [];
/** @var File $file */
foreach ($items as $file) {
if ($file->isInDB() && $file->canView()) {
$filteredItems->push($file);
$fileIDs[] = $file->ID;
}
}
// Filter and cache updated item list
$this->items = $filteredItems;
// Same format as posted form values for this field. Also ensures that
// $this->setValue($this->getValue()); is non-destructive
$value = $fileIDs ? ['Files' => $fileIDs] : null;
// Set value using parent
parent::setValue($value, $record);
return $this;
}
/**
* Sets the items assigned to this field as an SS_List of File objects.
* Calling setItems will also update the value of this field, as well as
* updating the internal list of File items.
*
* @param SS_List $items
* @return $this self reference
*/
public function setItems(SS_List $items)
{
return $this->setValue(null, $items);
}
/**
* Retrieves the current list of files
*
* @return SS_List|File[]
*/
public function getItems()
{
return $this->items ? $this->items : new ArrayList();
}
/**
* Retrieves the list of selected file IDs
*
* @return array
*/
public function getItemIDs()
{
$value = $this->Value();
return empty($value['Files']) ? [] : $value['Files'];
}
public function Value()
{
// Re-override FileField Value to use data value
return $this->dataValue();
}
/**
* @param DataObject|DataObjectInterface $record
* @return $this
*/
public function saveInto(DataObjectInterface $record)
{
// Check required relation details are available
$fieldname = $this->getName();
if (!$fieldname) {
return $this;
}
// Get details to save
$idList = $this->getItemIDs();
// Check type of relation
$relation = $record->hasMethod($fieldname) ? $record->$fieldname() : null;
if ($relation && ($relation instanceof RelationList || $relation instanceof UnsavedRelationList)) {
// has_many or many_many
$relation->setByIDList($idList);
} elseif ($class = DataObject::getSchema()->hasOneComponent(get_class($record), $fieldname)) {
// Assign has_one ID
$id = $idList ? reset($idList) : 0;
$record->{"{$fieldname}ID"} = $id;
// Polymorphic assignment
if ($class === DataObject::class) {
$file = $id ? File::get()->byID($id) : null;
$fileClass = $file ? get_class($file) : File::class;
$record->{"{$fieldname}Class"} = $id ? $fileClass : null;
}
}
return $this;
}
/**
* Loads the temporary file data into a File object
*
* @param array $tmpFile Temporary file data
* @param string $error Error message
* @return AssetContainer File object, or null if error
*/
protected function saveTemporaryFile($tmpFile, &$error = null)
{
// Determine container object
$error = null;
$fileObject = null;
if (empty($tmpFile)) {
$error = _t('SilverStripe\\Forms\\FileUploadReceiver.FIELDNOTSET', 'File information not found');
return null;
}
if ($tmpFile['error']) {
$this->getUpload()->validate($tmpFile);
$error = implode(' ' . PHP_EOL, $this->getUpload()->getErrors());
return null;
}
// Search for relations that can hold the uploaded files, but don't fallback
// to default if there is no automatic relation
if ($relationClass = $this->getRelationAutosetClass(null)) {
// Allow File to be subclassed
if ($relationClass === File::class && isset($tmpFile['name'])) {
$relationClass = File::get_class_for_file_extension(
File::get_file_extension($tmpFile['name'])
);
}
// Create new object explicitly. Otherwise rely on Upload::load to choose the class.
$fileObject = Injector::inst()->create($relationClass);
if (! ($fileObject instanceof DataObject) || !($fileObject instanceof AssetContainer)) {
throw new InvalidArgumentException("Invalid asset container $relationClass");
}
}
// Get the uploaded file into a new file object.
try {
$this->getUpload()->loadIntoFile($tmpFile, $fileObject, $this->getFolderName());
} catch (Exception $e) {
// we shouldn't get an error here, but just in case
$error = $e->getMessage();
return null;
}
// Check if upload field has an error
if ($this->getUpload()->isError()) {
$error = implode(' ' . PHP_EOL, $this->getUpload()->getErrors());
return null;
}
// return file
return $this->getUpload()->getFile();
}
/**
* Gets the foreign class that needs to be created, or 'File' as default if there
* is no relationship, or it cannot be determined.
*
* @param string $default Default value to return if no value could be calculated
* @return string Foreign class name.
*/
public function getRelationAutosetClass($default = File::class)
{
// Don't autodetermine relation if no relationship between parent record
if (!$this->getRelationAutoSetting()) {
return $default;
}
// Check record and name
$name = $this->getName();
$record = $this->getRecord();
if (empty($name) || empty($record)) {
return $default;
} else {
$class = $record->getRelationClass($name);
return empty($class) ? $default : $class;
}
}
/**
* Set if relation can be automatically assigned to the underlying dataobject
*
* @param bool $auto
* @return $this
*/
public function setRelationAutoSetting($auto)
{
$this->relationAutoSetting = $auto;
return $this;
}
/**
* Check if relation can be automatically assigned to the underlying dataobject
*
* @return bool
*/
public function getRelationAutoSetting()
{
return $this->relationAutoSetting;
}
/**
* Given an array of post variables, extract all temporary file data into an array
*
* @param array $postVars Array of posted form data
* @return array List of temporary file data
*/
protected function extractUploadedFileData($postVars)
{
// Note: Format of posted file parameters in php is a feature of using
// <input name='{$Name}[Uploads][]' /> for multiple file uploads
$tmpFiles = [];
if (!empty($postVars['tmp_name'])
&& is_array($postVars['tmp_name'])
&& !empty($postVars['tmp_name']['Uploads'])
) {
for ($i = 0; $i < count($postVars['tmp_name']['Uploads'] ?? []); $i++) {
// Skip if "empty" file
if (empty($postVars['tmp_name']['Uploads'][$i])) {
continue;
}
$tmpFile = [];
foreach (['name', 'type', 'tmp_name', 'error', 'size'] as $field) {
$tmpFile[$field] = $postVars[$field]['Uploads'][$i];
}
$tmpFiles[] = $tmpFile;
}
} elseif (!empty($postVars['tmp_name'])) {
// Fallback to allow single file uploads (method used by AssetUploadField)
$tmpFiles[] = $postVars;
}
return $tmpFiles;
}
}