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:
129
modules/backend/traits/CollapsableWidget.php
Normal file
129
modules/backend/traits/CollapsableWidget.php
Normal 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);
|
||||
}
|
||||
}
|
||||
54
modules/backend/traits/ErrorMaker.php
Normal file
54
modules/backend/traits/ErrorMaker.php
Normal 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;
|
||||
}
|
||||
}
|
||||
124
modules/backend/traits/FormModelSaver.php
Normal file
124
modules/backend/traits/FormModelSaver.php
Normal 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]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
98
modules/backend/traits/FormModelWidget.php
Normal file
98
modules/backend/traits/FormModelWidget.php
Normal 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);
|
||||
}
|
||||
}
|
||||
89
modules/backend/traits/InspectableContainer.php
Normal file
89
modules/backend/traits/InspectableContainer.php
Normal 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
|
||||
];
|
||||
}
|
||||
}
|
||||
139
modules/backend/traits/PreferenceMaker.php
Normal file
139
modules/backend/traits/PreferenceMaker.php
Normal 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();
|
||||
}
|
||||
}
|
||||
43
modules/backend/traits/SearchableWidget.php
Normal file
43
modules/backend/traits/SearchableWidget.php
Normal 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;
|
||||
}
|
||||
}
|
||||
69
modules/backend/traits/SelectableWidget.php
Normal file
69
modules/backend/traits/SelectableWidget.php
Normal 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];
|
||||
}
|
||||
}
|
||||
91
modules/backend/traits/SessionMaker.php
Normal file
91
modules/backend/traits/SessionMaker.php
Normal 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);
|
||||
}
|
||||
}
|
||||
249
modules/backend/traits/UploadableWidget.php
Normal file
249
modules/backend/traits/UploadableWidget.php
Normal 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);
|
||||
}
|
||||
}
|
||||
75
modules/backend/traits/WidgetMaker.php
Normal file
75
modules/backend/traits/WidgetMaker.php
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user