feat: VivesPOS landing on Winter CMS 1.2 — theme + plugin + Dockerfile
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:
2026-08-21 19:29:00 -06:00
commit 1f72193a64
3266 changed files with 531480 additions and 0 deletions

View File

@@ -0,0 +1,129 @@
<?php namespace Backend\Traits;
/**
* Collapsable Widget Trait
* Adds collapse/expand item features to back-end widgets
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
trait CollapsableWidget
{
/**
* @var string The key name to use when storing collapsed states in the session.
*/
public $collapseSessionKey = 'groups';
/**
* @var array|false Memory cache of collapsed states.
*/
protected $collapseGroupStatusCache = false;
/**
* AJAX handler to toggle a collapsed state. This should take two post variables:
* - group: The collapsible group name
* - status: The state of the group. Usually a 1 or a 0.
*
* @return void
*/
public function onSetCollapseStatus()
{
$this->setCollapseStatus(post('group'), post('status'));
}
/**
* Returns the array of all collapsed states belonging to this widget.
*
* @return array
*/
protected function getCollapseStatuses()
{
if ($this->collapseGroupStatusCache !== false) {
return $this->collapseGroupStatusCache;
}
$groups = $this->getSession($this->collapseSessionKey, []);
if (!is_array($groups)) {
return $this->collapseGroupStatusCache = [];
}
return $this->collapseGroupStatusCache = $groups;
}
/**
* Sets a collapsed state.
*
* @param string $group
* @param string $status
*/
protected function setCollapseStatus($group, $status)
{
$statuses = $this->getCollapseStatuses();
$statuses[$group] = $status;
$this->collapseGroupStatusCache = $statuses;
$this->putSession($this->collapseSessionKey, $statuses);
}
/**
* Gets a collapsed state.
*
* @param string $group
* @param bool $default
* @return bool|string
*/
protected function getCollapseStatus($group, $default = true)
{
$statuses = $this->getCollapseStatuses();
if (array_key_exists($group, $statuses)) {
return $statuses[$group];
}
return $default;
}
//
// Deprecations, remove if year >= 2019
//
/**
* @deprecated onGroupStatusUpdate is deprecated. Please update onSetCollapseStatus instead.
*/
public function onGroupStatusUpdate()
{
traceLog('onGroupStatusUpdate is deprecated. Please update onSetCollapseStatus instead. Class: '.get_class($this));
$this->onSetCollapseStatus();
}
/**
* @deprecated - getGroupStatuses is deprecated. Please update getCollapseStatuses instead.
*/
protected function getGroupStatuses()
{
traceLog('getGroupStatuses is deprecated. Please update getCollapseStatuses instead. Class: '.get_class($this));
return $this->getCollapseStatuses();
}
/**
* @deprecated - setGroupStatus is deprecated. Please update setCollapseStatus instead.
*/
protected function setGroupStatus($group, $status)
{
traceLog('setGroupStatus is deprecated. Please update setCollapseStatus instead. Class: '.get_class($this));
return $this->setCollapseStatus($group, $status);
}
/**
* @deprecated - getGroupStatus is deprecated. Please update getCollapseStatus instead.
*/
protected function getGroupStatus($group, $default = true)
{
traceLog('getGroupStatus is deprecated. Please update getCollapseStatus instead. Class: '.get_class($this));
return $this->getCollapseStatus($group, $default);
}
}

View File

@@ -0,0 +1,54 @@
<?php namespace Backend\Traits;
use Exception;
use System\Classes\ErrorHandler;
use System\Models\EventLog;
use Winter\Storm\Exception\ApplicationException;
/**
* Error Maker Trait
* Adds exception based methods to a class, goes well with `System\Traits\ViewMaker`.
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
trait ErrorMaker
{
/**
* @var string Object used for storing a fatal error.
*/
protected $fatalError;
/**
* @return boolean Whether a fatal error has been set or not.
*/
public function hasFatalError()
{
return !is_null($this->fatalError);
}
/**
* @return string The fatal error message
*/
public function getFatalError()
{
return $this->fatalError;
}
/**
* Sets standard page variables in the case of a controller error.
*/
public function handleError($exception)
{
if (
$exception instanceof Exception
&& !($exception instanceof ApplicationException)
) {
EventLog::addException($exception);
}
$errorMessage = ErrorHandler::getDetailedMessage($exception);
$this->fatalError = $errorMessage;
$this->vars['fatalError'] = $errorMessage;
}
}

