'crop', 'extension' => 'auto' ]; /** * @var boolean Allow the user to set a caption. */ public $useCaption = true; /** * @var boolean Automatically attaches the uploaded file on upload if the parent record exists instead of using deferred binding to attach on save of the parent record. Defaults to false. */ public $attachOnUpload = false; // // Object properties // /** * @inheritDoc */ protected $defaultAlias = 'fileupload'; /** * @var Form The embedded form for modifying the properties of the selected file */ protected $configFormWidget; /** * @inheritDoc */ public function init() { $this->maxFilesize = $this->getUploadMaxFilesize(); $this->fillFromConfig([ 'iconClass', 'prompt', 'imageWidth', 'imageHeight', 'fileTypes', 'maxFilesize', 'mimeTypes', 'thumbOptions', 'useCaption', 'attachOnUpload', ]); $this->iconClass = $this->iconClass ?? 'icon-upload'; if ($this->formField->disabled) { $this->previewMode = true; } $this->getConfigFormWidget(); } /** * @inheritDoc */ public function render() { $this->prepareVars(); return $this->makePartial('fileupload'); } /** * Prepares the view data */ protected function prepareVars() { if ($this->formField->disabled) { $this->previewMode = true; } if ($this->previewMode) { $this->useCaption = false; } if ($this->maxFilesize > $this->getUploadMaxFilesize()) { throw new ApplicationException('Maximum allowed size for uploaded files: ' . $this->getUploadMaxFilesize()); } $this->vars['fileList'] = $fileList = $this->getFileList(); $this->vars['singleFile'] = $fileList->first(); $this->vars['displayMode'] = $this->getDisplayMode(); $this->vars['emptyIcon'] = $this->getConfig('emptyIcon', 'icon-upload'); $this->vars['imageHeight'] = $this->imageHeight; $this->vars['imageWidth'] = $this->imageWidth; $this->vars['acceptedFileTypes'] = $this->getAcceptedFileTypes(true); $this->vars['maxFilesize'] = $this->maxFilesize; $this->vars['cssDimensions'] = $this->getCssDimensions(); $this->vars['cssBlockDimensions'] = $this->getCssDimensions('block'); $this->vars['useCaption'] = $this->useCaption; $this->vars['iconClass'] = $this->iconClass; $this->vars['prompt'] = $this->getPromptText(); } /** * Get the file record for this request, returns false if none available * * @return File|false */ protected function getFileRecord() { $record = false; if (!empty(post('file_id'))) { // Scope the lookup to this widget's own relation (including any files // bound via the current deferred-binding session) so that an // attacker-controlled file_id cannot reference an arbitrary // System\Models\File record belonging to another model. See // GHSA-3277-h8g9-qj5f. $record = $this->getRelationObject() ->withDeferred($this->sessionKey) ->find(post('file_id')) ?: false; } return $record; } /** * Get the instantiated config Form widget */ public function getConfigFormWidget(): Form { if ($this->configFormWidget) { return $this->configFormWidget; } $config = $this->makeConfig('~/modules/system/models/file/fields.yaml'); $config->model = $this->getFileRecord() ?: $this->getRelationModel(); $config->alias = $this->alias . $this->defaultAlias; $config->arrayName = $this->getFieldName(); $widget = $this->makeWidget(Form::class, $config); $widget->bindToController(); return $this->configFormWidget = $widget; } protected function getFileList() { $list = $this ->getRelationObject() ->withDeferred($this->sessionKey) ->orderBy('sort_order') ->get() ; /* * Decorate each file with thumb and custom download path */ $list->each(function ($file) { $this->decorateFileAttributes($file); }); return $list; } /** * Returns the display mode for the file upload. Eg: file-multi, image-single, etc. */ protected function getDisplayMode(): string { $mode = $this->getConfig('mode', 'image'); if (str_contains($mode, '-')) { return $mode; } $relationType = $this->getRelationType(); $mode .= ($relationType === 'attachMany' || $relationType === 'morphMany') ? '-multi' : '-single'; return $mode; } /** * Returns the escaped and translated prompt text to display according to the type. */ protected function getPromptText(): string { if ($this->prompt === null) { $isMulti = ends_with($this->getDisplayMode(), 'multi'); $this->prompt = $isMulti ? 'backend::lang.fileupload.upload_file' : 'backend::lang.fileupload.default_prompt'; } $uploadIconStr = sprintf('', $this->iconClass); return str_replace('%s', $uploadIconStr, e(trans($this->prompt))); } /** * Returns the CSS dimensions for the uploaded image, * uses auto where no dimension is provided. */ protected function getCssDimensions(?string $mode = null): string { if (!$this->imageWidth && !$this->imageHeight) { return ''; } $cssDimensions = ''; if ($mode == 'block') { $cssDimensions .= $this->imageWidth ? 'width: ' . $this->imageWidth . 'px;' : 'width: ' . $this->imageHeight . 'px;'; $cssDimensions .= ($this->imageHeight) ? 'max-height: ' . $this->imageHeight . 'px;' : 'height: auto;'; } else { $cssDimensions .= $this->imageWidth ? 'width: ' . $this->imageWidth . 'px;' : 'width: auto;'; $cssDimensions .= ($this->imageHeight) ? 'max-height: ' . $this->imageHeight . 'px;' : 'height: auto;'; } return $cssDimensions; } /** * Returns the specified accepted file types, or the default * based on the mode. Image mode will return: * - jpg,jpeg,bmp,png,gif,svg * @return string */ public function getAcceptedFileTypes($includeDot = false) { $types = $this->fileTypes; if ($types === false) { $isImage = starts_with($this->getDisplayMode(), 'image'); $types = implode(',', FileDefinitions::get($isImage ? 'imageExtensions' : 'defaultExtensions')); } if (!$types || $types == '*') { return null; } if (!is_array($types)) { $types = explode(',', $types); } $types = array_map(function ($value) use ($includeDot) { $value = trim($value); if (substr($value, 0, 1) == '.') { $value = substr($value, 1); } if ($includeDot) { $value = '.'.$value; } return $value; }, $types); return implode(',', $types); } /** * Removes a file attachment. */ public function onRemoveAttachment(): void { if ($file = $this->getFileRecord()) { $this->getRelationObject()->remove($file, $this->sessionKey); } } /** * Sorts file attachments. * * Expects (array) sortOrder [$fileId => $fileOrder] in the POST data. */ public function onSortAttachments(): void { if ($sortData = post('sortOrder')) { // Only reorder files that actually belong to this widget's relation // (including the current deferred-binding session), never arbitrary // System\Models\File rows referenced by a posted id. See // GHSA-3277-h8g9-qj5f. $keyName = $this->getRelationModel()->getKeyName(); $validIds = $this->getRelationObject() ->withDeferred($this->sessionKey) ->pluck($keyName) ->all(); $sortData = array_intersect_key($sortData, array_flip($validIds)); if (empty($sortData)) { return; } $ids = array_keys($sortData); $orders = array_values($sortData); $this->getRelationModel()->setSortableOrder($ids, $orders); } } /** * Loads the configuration form for an attachment, allowing title and description to be set. * * @throws ApplicationException if unable to find the file record */ public function onLoadAttachmentConfig(): string { if ($file = $this->getFileRecord()) { $file = $this->decorateFileAttributes($file); $this->vars['file'] = $file; $this->vars['displayMode'] = $this->getDisplayMode(); $this->vars['cssDimensions'] = $this->getCssDimensions(); $this->vars['parentElementId'] = $this->getId(); return $this->makePartial('config_form'); } throw new ApplicationException('Unable to find file, it may no longer exist'); } /** * Commit the changes of the attachment configuration form. */ public function onSaveAttachmentConfig() { try { $formWidget = $this->getConfigFormWidget(); if ($file = $formWidget->model) { $modelsToSave = $this->prepareModelsToSave($file, $formWidget->getSaveData()); Db::transaction(function () use ($modelsToSave, $formWidget) { foreach ($modelsToSave as $modelToSave) { $modelToSave->save(null, $formWidget->getSessionKey()); } }); return ['displayName' => $file->title ?: $file->file_name]; } throw new ApplicationException('Unable to find file, it may no longer exist'); } catch (Exception $ex) { return json_encode(['error' => $ex->getMessage()]); } } /** * @inheritDoc */ protected function loadAssets() { $this->addCss('css/fileupload.css', 'core'); $this->addJs('js/fileupload.js', 'core'); } /** * @inheritDoc */ public function getSaveValue($value) { return FormField::NO_SAVE_DATA; } /** * Upload handler for the server-side processing of uploaded files */ public function onUpload() { try { $file = $this->getRelationModel(); $fileRelation = $this->getRelationObject(); $file->is_public = $fileRelation->isPublic(); /** * @event backend.formwidgets.fileupload.onUpload * Provides an opportunity to process the file upload using custom logic. * * Example usage () */ if (!($data = Event::fire('backend.formwidgets.fileupload.onUpload', [$this, $file], true))) { if (!Input::hasFile('file_data')) { throw new ApplicationException('File missing from request'); } $validationRules = ['max:'.$file::getMaxFilesize()]; $data = Input::file('file_data'); if (!$data->isValid()) { throw new ApplicationException('File is not valid'); } if ($fileTypes = $this->getAcceptedFileTypes()) { $validationRules[] = 'extensions:'.$fileTypes; } if ($this->mimeTypes) { $validationRules[] = 'mimes:'.$this->mimeTypes; } $validation = Validator::make( ['file_data' => $data], ['file_data' => $validationRules] ); if ($validation->fails()) { throw new ValidationException($validation); } } $file->data = $data; $file->save(); /** * Attach directly to the parent model if it exists and attachOnUpload has been set to true * else attach via deferred binding */ $parent = $fileRelation->getParent(); if ($this->attachOnUpload && $parent && $parent->exists) { $fileRelation->add($file); } else { $fileRelation->add($file, $this->sessionKey); } $file = $this->decorateFileAttributes($file); $result = [ 'id' => $file->id, 'thumb' => $file->thumbUrl, 'path' => $file->pathUrl ]; $response = Response::make($result, 200); } catch (Exception $ex) { $response = Response::make($ex->getMessage(), 400); } return $response; } /** * Adds the bespoke attributes used internally by this widget. * - thumbUrl * - pathUrl * @return System\Models\File */ protected function decorateFileAttributes($file) { $path = $thumb = $file->getPath(); if ($this->imageWidth || $this->imageHeight) { $thumb = $file->getThumb($this->imageWidth, $this->imageHeight, $this->thumbOptions); } $file->pathUrl = $path; $file->thumbUrl = $thumb; return $file; } /** * Return max upload filesize in Mb * @return integer */ protected function getUploadMaxFilesize() { $size = ini_get('upload_max_filesize'); if (preg_match('/^([\d\.]+)([KMG])$/i', $size, $match)) { $pos = array_search(strtoupper($match[2]), ['K', 'M', 'G']); if ($pos !== false) { $size = $match[1] * pow(1024, $pos + 1); } } return floor($size / 1024 / 1024); } }