feat: VivesPOS landing on Winter CMS 1.2 — theme + plugin + Dockerfile
Some checks are pending
Module sub-split / Sub-split (push) Waiting to run
Some checks are pending
Module sub-split / Sub-split (push) Waiting to run
- Base: wintercms/winter branch 1.2 (full framework) - Theme vivespos: Canvas 7 + Bootstrap 5 CDN, custom CSS - Layout: deferred GTM/GA4 tracking, JSON-LD SoftwareApplication - Partials: hero (offline-first), features, modes (offline/nube toggle), screenshots, pricing (3 planes), comparison, FAQ, CTA - Plugin VivesPOS.Site with ContactForm - Dockerfile: PHP 8.2 Apache, port 80, healthcheck - Added winter/wn-pages, blog, sitemap, seo plugins - Active theme set to vivespos
This commit is contained in:
561
modules/backend/formwidgets/FileUpload.php
Normal file
561
modules/backend/formwidgets/FileUpload.php
Normal file
@@ -0,0 +1,561 @@
|
||||
<?php
|
||||
|
||||
namespace Backend\FormWidgets;
|
||||
|
||||
use Backend\Classes\FormField;
|
||||
use Backend\Classes\FormWidgetBase;
|
||||
use Backend\Widgets\Form;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Response;
|
||||
use System\Models\File;
|
||||
use Winter\Storm\Exception\ApplicationException;
|
||||
use Winter\Storm\Exception\ValidationException;
|
||||
use Winter\Storm\Filesystem\Definitions as FileDefinitions;
|
||||
use Winter\Storm\Support\Facades\DB;
|
||||
use Winter\Storm\Support\Facades\Event;
|
||||
use Winter\Storm\Support\Facades\Input;
|
||||
use Winter\Storm\Support\Facades\Validator;
|
||||
|
||||
/**
|
||||
* File upload field
|
||||
* Renders a form file uploader field.
|
||||
*
|
||||
* Supported options:
|
||||
* - mode: image-single, image-multi, file-single, file-multi
|
||||
* - upload-label: Add file
|
||||
* - empty-label: No file uploaded
|
||||
*
|
||||
* @package winter\wn-backend-module
|
||||
* @author Alexey Bobkov, Samuel Georges
|
||||
*/
|
||||
class FileUpload extends FormWidgetBase
|
||||
{
|
||||
use \Backend\Traits\FormModelSaver;
|
||||
use \Backend\Traits\FormModelWidget;
|
||||
|
||||
//
|
||||
// Configurable properties
|
||||
//
|
||||
|
||||
/**
|
||||
* @var string Icon class to use for the upload button.
|
||||
*/
|
||||
public $iconClass;
|
||||
|
||||
/**
|
||||
* @var string Prompt text to display for the upload button.
|
||||
*/
|
||||
public $prompt;
|
||||
|
||||
/**
|
||||
* @var int Preview image width
|
||||
*/
|
||||
public $imageWidth;
|
||||
|
||||
/**
|
||||
* @var int Preview image height
|
||||
*/
|
||||
public $imageHeight;
|
||||
|
||||
/**
|
||||
* @var mixed Collection of acceptable file types.
|
||||
*/
|
||||
public $fileTypes = false;
|
||||
|
||||
/**
|
||||
* @var mixed Collection of acceptable mime types.
|
||||
*/
|
||||
public $mimeTypes = false;
|
||||
|
||||
/**
|
||||
* @var mixed Max file size.
|
||||
*/
|
||||
public $maxFilesize;
|
||||
|
||||
/**
|
||||
* @var array Options used for generating thumbnails.
|
||||
*/
|
||||
public $thumbOptions = [
|
||||
'mode' => '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('<i class="%s"></i>', $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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user