View File

@@ -0,0 +1,124 @@
<?php namespace Backend\Traits;
use Str;
use Backend\Classes\FormField;
use Winter\Storm\Halcyon\Model as HalcyonModel;
/**
* Implements special logic for processing form data, typically from from postback, and
* filling the model attributes and attributes of any related models. This is a
* customized, safer and simplified version of `$model->push()`.
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
trait FormModelSaver
{
/**
* @var array List of prepared models that require saving.
*/
protected $modelsToSave = [];
/**
* Takes a model and fills it with data from a multidimensional array.
* If an attribute is found to be a relationship, that relationship
* is also filled.
*
* $modelsToSave = $this->prepareModelsToSave($model, [...]);
*
* foreach ($modelsToSave as $modelToSave) {
* $modelToSave->save();
* }
*
* @param \Winter\Storm\Database\Model $model Model to fill.
* @param array $saveData Attribute values to fill model.
* @return array The collection of models to save.
*/
protected function prepareModelsToSave($model, $saveData)
{
$this->modelsToSave = [];
$this->setModelAttributes($model, $saveData);
$this->modelsToSave = array_reverse($this->modelsToSave);
return $this->modelsToSave;
}
/**
* Sets a data collection to a model attributes, relations are also set.
*
* @param \Winter\Storm\Database\Model $model Model to fill.
* @param array $saveData Attribute values to fill model.
* @return void
*/
protected function setModelAttributes($model, $saveData)
{
$this->modelsToSave[] = $model;
if (!is_array($saveData)) {
return;
}
if ($model instanceof HalcyonModel) {
$model->fill($saveData);
return;
}
$attributesToPurge = [];
$singularTypes = ['belongsTo', 'hasOne', 'morphTo', 'morphOne'];
foreach ($saveData as $attribute => $value) {
$isNested = $attribute == 'pivot' || (
$model->hasRelation($attribute) &&
in_array($model->getRelationType($attribute), $singularTypes)
);
if ($isNested && is_array($value)) {
// Handle related records that don't exist yet
if (!$model->{$attribute} && $model->hasRelation($attribute)) {
$model->{$attribute} = $model->{$attribute}()->getRelated();
}
$this->setModelAttributes($model->{$attribute}, $value);
}
elseif ($value !== FormField::NO_SAVE_DATA) {
if (Str::startsWith($attribute, '_')) {
$attributesToPurge[] = $attribute;
}
$model->{$attribute} = $value;
}
}
if ($attributesToPurge) {
$this->deferPurgedSaveAttributes($model, $attributesToPurge);
}
}
/**
* Removes an array of attributes from the model. If the model implements
* the Purgeable trait, this is preferred over the internal logic.
*
* @param \Winter\Storm\Database\Model $model Model to adjust.
* @param array $attributesToPurge Attribute values to remove from the model.
* @return void
*/
protected function deferPurgedSaveAttributes($model, $attributesToPurge)
{
if (!is_array($attributesToPurge)) {
return;
}
/*
* Compatibility with Purgeable trait:
* This will give the ability to restore purged attributes
* and make them available again if necessary.
*/
if (method_exists($model, 'getPurgeableAttributes')) {
$model->addPurgeable($attributesToPurge);
}
else {
$model->bindEventOnce('model.saveInternal', function () use ($model, $attributesToPurge) {
foreach ($attributesToPurge as $attribute) {
unset($model->attributes[$attribute]);
}
});
}
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace Backend\Traits;
use Exception;
use Illuminate\Support\Facades\Lang;
use Winter\Storm\Database\Model;
use Winter\Storm\Database\Relations\Relation;
use Winter\Storm\Exception\ApplicationException;
/**
* Form Model Widget Trait
*
* Special logic for for form widgets that use a database stored model.
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
trait FormModelWidget
{
/**
* Returns the final model and attribute name of a nested HTML array attribute.
* Eg: list($model, $attribute) = $this->resolveModelAttribute($this->valueFrom);
* @throws ApplicationException if something goes wrong when attempting to resolve the model attribute
*/
public function resolveModelAttribute(string $attribute): array
{
try {
return $this->formField->resolveModelAttribute($this->model, $attribute);
}
catch (Exception $ex) {
throw new ApplicationException(Lang::get('backend::lang.model.missing_relation', [
'class' => get_class($this->model),
'relation' => $attribute
]));
}
}
/**
* Returns the model of a relation type, supports nesting via HTML array.
* @throws ApplicationException if the related model cannot be resolved
*/
public function getRelationModel(): Model
{
list($model, $attribute) = $this->resolveModelAttribute($this->valueFrom);
if (!$model) {
throw new ApplicationException(Lang::get('backend::lang.model.missing_relation', [
'class' => get_class($this->model),
'relation' => $this->valueFrom
]));
}
if (!$model->hasRelation($attribute)) {
throw new ApplicationException(Lang::get('backend::lang.model.missing_relation', [
'class' => get_class($model),
'relation' => $attribute
]));
}
return $model->makeRelation($attribute);
}
/**
* Returns the value as a relation object from the model, supports nesting via HTML array.
* @throws ApplicationException if the relationship cannot be resolved
* @return Relation
*/
protected function getRelationObject()
{
list($model, $attribute) = $this->resolveModelAttribute($this->valueFrom);
if (!$model) {
throw new ApplicationException(Lang::get('backend::lang.model.missing_relation', [
'class' => get_class($this->model),
'relation' => $this->valueFrom
]));
}
if (!$model->hasRelation($attribute)) {
throw new ApplicationException(Lang::get('backend::lang.model.missing_relation', [
'class' => get_class($model),
'relation' => $attribute
]));
}
return $model->{$attribute}();
}
/**
* Returns the value as a relation type from the model, supports nesting via HTML array.
*/
protected function getRelationType(): ?string
{
list($model, $attribute) = $this->resolveModelAttribute($this->valueFrom);
return $model->getRelationType($attribute);
}
}

