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 = array(); /** @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 ? array('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']) ? array() : $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 asignment 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 // for multiple file uploads $tmpFiles = array(); 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 = array(); foreach (array('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; } }