View File

@@ -0,0 +1,89 @@
<?php namespace Backend\Traits;
use Lang;
use Request;
use ApplicationException;
/**
* Inspectable Container Trait
* Extension for controllers that can host inspectable widgets (Components, etc.)
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
trait InspectableContainer
{
public function onInspectableGetOptions()
{
// Disable asset broadcasting
$this->flushAssets();
$property = trim(Request::input('inspectorProperty'));
if (!$property) {
throw new ApplicationException('The property name is not specified.');
}
$className = trim(Request::input('inspectorClassName'));
if (!$className) {
throw new ApplicationException('The inspectable class name is not specified.');
}
$traitFound = in_array('System\Traits\PropertyContainer', class_uses_recursive($className));
if (!$traitFound) {
throw new ApplicationException('The options cannot be loaded for the specified class.');
}
// Determine constructor requirements and pass the default value (or NULL) to all required arguments
$reflection = new \ReflectionClass($className);
$constructor = $reflection->getConstructor();
if (is_null($constructor) || !count($constructor->getParameters())) {
$obj = new $className();
} else {
$args = $constructor->getParameters();
$passed = [];
foreach ($args as $arg) {
if ($arg->isOptional()) {
$passed[] = $arg->getDefaultValue();
continue;
}
$passed[] = null;
}
$obj = $reflection->newInstanceArgs($passed);
}
// Nested properties have names like object.property.
// Convert them to Object.Property.
$propertyNameParts = explode('.', $property);
$propertyMethodName = '';
foreach ($propertyNameParts as $part) {
$part = trim($part);
if (!strlen($part)) {
continue;
}
$propertyMethodName .= ucfirst($part);
}
$methodName = 'get'.$propertyMethodName.'Options';
if (method_exists($obj, $methodName)) {
$options = $obj->$methodName();
}
else {
$options = $obj->getPropertyOptions($property);
}
/*
* Convert to array to retain the sort order in JavaScript
*/
$optionsArray = [];
foreach ((array) $options as $value => $title) {
$optionsArray[] = ['value' => $value, 'title' => Lang::get($title)];
}
return [
'options' => $optionsArray
];
}
}

View File

@@ -0,0 +1,139 @@
<?php namespace Backend\Traits;
use Str;
use Backend\Models\UserPreference;
/**
* Preference Maker Trait
*
* Adds methods for modifying user preferences in a controller class, or a class
* that contains a `$controller` property referencing a controller.
*/
trait PreferenceMaker
{
/**
* Cache for retrieved user preferences.
*
* @var array
*/
protected static $preferenceCache = [];
/**
* Saves a widget related key/value pair in to the users preferences
* @param string $key Unique key for the data store.
* @param mixed $value The value to store.
* @return void
*/
public function putUserPreference(string $key, $value)
{
$preferences = $this->getUserPreferences();
$preferences[$key] = $value;
$this->getPreferenceStorage()->set($this->getPreferenceKey(), $preferences);
// Re-cache user preferences
self::$preferenceCache[$this->getPreferenceKey()] = $preferences;
}
/**
* Retrieves a widget related key/value pair from the user preferences
*
* @param string $key Unique key for the data store.
* @param mixed $default A default value to use when value is not found.
* @return mixed
*/
public function getUserPreference(?string $key = null, $default = null)
{
$preferences = $this->getUserPreferences();
return (isset($preferences[$key])) ? $preferences[$key] : $default;
}
/**
* Retrieves and caches all user preferences for this particular controller/widget.
*
* @return array
*/
public function getUserPreferences()
{
if (isset(self::$preferenceCache[$this->getPreferenceKey()])) {
return self::$preferenceCache[$this->getPreferenceKey()];
}
$preferences = $this->getPreferenceStorage()->get($this->getPreferenceKey(), []);
// Cache user preferences
self::$preferenceCache[$this->getPreferenceKey()] = $preferences;
return $preferences;
}
/**
* Clears a single preference key from the user preferences for this controller/widget.
*
* @param string $key Unique key for the data store.
* @return void
*/
public function clearUserPreference(string $key)
{
$preferences = $this->getUserPreferences();
if (!isset($preferences[$key])) {
return;
}
unset($preferences[$key]);
if (count($preferences)) {
$this->getPreferenceStorage()->set($this->getPreferenceKey(), $preferences);
// Re-cache user preferences
self::$preferenceCache[$this->getPreferenceKey()] = $preferences;
} else {
// Remove record from user preferences
$this->clearUserPreferences();
}
}
/**
* Clears all user preferences for this controller/widget.
*
* @return void
*/
public function clearUserPreferences()
{
$this->getPreferenceStorage()->reset($this->getPreferenceKey());
self::$preferenceCache[$this->getPreferenceKey()] = [];
}
/**
* Returns a unique identifier for this widget and controller action for preference storage.
*
* @return string
*/
protected function getPreferenceKey()
{
$controller = (property_exists($this, 'controller') && $this->controller)
? $this->controller
: $this;
$uniqueId = (method_exists($this, 'getId')) ? $this->getId() : $controller->getId();
// Removes Class name and "Controllers" directory
$rootNamespace = Str::getClassId(Str::getClassNamespace(Str::getClassNamespace($controller)));
// The controller action is intentionally omitted, preferences should be shared for all actions
return $rootNamespace . '::' . strtolower(class_basename($controller)) . '.' . strtolower($uniqueId);
}
/**
* Specifies the model used for storing the user preferences.
*
* @return Winter\Storm\Database\Model
*/
protected function getPreferenceStorage()
{
return UserPreference::forUser();
}
}

View File

@@ -0,0 +1,43 @@
<?php namespace Backend\Traits;
use Str;
/**
* Searchable Widget Trait
* Adds search features to back-end widgets
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
trait SearchableWidget
{
protected $searchTerm = false;
protected function getSearchTerm()
{
return $this->searchTerm !== false ? $this->searchTerm : $this->getSession('search');
}
protected function setSearchTerm($term)
{
$this->searchTerm = trim($term);
$this->putSession('search', $this->searchTerm);
}
protected function textMatchesSearch(&$words, $text)
{
foreach ($words as $word) {
$word = trim($word);
if (!strlen($word)) {
continue;
}
if (Str::contains(Str::lower($text), $word)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,69 @@
<?php namespace Backend\Traits;
use Input;
/**
* Selectable Widget Trait
* Adds item selection features to back-end widgets
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
trait SelectableWidget
{
protected $selectedItemsCache = false;
protected $selectionInputName = 'object';
public function onSelect()
{
$this->extendSelection();
}
protected function getSelectedItems()
{
if ($this->selectedItemsCache !== false) {
return $this->selectedItemsCache;
}
$items = $this->getSession('selected', []);
if (!is_array($items)) {
return $this->selectedItemsCache = [];
}
return $this->selectedItemsCache = $items;
}
protected function extendSelection()
{
$items = (array) Input::get($this->selectionInputName, []);
$currentSelection = $this->getSelectedItems();
$this->putSession('selected', $currentSelection + $items);
}
protected function resetSelection()
{
$this->putSession('selected', []);
}
protected function removeSelection($itemId)
{
$currentSelection = $this->getSelectedItems();
unset($currentSelection[$itemId]);
$this->putSession('selected', $currentSelection);
$this->selectedItemsCache = $currentSelection;
}
protected function isItemSelected($itemId)
{
$selectedItems = $this->getSelectedItems();
if (!is_array($selectedItems) || !isset($selectedItems[$itemId])) {
return false;
}
return $selectedItems[$itemId];
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace Backend\Traits;
use Illuminate\Support\Facades\Session;
use Winter\Storm\Support\Str;
/**
* Session Maker Trait
*
* Adds session management based methods to a controller class, or a class
* that contains a `$controller` property referencing a controller.
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
trait SessionMaker
{
/**
* Saves a widget related key/value pair in to session data.
* @param string $key Unique key for the data store.
* @param mixed $value The value to store.
* @return void
*/
protected function putSession($key, $value)
{
$sessionId = $this->makeSessionId();
$currentStore = $this->getSession();
$currentStore[$key] = $value;
Session::put($sessionId, base64_encode(serialize($currentStore)));
}
/**
* Retrieves a widget related key/value pair from session data.
* @param string $key Unique key for the data store.
* @param string $default A default value to use when value is not found.
* @return string
*/
protected function getSession($key = null, $default = null)
{
$sessionId = $this->makeSessionId();
$currentStore = [];
if (
Session::has($sessionId) &&
($cached = @unserialize(@base64_decode(Session::get($sessionId)))) !== false
) {
$currentStore = $cached;
}
if ($key === null) {
return $currentStore;
}
return $currentStore[$key] ?? $default;
}
/**
* Returns a unique session identifier for this widget and controller action.
* @return string
*/
protected function makeSessionId()
{
$controller = property_exists($this, 'controller') && $this->controller
? $this->controller
: $this;
$uniqueId = method_exists($this, 'getId') ? $this->getId() : $controller->getId();
// Removes Class name and "Controllers" directory
$rootNamespace = Str::getClassId(Str::getClassNamespace(Str::getClassNamespace($controller)));
// The controller action is intentionally omitted, session should be shared for all actions
return 'widget.' . $rootNamespace . '-' . class_basename($controller) . '-' . $uniqueId;
}
/**
* Resets all session data related to this widget.
* @return void
*/
public function resetSession()
{
$sessionId = $this->makeSessionId();
Session::forget($sessionId);
}
}

View File

@@ -0,0 +1,249 @@
<?php namespace Backend\Traits;
use ApplicationException;
use Event;
use File;
use Illuminate\Filesystem\FilesystemAdapter;
use Lang;
use Request;
use Response;
use Str;
use System\Classes\MediaLibrary;
use Winter\Storm\Filesystem\Definitions as FileDefinitions;
use Winter\Storm\Support\Svg;
/**
* Uploadable Widget Trait
* Adds media library upload features to back-end widgets
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
trait UploadableWidget
{
// /**
// * @var string Path in the Media Library where uploaded files should be stored. If empty it will be pulled from Request::input('path');
// */
// public $uploadPath;
/**
* Returns the disk that will be used to store the uploaded file
*/
public function uploadableGetDisk(): FilesystemAdapter
{
return MediaLibrary::instance()->getStorageDisk();
}
/**
* Returns the path on the disk to store the uploaded file
*/
public function uploadableGetUploadPath(string $fileName): string
{
// Use the configured upload path unless it's null, in which case use the user-provided path
$path = !empty($this->uploadPath) ? $this->uploadPath : Request::input('path');
$path = MediaLibrary::validatePath($path);
$path = MediaLibrary::instance()->getMediaPath($path);
$filePath = rtrim($path, '/') . '/' . $fileName;
return $filePath;
}
/**
* Returns the URL to the uploaded file
*
* @TODO: Replace cms.storage system with real disks
*/
public function uploadableGetUploadUrl(string $diskPath): string
{
// Get the media folder
$storageFolder = MediaLibrary::instance()->getMediaPath('');
// Remove the media folder from the provided disk path since it already has it
$url = MediaLibrary::url(Str::after($diskPath, $storageFolder));
return $url;
}
/**
* Process file uploads submitted via AJAX
*
* @throws ApplicationException If the file "file_data" wasn't detected in the request or if the file failed to pass validation / security checks
*/
public function onUpload(): ?\Illuminate\Http\Response
{
if ($this->readOnly) {
return null;
}
/**
* @event backend.widgets.uploadable.onUpload
* Provides an opportunity to process the file upload using custom logic.
*
* Example usage ()
*/
if ($result = Event::fire('backend.widgets.uploadable.onUpload', [$this], true)) {
return $result;
}
return $this->onUploadDirect();
}
protected function onUploadDirect(): \Illuminate\Http\Response
{
if (!Request::hasFile('file_data')) {
throw new ApplicationException('File missing from request');
}
try {
$uploadedFile = Request::file('file_data');
$fileName = $this->validateMediaFileName(
$uploadedFile->getClientOriginalName(),
$uploadedFile->getClientOriginalExtension()
);
/*
* See mime type handling in the asset manager
*/
if (!$uploadedFile->isValid()) {
if ($uploadedFile->getError() === UPLOAD_ERR_OK) {
$message = "The file \"{$uploadedFile->getClientOriginalName()}\" uploaded successfully but wasn't "
. "available at {$uploadedFile->getPathName()}. Check to make sure that nothing moved it away.";
} else {
$message = $uploadedFile->getErrorMessage();
}
throw new ApplicationException($message);
}
/*
* getRealPath() can be empty for some environments (IIS)
*/
$sourcePath = empty(trim($uploadedFile->getRealPath()))
? $uploadedFile->getPath() . DIRECTORY_SEPARATOR . $uploadedFile->getFileName()
: $uploadedFile->getRealPath();
$filePath = $this->uploadableGetUploadPath($fileName);
// Filter SVG files
if (pathinfo($filePath, PATHINFO_EXTENSION) === 'svg') {
file_put_contents($sourcePath, Svg::extract($sourcePath));
}
$this->uploadableGetDisk()->put($filePath, File::get($sourcePath));
/**
* @event media.file.upload
* Called after a file is uploaded
*
* Example usage:
*
* Event::listen('media.file.upload', function ((\Backend\Widgets\MediaManager) $mediaWidget, (string) &$path, (\Symfony\Component\HttpFoundation\File\UploadedFile) $uploadedFile) {
* \Log::info($path . " was upoaded.");
* });
*
* Or
*
* $mediaWidget->bindEvent('file.upload', function ((string) &$path, (\Symfony\Component\HttpFoundation\File\UploadedFile) $uploadedFile) {
* \Log::info($path . " was uploaded");
* });
*
*/
$this->fireSystemEvent('media.file.upload', [&$filePath, $uploadedFile]);
$response = Response::make([
'link' => $this->uploadableGetUploadUrl($filePath),
'result' => 'success'
]);
} catch (\Exception $ex) {
throw new ApplicationException($ex->getMessage());
}
return $response;
}
public function validateMediaFileName(string $fileName, string $extension): string
{
/*
* Convert uppcare case file extensions to lower case
*/
$extension = strtolower($extension);
$fileName = File::name($fileName).'.'.$extension;
/*
* File name contains non-latin characters, attempt to slug the value
*/
if (!$this->validateFileName($fileName)) {
$fileName = $this->cleanFileName(File::name($fileName)) . '.' . $extension;
}
/*
* Check for unsafe file extensions
*/
if (!$this->validateFileType($fileName)) {
throw new ApplicationException(Lang::get('backend::lang.media.type_blocked'));
}
return $fileName;
}
/**
* Validate a proposed media item file name.
*
* @param string
* @return bool
*/
protected function validateFileName($name): bool
{
if (!preg_match('/^[\w@\.\s_\-]+$/iu', $name)) {
return false;
}
if (strpos($name, '..') !== false) {
return false;
}
return true;
}
/**
* Check for blocked / unsafe file extensions
*
* @param string
* @return bool
*/
protected function validateFileType($name): bool
{
$extension = strtolower(File::extension($name));
$allowedFileTypes = FileDefinitions::get('defaultExtensions');
if (!in_array($extension, $allowedFileTypes)) {
return false;
}
return true;
}
/**
* Creates a slug form the string. A modified version of Str::slug
* with the main difference that it accepts @-signs
*
* @param string $name
* @return string
*/
protected function cleanFileName($name)
{
$title = Str::ascii($name);
// Convert all dashes/underscores into separator
$flip = $separator = '-';
$title = preg_replace('!['.preg_quote($flip).']+!u', $separator, $title);
// Remove all characters that are not the separator, letters, numbers, whitespace or @.
$title = preg_replace('![^'.preg_quote($separator).'\pL\pN\s@]+!u', '', mb_strtolower($title));
// Replace all separator characters and whitespace by a single separator
$title = preg_replace('!['.preg_quote($separator).'\s]+!u', $separator, $title);
return trim($title, $separator);
}
}

View File

@@ -0,0 +1,75 @@
<?php namespace Backend\Traits;
use Lang;
use Backend\Classes\FormField;
use SystemException;
/**
* Widget Maker Trait
*
* Adds widget based methods to a controller class, or a class that
* contains a `$controller` property referencing a controller.
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
trait WidgetMaker
{
/**
* Makes a widget object with the supplied configuration file.
* @param string $class Widget class name
* @param array $widgetConfig An array of config.
* @return mixed|\Backend\Classes\WidgetBase The widget object
*/
public function makeWidget($class, $widgetConfig = [])
{
$controller = property_exists($this, 'controller') && $this->controller
? $this->controller
: $this;
if (!class_exists($class)) {
throw new SystemException(Lang::get('backend::lang.widget.not_registered', [
'name' => $class
]));
}
return new $class($controller, $widgetConfig);
}
/**
* Makes a form widget object with the supplied form field and widget configuration.
* @param string $class Widget class name
* @param mixed $fieldConfig A field name, an array of config or a FormField object.
* @param array $widgetConfig An array of config.
* @return \Backend\Classes\FormWidgetBase The widget object
*/
public function makeFormWidget($class, $fieldConfig = [], $widgetConfig = [])
{
$controller = property_exists($this, 'controller') && $this->controller
? $this->controller
: $this;
if (!class_exists($class)) {
throw new SystemException(Lang::get('backend::lang.widget.not_registered', [
'name' => $class
]));
}
if (is_string($fieldConfig)) {
$fieldConfig = ['name' => $fieldConfig];
}
if (is_array($fieldConfig)) {
$formField = new FormField(
array_get($fieldConfig, 'name'),
array_get($fieldConfig, 'label')
);
$formField->displayAs('widget', $fieldConfig);
}
else {
$formField = $fieldConfig;
}
return new $class($controller, $formField, $widgetConfig);
}
}