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,289 @@
<?php namespace Backend\Classes;
use Config;
use System\Classes\PluginManager;
use Winter\Storm\Auth\Manager as StormAuthManager;
use Winter\Storm\Exception\SystemException;
/**
* Back-end authentication manager.
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
class AuthManager extends StormAuthManager
{
protected static $instance;
protected $sessionKey = 'admin_auth';
protected $userModel = 'Backend\Models\User';
protected $groupModel = 'Backend\Models\UserGroup';
protected $throttleModel = 'Backend\Models\UserThrottle';
protected $requireActivation = false;
//
// Permission management
//
protected static $permissionDefaults = [
'code' => null,
'label' => null,
'comment' => null,
'roles' => null,
'order' => 500
];
/**
* @var array Cache of registration callbacks.
*/
protected $callbacks = [];
/**
* @var array List of registered permissions.
*/
protected $permissions = [];
/**
* @var array List of owner aliases. ['Aliased.Owner' => 'Real.Owner']
*/
protected $aliases = [];
/**
* @var array List of registered permission roles.
*/
protected $permissionRoles = false;
/**
* @var array Cache of registered permissions.
*/
protected $permissionCache = false;
protected function init()
{
$this->useThrottle = Config::get('auth.throttle.enabled', true);
parent::init();
}
/**
* Registers a callback function that defines authentication permissions.
* The callback function should register permissions by calling the manager's
* registerPermissions() function. The manager instance is passed to the
* callback function as an argument. Usage:
*
* BackendAuth::registerCallback(function ($manager) {
* $manager->registerPermissions([...]);
* });
*
* @param callable $callback A callable function.
*/
public function registerCallback(callable $callback)
{
$this->callbacks[] = $callback;
}
/**
* Registers the back-end permission items.
* The argument is an array of the permissions. The array keys represent the
* permission codes, specific for the plugin/module. Each element in the
* array should be an associative array with the following keys:
* - label - specifies the menu label localization string key, required.
* - order - a position of the item in the menu, optional.
* - comment - a brief comment that describes the permission, optional.
* - tab - assign this permission to a tabbed group, optional.
* @param string $owner Specifies the permissions' owner plugin or module in the format Author.Plugin
* @param array $definitions An array of the menu item definitions.
*/
public function registerPermissions($owner, array $definitions)
{
// Resolve alias
$owner = $this->aliases[$owner] ?? $owner;
foreach ($definitions as $code => $definition) {
$permission = (object) array_merge(self::$permissionDefaults, array_merge($definition, [
'code' => $code,
'owner' => $owner
]));
$this->permissions[] = $permission;
}
// Clear the permission cache
$this->permissionCache = false;
}
/**
* Register a permission owner alias
*
* @param string $owner The owner to register an alias for. Example: Real.Owner
* @param string $alias The alias to register. Example: Aliased.Owner
* @return void
*/
public function registerPermissionOwnerAlias(string $owner, string $alias)
{
$this->aliases[$alias] = $owner;
}
/**
* Removes a single back-end permission
* @param string $owner Specifies the permissions' owner plugin or module in the format Author.Plugin
* @param string $code The code of the permission to remove
* @return void
*/
public function removePermission($owner, $code)
{
if (!$this->permissions) {
throw new SystemException('Unable to remove permissions before they are loaded.');
}
// Resolve alias
$owner = $this->aliases[$owner] ?? $owner;
$ownerPermissions = array_filter($this->permissions, function ($permission) use ($owner) {
return $permission->owner === $owner;
});
foreach ($ownerPermissions as $key => $permission) {
if ($permission->code === $code) {
unset($this->permissions[$key]);
}
}
// Clear the permission cache
$this->permissionCache = false;
}
/**
* Returns a list of the registered permissions items.
* @return array
*/
public function listPermissions()
{
if ($this->permissionCache !== false) {
return $this->permissionCache;
}
/*
* Load module items
*/
foreach ($this->callbacks as $callback) {
$callback($this);
}
/*
* Load plugin items
*/
$plugins = PluginManager::instance()->getPlugins();
foreach ($plugins as $id => $plugin) {
$items = $plugin->registerPermissions();
if (!is_array($items)) {
continue;
}
$this->registerPermissions($id, $items);
}
/*
* Sort permission items
*/
usort($this->permissions, function ($a, $b) {
if ($a->order == $b->order) {
return 0;
}
return $a->order > $b->order ? 1 : -1;
});
return $this->permissionCache = $this->permissions;
}
/**
* Returns an array of registered permissions, grouped by tabs.
* @return array
*/
public function listTabbedPermissions()
{
$tabs = [];
foreach ($this->listPermissions() as $permission) {
$tab = $permission->tab ?? 'backend::lang.form.undefined_tab';
if (!array_key_exists($tab, $tabs)) {
$tabs[$tab] = [];
}
$tabs[$tab][] = $permission;
}
return $tabs;
}
/**
* {@inheritdoc}
*/
protected function createUserModelQuery()
{
return parent::createUserModelQuery()->withTrashed();
}
/**
* {@inheritdoc}
*/
protected function validateUserModel($user)
{
if ( ! $user instanceof $this->userModel) {
return false;
}
// Perform the deleted_at check manually since the relevant migrations
// might not have been run yet during the update to build 444.
// @see https://github.com/octobercms/october/issues/3999
if (array_key_exists('deleted_at', $user->getAttributes()) && $user->deleted_at !== null) {
return false;
}
return $user;
}
/**
* Returns an array of registered permissions belonging to a given role code
* @param string $role
* @param bool $includeOrphans Include any permissons that do not have a default role specified
* @return array
*/
public function listPermissionsForRole($role, $includeOrphans = true)
{
if ($this->permissionRoles === false) {
$this->permissionRoles = [];
foreach ($this->listPermissions() as $permission) {
if ($permission->roles) {
foreach ((array) $permission->roles as $_role) {
$this->permissionRoles[$_role][$permission->code] = 1;
}
}
else {
$this->permissionRoles['*'][$permission->code] = 1;
}
}
}
$result = $this->permissionRoles[$role] ?? [];
if ($includeOrphans) {
$result += $this->permissionRoles['*'] ?? [];
}
return $result;
}
public function hasPermissionsForRole($role)
{
return !!$this->listPermissionsForRole($role, false);
}
}

View File

@@ -0,0 +1,347 @@
<?php
namespace Backend\Classes;
use Backend\Facades\BackendAuth;
use Closure;
use Illuminate\Routing\Controller as ControllerBase;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\View;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use System\Classes\PluginManager;
use Winter\Storm\Router\Helper as RouterHelper;
use Winter\Storm\Support\Facades\Config;
use Winter\Storm\Support\Facades\Event;
use Winter\Storm\Support\Facades\File;
use Winter\Storm\Support\Str;
/**
* This is the master controller for all back-end pages.
* All requests that are prefixed with the backend URI pattern are sent here,
* then the next URI segments are analysed and the request is routed to the
* relevant back-end controller.
*
* For example, a request with the URL `/backend/acme/blog/posts` will look
* for the `Posts` controller inside the `Acme.Blog` plugin.
*
* @see Backend\Classes\Controller Base class for back-end controllers
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
class BackendController extends ControllerBase
{
use \Winter\Storm\Extension\ExtendableTrait;
/**
* @var array Behaviors implemented by this controller.
*/
public $implement;
/**
* @var string Allows early access to page action.
*/
public static $action;
/**
* @var array Allows early access to page parameters.
*/
public static $params;
/**
* @var boolean Flag to indicate that the CMS module is handling the current request
*/
protected $cmsHandling = false;
/**
* Stores the requested controller so that the constructor is only run once
*
* @var Backend\Classes\Controller
*/
protected $requestedController;
/**
* Instantiate a new BackendController instance.
*/
public function __construct()
{
$this->middleware(function ($request, $next) {
// Process the request before retrieving controller middleware, to allow for the session and auth data
// to be made available to the controller's constructor.
$response = $next($request);
// Find requested controller to determine if any middleware has been attached
$pathParts = explode('/', str_replace(Request::root() . '/', '', Request::url()));
if (count($pathParts)) {
// Drop off preceding backend URL part if needed
if (!empty(Config::get('cms.backendUri', 'backend'))) {
array_shift($pathParts);
}
$path = implode('/', $pathParts);
$requestedController = $this->getRequestedController($path);
if (
!is_null($requestedController)
&& is_array($requestedController)
&& count($requestedController['controller']->getMiddleware())
) {
$action = $requestedController['action'];
// Collect applicable middleware and insert middleware into pipeline
$controllerMiddleware = collect($requestedController['controller']->getMiddleware())
->reject(function ($data) use ($action) {
return static::methodExcludedByOptions($action, $data['options']);
})
->pluck('middleware');
foreach ($controllerMiddleware as $middleware) {
$middleware->call($requestedController['controller'], $request, $response);
}
}
}
return $response;
});
$this->extendableConstruct();
}
/**
* @inheritDoc
*/
public function callAction($method, $parameters)
{
return parent::callAction($method, array_values($parameters));
}
/**
* Pass unhandled URLs to the CMS Controller, if it exists
*
* @param string $url
* @return Response
*/
protected function passToCmsController($url)
{
if (
in_array('Cms', Config::get('cms.loadModules', [])) &&
class_exists('\Cms\Classes\Controller')
) {
$this->cmsHandling = true;
$response = App::make('Cms\Classes\Controller')->run($url);
if ($response->getStatusCode() !== 404 || !BackendAuth::check()) {
return $response;
}
}
return Response::make(View::make('backend::404'), 404);
}
/**
* Finds and serves the requested backend controller.
* If the controller cannot be found, returns the Cms page with the URL /404.
* If the /404 page doesn't exist, returns the system 404 page.
* @param string $url Specifies the requested page URL.
* If the parameter is omitted, the current URL used.
* @return string Returns the processed page content.
*/
public function run($url = null)
{
// Handle NotFoundHttpExceptions in the backend (usually triggered by abort(404))
Event::listen('exception.beforeRender', function ($exception, $httpCode, $request) {
if ($this->cmsHandling) {
return;
}
if ($exception instanceof NotFoundHttpException) {
return View::make('backend::404');
} elseif (
$exception instanceof HttpException
&& $exception->getStatusCode() === 403
) {
return View::make('backend::access_denied');
}
}, 1);
/*
* Database check
*/
if (!App::hasDatabase()) {
return Config::get('app.debug', false)
? Response::make(View::make('backend::no_database'), 200)
: $this->passToCmsController($url);
}
$controllerRequest = $this->getRequestedController($url);
if (!is_null($controllerRequest)) {
return $controllerRequest['controller']->run(
$controllerRequest['action'],
$controllerRequest['params']
);
}
/*
* Fall back on Cms controller
*/
return $this->passToCmsController($url);
}
/**
* Determines the controller and action to load in the backend via a provided URL.
*
* If a suitable controller is found, this will return an array with the controller class name as a string, the
* action to call as a string and an array of parameters. If a suitable controller and action cannot be found,
* this method will return null.
*
* @param string $url A URL to determine the requested controller and action for
* @return array|null A suitable controller, action and parameters in an array if found, otherwise null.
*/
protected function getRequestedController($url)
{
$params = RouterHelper::segmentizeUrl($url);
/*
* Look for a Module controller
*/
$module = $params[0] ?? 'backend';
$controller = $params[1] ?? 'index';
self::$action = $action = isset($params[2]) ? $this->parseAction($params[2]) : 'index';
self::$params = $controllerParams = array_slice($params, 3);
$controllerClass = '\\'.$module.'\Controllers\\'.$controller;
if ($controllerObj = $this->findController(
$controllerClass,
$action,
base_path().'/modules'
)) {
return [
'controller' => $controllerObj,
'action' => $action,
'params' => $controllerParams
];
}
/*
* Look for a Plugin controller
*/
if (count($params) >= 2) {
list($author, $plugin) = $params;
$pluginCode = ucfirst($author) . '.' . ucfirst($plugin);
if (PluginManager::instance()->isDisabled($pluginCode)) {
return Response::make(View::make('backend::404'), 404);
}
$controller = $params[2] ?? 'index';
self::$action = $action = isset($params[3]) ? $this->parseAction($params[3]) : 'index';
self::$params = $controllerParams = array_slice($params, 4);
$controllerClass = '\\'.$author.'\\'.$plugin.'\Controllers\\'.$controller;
if ($controllerObj = $this->findController(
$controllerClass,
$action,
plugins_path()
)) {
return [
'controller' => $controllerObj,
'action' => $action,
'params' => $controllerParams
];
}
}
return null;
}
/**
* This method is used internally.
* Finds a backend controller with a callable action method.
* @param string $controller Specifies a method name to execute.
* @param string $action Specifies a method name to execute.
* @param string $inPath Base path for class file location.
* @return ControllerBase Returns the backend controller object
*/
protected function findController($controller, $action, $inPath)
{
if (isset($this->requestedController)) {
return $this->requestedController;
}
/*
* Workaround: Composer does not support case insensitivity.
*/
if (!class_exists($controller)) {
$controller = Str::normalizeClassName($controller);
$controllerFile = $inPath.strtolower(str_replace('\\', '/', $controller)) . '.php';
if ($controllerFile = File::existsInsensitive($controllerFile)) {
include_once $controllerFile;
}
}
if (!class_exists($controller)) {
return $this->requestedController = null;
}
$controllerObj = App::make($controller);
if ($controllerObj->actionExists($action)) {
return $this->requestedController = $controllerObj;
}
return $this->requestedController = null;
}
/**
* Process the action name, since dashes are not supported in PHP methods.
* @param string $actionName
* @return string
*/
protected function parseAction($actionName)
{
if (strpos($actionName, '-') !== false) {
return snake_case(camel_case($actionName));
}
return $actionName;
}
/**
* Determine if the given options exclude a particular method.
*
* @param string $method
* @param array $options
* @return bool
*/
protected static function methodExcludedByOptions($method, array $options)
{
return (isset($options['only']) && !in_array($method, (array) $options['only'])) ||
(!empty($options['except']) && in_array($method, (array) $options['except']));
}
public function __call($name, $params)
{
if ($name === 'extend') {
if (empty($params[0]) || !is_callable($params[0])) {
throw new \InvalidArgumentException('The extend() method requires a callback parameter or closure.');
}
if ($params[0] instanceof Closure) {
return $params[0]->call($this, $params[1] ?? $this);
}
return Closure::fromCallable($params[0])->call($this, $params[1] ?? $this);
}
return $this->extendableCall($name, $params);
}
public static function __callStatic($name, $params)
{
if ($name === 'extend') {
if (empty($params[0])) {
throw new \InvalidArgumentException('The extend() method requires a callback parameter or closure.');
}
self::extendableExtendCallback($params[0], $params[1] ?? false, $params[2] ?? null);
return;
}
return self::extendableCallStatic($name, $params);
}
}

View File

@@ -0,0 +1,823 @@
<?php
namespace Backend\Classes;
use Backend\Facades\Backend;
use Backend\Facades\BackendAuth;
use Backend\Facades\BackendMenu;
use Backend\Models\Preference as BackendPreference;
use Backend\Models\UserPreference;
use Backend\Widgets\MediaManager;
use Exception;
use Illuminate\Database\Eloquent\MassAssignmentException;
use Illuminate\Http\RedirectResponse;
use Illuminate\Routing\Controller as ControllerBase;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\View;
use Winter\Storm\Exception\AjaxException;
use Winter\Storm\Exception\ApplicationException;
use Winter\Storm\Exception\SystemException;
use Winter\Storm\Exception\ValidationException;
use Winter\Storm\Support\Facades\Config;
use Winter\Storm\Support\Facades\Flash;
/**
* The Backend base controller class, used by Backend controllers.
* The base controller services back end pages.
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
class Controller extends ControllerBase
{
use \System\Traits\ViewMaker;
use \System\Traits\AssetMaker;
use \System\Traits\ConfigMaker;
use \System\Traits\EventEmitter;
use \System\Traits\ResponseMaker;
use \System\Traits\SecurityController;
use \Backend\Traits\ErrorMaker;
use \Backend\Traits\WidgetMaker;
use \Winter\Storm\Extension\ExtendableTrait;
/**
* @var array Behaviors implemented by this controller.
*/
public $implement;
/**
* @var object Reference the logged in admin user.
*/
protected $user;
/**
* @var object Collection of WidgetBase objects used on this page.
*/
public $widget;
/**
* @var bool Prevents the automatic view display.
*/
public $suppressView = false;
/**
* @var array Routed parameters.
*/
protected $params;
/**
* @var string Page action being called.
*/
protected $action;
/**
* @var array Defines a collection of actions available without authentication.
*/
protected $publicActions = [];
/**
* @var array Permissions required to view this page.
*/
protected $requiredPermissions = [];
/**
* @var string Page title
*/
public $pageTitle;
/**
* @var string Page title template
*/
public $pageTitleTemplate;
/**
* @var string Body class property used for customising the layout on a controller basis.
*/
public $bodyClass = '';
/**
* @var array Default methods which cannot be called as actions.
*/
public $hiddenActions = [
'run',
'actionExists',
'pageAction',
'getId',
'setStatusCode',
'handleError',
'makeHintPartial'
];
/**
* @var array Controller specified methods which cannot be called as actions.
*/
protected $guarded = [];
/**
* Constructor.
*/
public function __construct()
{
/*
* Allow early access to route data.
*/
$this->action = BackendController::$action;
$this->params = BackendController::$params;
/*
* Apply $guarded methods to hidden actions
*/
$this->hiddenActions = array_merge($this->hiddenActions, $this->guarded);
/*
* Define layout and view paths
*/
$this->layout = $this->layout ?: 'default';
$this->layoutPath = Skin::getActive()->getLayoutPaths();
$this->viewPath = $this->configPath = $this->guessViewPath();
/*
* Add layout paths from the plugin / module context
*/
$relativePath = dirname(dirname(strtolower(str_replace('\\', '/', get_called_class()))));
$this->layoutPath[] = '~/modules/' . $relativePath . '/layouts';
$this->layoutPath[] = '~/plugins/' . $relativePath . '/layouts';
/*
* Create a new instance of the admin user
*/
$this->user = BackendAuth::getUser();
/*
* Media Manager widget is available on all back-end pages
*/
if ($this->user && $this->user->hasAccess('media.*')) {
$manager = new MediaManager($this, 'ocmediamanager');
$manager->bindToController();
}
$this->extendableConstruct();
}
public function __get($name)
{
return $this->extendableGet($name);
}
public function __set($name, $value)
{
$this->extendableSet($name, $value);
}
public function __call($name, $params)
{
if ($name === 'extend') {
if (empty($params[0]) || !is_callable($params[0])) {
throw new \InvalidArgumentException('The extend() method requires a callback parameter or closure.');
}
if ($params[0] instanceof \Closure) {
return $params[0]->call($this, $params[1] ?? $this);
}
return \Closure::fromCallable($params[0])->call($this, $params[1] ?? $this);
}
return $this->extendableCall($name, $params);
}
public static function __callStatic($name, $params)
{
if ($name === 'extend') {
if (empty($params[0])) {
throw new \InvalidArgumentException('The extend() method requires a callback parameter or closure.');
}
self::extendableExtendCallback($params[0], $params[1] ?? false, $params[2] ?? null);
return;
}
return self::extendableCallStatic($name, $params);
}
/**
* Set the navigation context based on the current action & parameters
*/
protected function setNavigationContext(?string $action = null, array $params = []): void
{
$context = BackendMenu::getContext();
// @TODO: Support detecting module controllers as well
$currentClass = explode('\\', get_class($this));
$author = $currentClass[0];
$plugin = $currentClass[1];
$controller = $currentClass[count($currentClass) - 1];
$owner = $context->owner ?? "$author.$plugin";
$mainMenuCode = $context->mainMenuCode ?? strtolower($plugin);
$sideMenuCode = $context->sideMenuCode ?? strtolower($controller);
BackendMenu::setContext($owner, $mainMenuCode, $sideMenuCode);
}
/**
* Execute the controller action.
* @param string $action The action name.
* @param array $params Routing parameters to pass to the action.
* @return mixed The action result.
*/
public function run($action = null, $params = [])
{
$this->action = $action;
$this->params = $params;
/*
* Short circuit requests without a valid CSRF token
* @see \System\Traits\SecurityController
*/
if (!in_array(Request::method(), ['HEAD', 'GET', 'OPTIONS']) && !$this->verifyCsrfToken()) {
return Response::make(Lang::get('system::lang.page.invalid_token.label'), 403);
}
/*
* Check forced HTTPS protocol.
* @see \System\Traits\SecurityController
*/
if (!$this->verifyForceSecure()) {
return Redirect::secure(Request::path());
}
/*
* Determine if this request is a public action.
*/
$isPublicAction = in_array($action, $this->publicActions);
/*
* Check that user is logged in and has permission to view this page
*/
if (!$isPublicAction) {
/*
* Not logged in, redirect to login screen or show ajax error.
*/
if (!BackendAuth::check()) {
return Request::ajax()
? Response::make(Lang::get('backend::lang.page.access_denied.label'), 403)
: Backend::redirectGuest('backend/auth');
}
/*
* Check access groups against the page definition
*/
if ($this->requiredPermissions && !$this->user->hasAnyAccess($this->requiredPermissions)) {
abort(403);
}
}
/**
* @event backend.page.beforeDisplay
* Provides an opportunity to override backend page content
*
* Example usage:
*
* Event::listen('backend.page.beforeDisplay', function ((\Backend\Classes\Controller) $backendController, (string) $action, (array) $params) {
* trace_log('redirect all backend pages to google');
* return \Redirect::to('https://google.com');
* });
*
* Or
*
* $backendController->bindEvent('page.beforeDisplay', function ((string) $action, (array) $params) {
* trace_log('redirect all backend pages to google');
* return \Redirect::to('https://google.com');
* });
*
*/
if ($event = $this->fireSystemEvent('backend.page.beforeDisplay', [$action, $params])) {
return $event;
}
/*
* Set the admin preference locale
*/
BackendPreference::setAppLocale();
BackendPreference::setAppFallbackLocale();
/*
* Set the navigation context
*/
$this->setNavigationContext($action, $params);
/*
* Execute AJAX event
*/
if ($ajaxResponse = $this->execAjaxHandlers()) {
$result = $ajaxResponse;
}
/*
* Execute postback handler
*/
elseif (
($handler = post('_handler')) &&
$this->verifyCsrfToken()
) {
$this->validateHandlerName($handler);
if (
($handlerResponse = $this->runAjaxHandler($handler)) &&
$handlerResponse !== true
) {
$result = $handlerResponse;
}
}
/*
* Execute page action
*/
else {
$result = $this->execPageAction($action, $params);
}
/*
* Prepare and return response
* @see \System\Traits\ResponseMaker
*/
return $this->makeResponse($result);
}
/**
* This method is used internally.
* Determines whether an action with the specified name exists.
* Action must be a class public method. Action name can not be prefixed with the underscore character.
* @param string $name Specifies the action name.
* @param bool $internal Allow protected actions.
* @return boolean
*/
public function actionExists($name, $internal = false)
{
if (!strlen($name) || substr($name, 0, 1) == '_' || !$this->methodExists($name)) {
return false;
}
foreach ($this->hiddenActions as $method) {
if (strtolower($name) == strtolower($method)) {
return false;
}
}
$ownMethod = method_exists($this, $name);
if ($ownMethod) {
$methodInfo = new \ReflectionMethod($this, $name);
/*
* Only allow lowercase actions. Compare the resolved method name rather than the
* requested one - PHP method names are case-insensitive, so a lowercased URL
* segment would otherwise pass this check and still resolve to the mixed-case
* method (eg. "index_onemptylog" reaching index_onEmptyLog()).
*/
if (strtolower($methodInfo->getName()) !== $methodInfo->getName()) {
return false;
}
$public = $methodInfo->isPublic();
if ($public) {
return true;
}
}
/*
* Extension methods are resolved through a case-sensitive lookup, so the requested
* name is already the canonical one.
*/
elseif (strtolower($name) !== $name) {
return false;
}
if ($internal && (($ownMethod && $methodInfo->isProtected()) || !$ownMethod)) {
return true;
}
if (!$ownMethod) {
return true;
}
return false;
}
/**
* Returns a URL for this controller and supplied action.
*/
public function actionUrl($action = null, $path = null)
{
if ($action === null) {
$action = $this->action;
}
$class = get_called_class();
$uriPath = dirname(dirname(strtolower(str_replace('\\', '/', $class))));
$controllerName = strtolower(class_basename($class));
$url = $uriPath.'/'.$controllerName.'/'.$action;
if ($path) {
$url .= '/'.$path;
}
return Backend::url($url);
}
/**
* Invokes the current controller action without rendering a view,
* used by AJAX handler that may rely on the logic inside the action.
*/
public function pageAction()
{
if (!$this->action) {
return;
}
$this->suppressView = true;
$this->execPageAction($this->action, $this->params);
}
/**
* This method is used internally.
* Invokes the controller action and loads the corresponding view.
* @param string $actionName Specifies a action name to execute.
* @param array $parameters A list of the action parameters.
*/
protected function execPageAction($actionName, $parameters)
{
$result = null;
if (!$this->actionExists($actionName)) {
if (Config::get('app.debug', false)) {
throw new SystemException(sprintf(
"Action %s is not found in the controller %s",
$actionName,
get_class($this)
));
} else {
Response::make(View::make('backend::404'), 404);
}
}
// Execute the action
$result = call_user_func_array([$this, $actionName], $parameters);
// Expecting \Response and \RedirectResponse
if ($result instanceof \Symfony\Component\HttpFoundation\Response) {
return $result;
}
// No page title
if (!$this->pageTitle) {
$this->pageTitle = 'backend::lang.page.untitled';
}
// Load the view
if (!$this->suppressView && $result === null) {
return $this->makeView($actionName);
}
return $this->makeViewContent((string) $result);
}
/**
* Returns the AJAX handler for the current request, if available.
* @return string
*/
public function getAjaxHandler()
{
if (!Request::ajax() || Request::method() != 'POST') {
return null;
}
if ($handler = Request::header('X_WINTER_REQUEST_HANDLER')) {
return trim($handler);
}
return null;
}
/**
* Validates the AJAX handler name follows the expected format.
*
* @throws \Winter\Storm\Exception\SystemException if the handler name is invalid
*/
protected function validateHandlerName(string $handler): void
{
if (!preg_match('/^(?:\w+\:{2})?on[A-Z]{1}[\w+]*$/', $handler)) {
throw new SystemException(Lang::get('backend::lang.ajax_handler.invalid_name', ['name' => $handler]));
}
}
/**
* This method is used internally.
* Invokes a controller event handler and loads the supplied partials.
*/
protected function execAjaxHandlers()
{
if ($handler = $this->getAjaxHandler()) {
try {
/*
* Validate the handler name
*/
$this->validateHandlerName($handler);
/*
* Validate the handler partial list
*/
if ($partialList = trim(Request::header('X_WINTER_REQUEST_PARTIALS'))) {
$partialList = explode('&', $partialList);
foreach ($partialList as $partial) {
if (!preg_match('/^(?!.*\/\/)[a-z0-9\_][a-z0-9\_\-\/]*$/i', $partial)) {
throw new SystemException(Lang::get('backend::lang.partial.invalid_name', ['name'=>$partial]));
}
}
}
else {
$partialList = [];
}
$responseContents = [];
/*
* Execute the handler
*/
if (!$result = $this->runAjaxHandler($handler)) {
throw new SystemException(Lang::get('backend::lang.ajax_handler.not_found', ['name'=>$handler]));
}
/*
* Render partials and return the response as array that will be converted to JSON automatically.
*/
foreach ($partialList as $partial) {
$responseContents[$partial] = $this->makePartial($partial);
}
/*
* If the handler returned a redirect, process the URL and dispose of it so
* framework.js knows to redirect the browser and not the request!
*/
if ($result instanceof RedirectResponse) {
$responseContents['X_WINTER_REDIRECT'] = $result->getTargetUrl();
$result = null;
}
/*
* No redirect is used, look for any flash messages
*/
elseif (Flash::check()) {
$responseContents['#layout-flash-messages'] = $this->makeLayoutPartial('flash_messages');
}
/*
* Detect assets
*/
if ($this->hasAssetsDefined()) {
$responseContents['X_WINTER_ASSETS'] = $this->getAssetPaths();
}
/*
* If the handler returned an array, we should add it to output for rendering.
* If it is a string, add it to the array with the key "result".
* If an object, pass it to Laravel as a response object.
*/
if (is_array($result)) {
$responseContents = array_merge($responseContents, $result);
}
elseif (is_string($result)) {
$responseContents['result'] = $result;
}
elseif (is_object($result)) {
return $result;
}
return Response::make()->setContent($responseContents);
}
catch (ValidationException $ex) {
/*
* Handle validation error gracefully
*/
Flash::error($ex->getMessage());
$responseContents = [];
$responseContents['#layout-flash-messages'] = $this->makeLayoutPartial('flash_messages');
$responseContents['X_WINTER_ERROR_FIELDS'] = $ex->getFields();
throw new AjaxException($responseContents);
}
catch (MassAssignmentException $ex) {
throw new ApplicationException(Lang::get('backend::lang.model.mass_assignment_failed', ['attribute' => $ex->getMessage()]));
}
catch (Exception $ex) {
throw $ex;
}
}
return null;
}
/**
* Tries to find and run an AJAX handler in the page action.
* The method stops as soon as the handler is found.
* @return boolean Returns true if the handler was found. Returns false otherwise.
*/
protected function runAjaxHandler($handler)
{
/**
* @event backend.ajax.beforeRunHandler
* Provides an opportunity to modify an AJAX request
*
* The parameter provided is `$handler` (the requested AJAX handler to be run)
*
* Example usage (forwards AJAX handlers to a backend widget):
*
* Event::listen('backend.ajax.beforeRunHandler', function ((\Backend\Classes\Controller) $controller, (string) $handler) {
* if (strpos($handler, '::')) {
* list($componentAlias, $handlerName) = explode('::', $handler);
* if ($componentAlias === $this->getBackendWidgetAlias()) {
* return $this->backendControllerProxy->runAjaxHandler($handler);
* }
* }
* });
*
* Or
*
* $this->controller->bindEvent('ajax.beforeRunHandler', function ((string) $handler) {
* if (strpos($handler, '::')) {
* list($componentAlias, $handlerName) = explode('::', $handler);
* if ($componentAlias === $this->getBackendWidgetAlias()) {
* return $this->backendControllerProxy->runAjaxHandler($handler);
* }
* }
* });
*
*/
if ($event = $this->fireSystemEvent('backend.ajax.beforeRunHandler', [$handler])) {
return $event;
}
/*
* Process Widget handler
*/
if (strpos($handler, '::')) {
list($widgetName, $handlerName) = explode('::', $handler);
/*
* Execute the page action so widgets are initialized
*/
$this->pageAction();
if ($this->fatalError) {
throw new SystemException($this->fatalError);
}
if (!isset($this->widget->{$widgetName})) {
throw new SystemException(Lang::get('backend::lang.widget.not_bound', ['name'=>$widgetName]));
}
if (($widget = $this->widget->{$widgetName}) && $widget->methodExists($handlerName)) {
$result = $this->runAjaxHandlerForWidget($widget, $handlerName);
return $result ?: true;
}
}
else {
/*
* Process page specific handler (index_onSomething)
*/
$pageHandler = $this->action . '_' . $handler;
if ($this->methodExists($pageHandler)) {
$result = call_user_func_array([$this, $pageHandler], array_values($this->params));
return $result ?: true;
}
/*
* Process page global handler (onSomething)
*/
if ($this->methodExists($handler)) {
$result = call_user_func_array([$this, $handler], array_values($this->params));
return $result ?: true;
}
/*
* Cycle each widget to locate a usable handler (widget::onSomething)
*/
$this->suppressView = true;
$this->execPageAction($this->action, $this->params);
foreach ((array) $this->widget as $widget) {
if ($widget->methodExists($handler)) {
$result = $this->runAjaxHandlerForWidget($widget, $handler);
return $result ?: true;
}
}
}
/*
* Generic handler that does nothing
*/
if ($handler == 'onAjax') {
return true;
}
return false;
}
/**
* Specific code for executing an AJAX handler for a widget.
* This will append the widget view paths to the controller and merge the vars.
* @return mixed
*/
protected function runAjaxHandlerForWidget($widget, $handler)
{
$this->prependViewPath($widget->getViewPaths());
$result = call_user_func_array([$widget, $handler], array_values($this->params));
$this->vars = $widget->vars + $this->vars;
return $result;
}
/**
* Returns the controllers public actions.
*/
public function getPublicActions()
{
return $this->publicActions;
}
/**
* Returns a unique ID for the controller and route. Useful in creating HTML markup.
*/
public function getId($suffix = null)
{
$id = class_basename(get_called_class()) . '-' . $this->action;
if ($suffix !== null) {
$id .= '-' . $suffix;
}
return $id;
}
//
// Hints
//
/**
* Renders a hint partial, used for displaying informative information that
* can be hidden by the user. If you don't want to render a partial, you can
* supply content via the 'content' key of $params.
* @param string $name Unique key name
* @param string $partial Reference to content (partial name)
* @param array $params Extra parameters
* @return string
*/
public function makeHintPartial($name, $partial = null, $params = [])
{
if (is_array($partial)) {
$params = $partial;
$partial = null;
}
if (!$partial) {
$partial = array_get($params, 'partial', $name);
}
return $this->makeLayoutPartial('hint', [
'hintName' => $name,
'hintPartial' => $partial,
'hintContent' => array_get($params, 'content'),
'hintParams' => $params
] + $params);
}
/**
* Ajax handler to hide a backend hint, once hidden the partial
* will no longer display for the user.
* @return void
*/
public function onHideBackendHint()
{
if (!$name = post('name')) {
throw new ApplicationException('Missing a hint name.');
}
$preferences = UserPreference::forUser();
$hiddenHints = $preferences->get('backend::hints.hidden', []);
$hiddenHints[$name] = 1;
$preferences->set('backend::hints.hidden', $hiddenHints);
}
/**
* Checks if a hint has been hidden by the user.
* @param string $name Unique key name
* @return boolean
*/
public function isBackendHintHidden($name)
{
$hiddenHints = UserPreference::forUser()->get('backend::hints.hidden', []);
return array_key_exists($name, $hiddenHints);
}
}

View File

@@ -0,0 +1,168 @@
<?php namespace Backend\Classes;
use Lang;
use ApplicationException;
use Winter\Storm\Extension\ExtensionBase;
use System\Traits\ViewMaker;
use Winter\Storm\Html\Helper as HtmlHelper;
/**
* Controller Behavior base class
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
class ControllerBehavior extends ExtensionBase
{
use \Backend\Traits\WidgetMaker;
use \Backend\Traits\SessionMaker;
use \System\Traits\AssetMaker;
use \System\Traits\ConfigMaker;
use \System\Traits\ViewMaker {
ViewMaker::makeFileContents as localMakeFileContents;
}
/**
* @var array Supplied configuration.
*/
protected $config;
/**
* @var \Backend\Classes\Controller Reference to the back end controller.
*/
protected $controller;
/**
* @var array Properties that must exist in the controller using this behavior.
*/
protected $requiredProperties = [];
/**
* @var array Visible actions in context of the controller. Only takes effect if it is an array
*/
protected $actions;
/**
* Constructor.
*/
public function __construct($controller)
{
$this->controller = $controller;
$this->viewPath = $this->configPath = $this->guessViewPath('/partials');
$this->assetPath = $this->guessViewPath('/assets', true);
/*
* Validate controller properties
*/
foreach ($this->requiredProperties as $property) {
if (!isset($controller->{$property})) {
throw new ApplicationException(Lang::get('system::lang.behavior.missing_property', [
'class' => get_class($controller),
'property' => $property,
'behavior' => get_called_class()
]));
}
}
// Hide all methods that aren't explicitly listed as actions
if (is_array($this->actions)) {
$this->hideAction(array_diff(get_class_methods(get_class($this)), $this->actions));
}
// Include this behavior's default views in the controller's view paths
$this->controller->appendViewPath($this->guessViewPath('/views'));
}
/**
* Sets the configuration values
* @param mixed $config Config object or array
* @param array $required Required config items
*/
public function setConfig($config, $required = [])
{
$this->config = $this->makeConfig($config, $required);
}
/**
* Safe accessor for configuration values.
* @param string $name Config name, supports array names like "field[key]"
* @param mixed $default Default value if nothing is found
* @return string
*/
public function getConfig($name = null, $default = null)
{
/*
* Return all config
*/
if ($name === null) {
return $this->config;
}
/*
* Array field name, eg: field[key][key2][key3]
*/
$keyParts = HtmlHelper::nameToArray($name);
/*
* First part will be the field name, pop it off
*/
$fieldName = array_shift($keyParts);
if (!isset($this->config->{$fieldName})) {
return $default;
}
$result = $this->config->{$fieldName};
/*
* Loop the remaining key parts and build a result
*/
foreach ($keyParts as $key) {
if (!is_array($result) || !array_key_exists($key, $result)) {
return $default;
}
$result = $result[$key];
}
return $result;
}
/**
* Protects a public method from being available as an controller action.
* These methods could be defined in a controller to override a behavior default action.
* Such methods should be defined as public, to allow the behavior object to access it.
* By default public methods of a controller are considered as actions.
* To prevent this occurrence, methods should be hidden by using this method.
* @param mixed $methodName Specifies a method name.
*/
protected function hideAction($methodName)
{
if (!is_array($methodName)) {
$methodName = [$methodName];
}
$this->controller->hiddenActions = array_merge($this->controller->hiddenActions, $methodName);
}
/**
* Makes all views in context of the controller, not the behavior.
* @param string $filePath Absolute path to the view file.
* @param array $extraParams Parameters that should be available to the view.
* @return string
*/
public function makeFileContents($filePath, $extraParams = [])
{
$this->controller->vars = array_merge($this->controller->vars, $this->vars);
return $this->controller->makeFileContents($filePath, $extraParams);
}
/**
* Returns true in case if a specified method exists in the extended controller.
* @param string $methodName Specifies the method name
* @return bool
*/
protected function controllerMethodExists($methodName)
{
return method_exists($this->controller, $methodName);
}
}

View File

@@ -0,0 +1,168 @@
<?php namespace Backend\Classes;
use Winter\Storm\Html\Helper as HtmlHelper;
/**
* Filter scope definition
* A translation of the filter scope configuration
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
class FilterScope
{
/**
* @var string Scope name.
*/
public $scopeName;
/**
* @var string A prefix to the field identifier so it can be totally unique.
*/
public $idPrefix;
/**
* @var string Column to display for the display name
*/
public $nameFrom = 'name';
/**
* @var string Column to display for the description (optional)
*/
public $descriptionFrom;
/**
* @var string Filter scope label.
*/
public $label;
/**
* @var mixed Filter scope value.
*/
public $value;
/**
* @var string Filter mode.
*/
public $type = 'group';
/**
* @var string Filter options.
*/
public $options;
/**
* @var array Other scope names this scope depends on, when the other scopes are modified, this scope will update.
*/
public $dependsOn;
/**
* @var string Specifies contextual visibility of this form scope.
*/
public $context;
/**
* @var bool Specify if the scope is disabled or not.
*/
public $disabled = false;
/**
* @var string Specifies a default value for supported scopes.
*/
public $defaults;
/**
* @var string Raw SQL conditions to use when applying this scope.
*/
public $conditions;
/**
* @var string Model scope method to use when applying this filter scope.
*/
public $scope;
/**
* @var string Specifies a CSS class to attach to the scope container.
*/
public $cssClass;
/**
* @var array Raw scope configuration.
*/
public $config;
public function __construct($scopeName, $label)
{
$this->scopeName = $scopeName;
$this->label = $label;
}
/**
* Specifies a scope control rendering mode. Supported modes are:
* - group - filter by a group of IDs. Default.
* - checkbox - filter by a simple toggle switch.
* @param string $type Specifies a render mode as described above
* @param array $config A list of render mode specific config.
*/
public function displayAs($type, $config = [])
{
$this->type = strtolower($type) ?: $this->type;
$this->config = $this->evalConfig($config);
return $this;
}
/**
* Process options and apply them to this object.
* @param array $config
* @return array
*/
protected function evalConfig($config)
{
if ($config === null) {
$config = [];
}
/*
* Standard config:property values
*/
$applyConfigValues = [
'options',
'dependsOn',
'context',
'default',
'conditions',
'scope',
'cssClass',
'nameFrom',
'descriptionFrom',
'disabled',
];
foreach ($applyConfigValues as $value) {
if (array_key_exists($value, $config)) {
$this->{$value} = $config[$value];
}
}
return $config;
}
/**
* Returns a value suitable for the scope id property.
*/
public function getId($suffix = null)
{
$id = 'scope';
$id .= '-'.$this->scopeName;
if ($suffix) {
$id .= '-'.$suffix;
}
if ($this->idPrefix) {
$id = $this->idPrefix . '-' . $id;
}
return HtmlHelper::nameToId($id);
}
}

View File

@@ -0,0 +1,764 @@
<?php
namespace Backend\Classes;
use BackedEnum;
use Illuminate\Support\Facades\Lang;
use Winter\Storm\Database\Model;
use Winter\Storm\Html\Helper as HtmlHelper;
use Winter\Storm\Support\Facades\Html;
use Winter\Storm\Support\Str;
/**
* Form Field definition
* A translation of the form field configuration
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
class FormField
{
/**
* @var int Value returned when the form field should not contribute any save data.
*/
const NO_SAVE_DATA = -1;
/**
* @var string A special character in yaml config files to indicate a field higher in hierarchy
*/
const HIERARCHY_UP = '^';
/**
* @var string Form field name.
*/
public $fieldName;
/**
* @var string If the field element names should be contained in an array. Eg:
*
* <input name="nameArray[fieldName]" />
*/
public $arrayName;
/**
* @var string A prefix to the field identifier so it can be totally unique.
*/
public $idPrefix;
/**
* @var string Form field label.
*/
public $label;
/**
* @var string Form field value.
*/
public $value;
/**
* @var string Model attribute to use for the display value.
*/
public $valueFrom;
/**
* @var string Specifies a default value for supported fields.
*/
public $defaults;
/**
* @var string Model attribute to use for the default value.
*/
public $defaultFrom;
/**
* @var string Specifies if this field belongs to a tab.
*/
public $tab;
/**
* @var string Display mode. Text, textarea
*/
public $type = 'text';
/**
* @var string Field options.
*/
public $options;
/**
* @var string Specifies a side. Possible values: auto, left, right, full.
*/
public $span = 'full';
/**
* @var string|int Specifies a size. Possible values for textarea: tiny, small, large, huge, giant.
*/
public $size;
/**
* @var string Specifies contextual visibility of this form field.
*/
public $context;
/**
* @var bool Specifies if this field is mandatory.
*/
public $required;
/**
* @var bool Specify if the field is read-only or not.
*/
public $readOnly = false;
/**
* @var bool Specify if the field is disabled or not.
*/
public $disabled = false;
/**
* @var bool Specify if the field is hidden. Hiddens fields are not included in postbacks.
*/
public $hidden = false;
/**
* @var bool Specifies if this field stretch to fit the page height.
*/
public $stretch = false;
/**
* @var string Specifies a comment to accompany the field
*/
public $comment = '';
/**
* @var string Specifies the comment position.
*/
public $commentPosition = 'below';
/**
* @var string Specifies if the comment is in HTML format.
*/
public $commentHtml = false;
/**
* @var string Specifies a message to display when there is no value supplied (placeholder).
*/
public $placeholder = '';
/**
* @var array Contains a list of attributes specified in the field configuration.
*/
public $attributes;
/**
* @var string Specifies a CSS class to attach to the field container.
*/
public $cssClass;
/**
* @var string Specifies a path for partial-type fields.
*/
public $path;
/**
* @var array Raw field configuration.
*/
public $config;
/**
* @var array Other field names this field depends on, when the other fields are modified, this field will update.
*/
public $dependsOn;
/**
* @var array Other field names this field can be triggered by, see the Trigger API documentation.
*/
public $trigger;
/**
* @var array Other field names text is converted in to a URL, slug or file name value in this field.
*/
public $preset;
/**
* Constructor.
* @param string $fieldName The name of the field
* @param string $label The label of the field
*/
public function __construct($fieldName, $label)
{
$this->fieldName = $fieldName;
$this->label = $label;
}
/**
* If this field belongs to a tab.
*/
public function tab($value)
{
$this->tab = $value;
return $this;
}
/**
* Sets a side of the field on a form.
* @param string $value Specifies a side. Possible values: left, right, full
*/
public function span($value = 'full')
{
$this->span = $value;
return $this;
}
/**
* Sets the size of the field on a form.
* @param string $value Specifies a size. Possible values: tiny, small, large, huge, giant
*/
public function size($value = 'large')
{
$this->size = $value;
return $this;
}
/**
* Sets field options, for dropdowns, radio lists and checkbox lists.
* @param array $value
* @return self
*/
public function options($value = null)
{
if ($value === null) {
if (is_array($this->options)) {
return $this->options;
} elseif (is_callable($this->options)) {
$callable = $this->options;
return $callable();
} elseif (is_string($this->options) && is_array($options = Lang::get($this->options))) {
return $options;
}
return [];
}
$this->options = $value;
return $this;
}
/**
* Specifies a field control rendering mode. Supported modes are:
* - text - creates a text field. Default for varchar column types.
* - textarea - creates a textarea control. Default for text column types.
* - dropdown - creates a drop-down list. Default for reference-based columns.
* - radio - creates a set of radio buttons.
* - checkbox - creates a single checkbox.
* - checkboxlist - creates a checkbox list.
* - switch - creates a switch field.
* @param string $type Specifies a render mode as described above
* @param array $config A list of render mode specific config.
*/
public function displayAs($type, $config = [])
{
if (in_array($type, ['textarea', 'widget'])) {
// defaults to 'large'
$this->size = 'large';
}
$this->type = strtolower($type) ?: $this->type;
$this->config = $this->evalConfig($config);
return $this;
}
/**
* Process options and apply them to this object.
* @param array $config
* @return array
*/
protected function evalConfig($config)
{
if ($config === null) {
$config = [];
}
/*
* Standard config:property values
*/
$applyConfigValues = [
'commentHtml',
'context',
'cssClass',
'dependsOn',
'disabled',
'hidden',
'path',
'placeholder',
'preset',
'readOnly',
'required',
'stretch',
'trigger',
];
foreach ($applyConfigValues as $value) {
if (array_key_exists($value, $config)) {
$this->{$value} = $config[$value];
}
}
/*
* Custom applicators
*/
if (isset($config['options'])) {
$this->options($config['options']);
}
if (isset($config['span'])) {
$this->span($config['span']);
}
if (isset($config['size'])) {
$this->size($config['size']);
}
if (isset($config['tab'])) {
$this->tab($config['tab']);
}
if (isset($config['commentAbove'])) {
$this->comment($config['commentAbove'], 'above');
}
if (isset($config['comment'])) {
$this->comment($config['comment']);
}
if (isset($config['default'])) {
$this->defaults = $config['default'];
}
if (isset($config['defaultFrom'])) {
$this->defaultFrom = $config['defaultFrom'];
}
if (isset($config['attributes'])) {
$this->attributes($config['attributes']);
}
if (isset($config['containerAttributes'])) {
$this->attributes($config['containerAttributes'], 'container');
}
if (isset($config['valueFrom'])) {
$this->valueFrom = $config['valueFrom'];
}
else {
$this->valueFrom = $this->fieldName;
}
return $config;
}
/**
* Adds a text comment above or below the field.
* @param string $text Specifies a comment text.
* @param string $position Specifies a comment position.
* @param bool $isHtml Set to true if you use HTML formatting in the comment
* Supported values are 'below' and 'above'
*/
public function comment($text, $position = 'below', $isHtml = null)
{
$this->comment = $text;
$this->commentPosition = $position;
if ($isHtml !== null) {
$this->commentHtml = $isHtml;
}
return $this;
}
/**
* Determine if the provided value matches this field's value.
* @param string $value
* @return bool
*/
public function isSelected($value = true)
{
if ($this->value === null) {
return false;
}
$value = ($value instanceof BackedEnum) ? $value->value : $value;
$currentValue = ($this->value instanceof BackedEnum) ? $this->value->value : $this->value;
return (string) $value === (string) $currentValue;
}
/**
* Sets the attributes for this field in a given position.
* - field: Attributes are added to the form field element (input, select, textarea, etc)
* - container: Attributes are added to the form field container (div.form-group)
* @param array $items
* @param string $position
* @return void
*/
public function attributes($items, $position = 'field')
{
if (!is_array($items)) {
return;
}
$multiArray = array_filter($items, 'is_array');
if (!$multiArray) {
$this->attributes[$position] = $items;
return;
}
foreach ($items as $_position => $_items) {
$this->attributes($_items, $_position);
}
return $this;
}
/**
* Checks if the field has the supplied [unfiltered] attribute.
* @param string $name
* @param string $position
* @return bool
*/
public function hasAttribute($name, $position = 'field')
{
if (!isset($this->attributes[$position])) {
return false;
}
return array_key_exists($name, $this->attributes[$position]);
}
/**
* Returns the attributes for this field at a given position.
* @param string $position
* @return array
*/
public function getAttributes($position = 'field', $htmlBuild = true)
{
$result = array_get($this->attributes, $position, []);
$result = $this->filterAttributes($result, $position);
// Field is required, so add the "required" attribute
if ($position === 'field' && $this->required && (!isset($result['required']) || $result['required'])) {
$result['required'] = '';
} elseif ($position === 'field' && isset($result['required']) && !$result['required']) {
// The "required" attribute is set and falsy, so unset it
unset($result['required']);
}
return $htmlBuild ? Html::attributes($result) : $result;
}
/**
* Adds any circumstantial attributes to the field based on other
* settings, such as the 'disabled' option.
* @param array $attributes
* @param string $position
* @return array
*/
protected function filterAttributes($attributes, $position = 'field')
{
$position = strtolower($position);
$attributes = $this->filterTriggerAttributes($attributes, $position);
$attributes = $this->filterPresetAttributes($attributes, $position);
if ($position == 'field' && $this->disabled) {
$attributes = $attributes + ['disabled' => 'disabled'];
}
if ($position == 'field' && $this->readOnly) {
$attributes = $attributes + ['readonly' => 'readonly'];
if ($this->type == 'checkbox' || $this->type == 'switch') {
$attributes = $attributes + ['onclick' => 'return false;'];
}
}
return $attributes;
}
/**
* Adds attributes used specifically by the Trigger API
* @param array $attributes
* @param string $position
* @return array
*/
protected function filterTriggerAttributes($attributes, $position = 'field')
{
if (!$this->trigger || !is_array($this->trigger)) {
return $attributes;
}
$triggerAction = array_get($this->trigger, 'action');
$triggerField = array_get($this->trigger, 'field');
$triggerCondition = array_get($this->trigger, 'condition');
$triggerForm = $this->arrayName;
$triggerMulti = '';
// Apply these to container
if (in_array($triggerAction, ['hide', 'show']) && $position != 'container') {
return $attributes;
}
// Apply these to field/input
if (in_array($triggerAction, ['enable', 'disable', 'empty']) && $position != 'field') {
return $attributes;
}
// Reduce the field reference for the trigger condition field
$triggerFieldParentLevel = Str::getPrecedingSymbols($triggerField, self::HIERARCHY_UP);
if ($triggerFieldParentLevel > 0) {
// Remove the preceding symbols from the trigger field name
$triggerField = substr($triggerField, $triggerFieldParentLevel);
$triggerForm = HtmlHelper::reduceNameHierarchy($triggerForm, $triggerFieldParentLevel);
}
// Preserve multi field types
if (Str::endsWith($triggerField, '[]')) {
$triggerField = substr($triggerField, 0, -2);
$triggerMulti = '[]';
}
// Final compilation
if ($this->arrayName) {
$fullTriggerField = $triggerForm.'['.implode('][', HtmlHelper::nameToArray($triggerField)).']'.$triggerMulti;
}
else {
$fullTriggerField = $triggerField.$triggerMulti;
}
$newAttributes = [
'data-trigger' => '[name="'.$fullTriggerField.'"]',
'data-trigger-action' => $triggerAction,
'data-trigger-condition' => $triggerCondition,
'data-trigger-closest-parent' => 'form, div[data-control="formwidget"]'
];
return $attributes + $newAttributes;
}
/**
* Adds attributes used specifically by the Input Preset API
* @param array $attributes
* @param string $position
* @return array
*/
protected function filterPresetAttributes($attributes, $position = 'field')
{
if (!$this->preset || $position != 'field') {
return $attributes;
}
if (!is_array($this->preset)) {
$this->preset = ['field' => $this->preset, 'type' => 'slug'];
}
$presetField = array_get($this->preset, 'field');
$presetType = array_get($this->preset, 'type');
if ($this->arrayName) {
$fullPresetField = $this->arrayName.'['.implode('][', HtmlHelper::nameToArray($presetField)).']';
}
else {
$fullPresetField = $presetField;
}
$newAttributes = [
'data-input-preset' => '[name="'.$fullPresetField.'"]',
'data-input-preset-type' => $presetType,
'data-input-preset-closest-parent' => 'form'
];
if ($prefixInput = array_get($this->preset, 'prefixInput')) {
$newAttributes['data-input-preset-prefix-input'] = $prefixInput;
}
return $attributes + $newAttributes;
}
/**
* Returns a value suitable for the field name property.
* @param string $arrayName Specify a custom array name
* @return string
*/
public function getName($arrayName = null)
{
if ($arrayName === null) {
$arrayName = $this->arrayName;
}
if ($arrayName) {
return $arrayName.'['.implode('][', HtmlHelper::nameToArray($this->fieldName)).']';
}
return $this->fieldName;
}
/**
* Returns a value suitable for the field id property.
* @param string $suffix Specify a suffix string
* @return string
*/
public function getId($suffix = null)
{
$id = 'field';
if ($this->arrayName) {
$id .= '-'.$this->arrayName;
}
$id .= '-'.$this->fieldName;
if ($suffix) {
$id .= '-'.$suffix;
}
if ($this->idPrefix) {
$id = $this->idPrefix . '-' . $id;
}
return HtmlHelper::nameToId($id);
}
/**
* Returns a raw config item value.
* @param string $value
* @param string $default
* @return mixed
*/
public function getConfig($value, $default = null)
{
return array_get($this->config, $value, $default);
}
/**
* Returns this fields value from a supplied data set, which can be
* an array or a model or another generic collection.
* @param mixed $data
* @param mixed $default
* @return mixed
*/
public function getValueFromData($data, $default = null)
{
$fieldName = $this->valueFrom ?: $this->fieldName;
return $this->getFieldNameFromData($fieldName, $data, $default);
}
/**
* Returns the default value for this field, the supplied data is used
* to source data when defaultFrom is specified.
* @param mixed $data
* @return mixed
*/
public function getDefaultFromData($data)
{
if ($this->defaultFrom) {
return $this->getFieldNameFromData($this->defaultFrom, $data);
}
if ($this->defaults !== '') {
return $this->defaults;
}
return null;
}
/**
* Returns the final model and attribute name of a nested attribute. Eg:
*
* list($model, $attribute) = $this->resolveAttribute('person[phone]');
*
* @param string $attribute.
* @return array
*/
public function resolveModelAttribute($model, $attribute = null)
{
if ($attribute === null) {
$attribute = $this->valueFrom ?: $this->fieldName;
}
$parts = is_array($attribute) ? $attribute : HtmlHelper::nameToArray($attribute);
$last = array_pop($parts);
foreach ($parts as $part) {
$model = $model->{$part};
}
return [$model, $last];
}
/**
* Internal method to extract the value of a field name from a data set.
* @param string $fieldName
* @param mixed $data
* @param mixed $default
* @return mixed
*/
protected function getFieldNameFromData($fieldName, $data, $default = null)
{
/*
* Array field name, eg: field[key][key2][key3]
*/
$keyParts = HtmlHelper::nameToArray($fieldName);
$lastField = end($keyParts);
$result = $data;
/*
* Loop the field key parts and build a value.
* To support relations only the last field should return the
* relation value, all others will look up the relation object as normal.
*/
foreach ($keyParts as $key) {
if ($result instanceof Model && $result->hasRelation($key)) {
if ($key == $lastField) {
$result = $result->getRelationValue($key) ?: $default;
} else {
$result = $result->{$key};
}
} elseif (is_array($result)) {
if (!array_key_exists($key, $result)) {
return $default;
}
$result = $result[$key];
} else {
if (!isset($result->{$key})) {
return $default;
}
$result = $result->{$key};
}
}
if ($result instanceof BackedEnum) {
$result = $result->value;
}
return $result;
}
/**
* Implements the getter functionality.
* @param string $name
*/
public function __get($name)
{
if (is_array($this->config) && array_key_exists($name, $this->config)) {
return array_get($this->config, $name);
}
if (property_exists($this, $name)) {
return $this->{$name};
}
return null;
}
/**
* Determine if an attribute exists on the object.
* @param string $name
*/
public function __isset($name)
{
if (is_array($this->config) && array_key_exists($name, $this->config)) {
return true;
}
return property_exists($this, $name) && !is_null($this->{$name});
}
}

View File

@@ -0,0 +1,279 @@
<?php namespace Backend\Classes;
use IteratorAggregate;
use ArrayIterator;
use ArrayAccess;
use Traversable;
/**
* Form Tabs definition
* A translation of the form field tab configuration
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
class FormTabs implements IteratorAggregate, ArrayAccess
{
const SECTION_OUTSIDE = 'outside';
const SECTION_PRIMARY = 'primary';
const SECTION_SECONDARY = 'secondary';
/**
* @var string Specifies the form section these tabs belong to.
*/
public $section = 'outside';
/**
* @var array Collection of panes fields to these tabs.
*/
public $fields = [];
/**
* @var array Names of tabs to lazy load.
*/
public $lazy = [];
/**
* @var string Default tab label to use when none is specified.
*/
public $defaultTab = 'backend::lang.form.undefined_tab';
/**
* @var array List of icons for their corresponding tabs.
*/
public $icons = [];
/**
* @var bool Should these tabs stretch to the bottom of the page layout.
*/
public $stretch;
/**
* @var boolean If set to TRUE, fields will not be displayed in tabs.
*/
public $suppressTabs = false;
/**
* @var string Specifies a CSS class to attach to the tab container.
*/
public $cssClass;
/**
* @var array Specifies a CSS class to an individual tab pane.
*/
public $paneCssClass;
/**
* @var bool Each tab gets url fragment to be linkable.
*/
public $linkable = true;
/**
* Constructor.
* Specifies a tabs rendering section. Supported sections are:
* - outside - stores a section of "tabless" fields.
* - primary - tabs section for primary fields.
* - secondary - tabs section for secondary fields.
* @param string $section Specifies a section as described above.
* @param array $config A list of render mode specific config.
*/
public function __construct($section, $config = [])
{
$this->section = strtolower($section) ?: $this->section;
$this->evalConfig($config);
if ($this->section == self::SECTION_OUTSIDE) {
$this->suppressTabs = true;
}
}
/**
* Process options and apply them to this object.
*/
protected function evalConfig(array $config): void
{
if (array_key_exists('defaultTab', $config)) {
$this->defaultTab = $config['defaultTab'];
}
if (array_key_exists('icons', $config)) {
$this->icons = $config['icons'];
}
if (array_key_exists('stretch', $config)) {
$this->stretch = $config['stretch'];
}
if (array_key_exists('suppressTabs', $config)) {
$this->suppressTabs = $config['suppressTabs'];
}
if (array_key_exists('cssClass', $config)) {
$this->cssClass = $config['cssClass'];
}
if (array_key_exists('paneCssClass', $config)) {
$this->paneCssClass = $config['paneCssClass'];
}
if (array_key_exists('linkable', $config)) {
$this->linkable = (bool) $config['linkable'];
}
if (array_key_exists('lazy', $config)) {
$this->lazy = $config['lazy'];
}
}
/**
* Add a field to the collection of tabs.
* @param string $name
* @param FormField $field
* @param string $tab
*/
public function addField($name, FormField $field, $tab = null)
{
if (!$tab) {
$tab = $this->defaultTab;
}
$this->fields[$tab][$name] = $field;
}
/**
* Remove a field from all tabs by name.
* @param string $name
* @return boolean
*/
public function removeField($name)
{
foreach ($this->fields as $tab => $fields) {
foreach ($fields as $fieldName => $field) {
if ($fieldName == $name) {
unset($this->fields[$tab][$fieldName]);
/*
* Remove empty tabs from collection
*/
if (!count($this->fields[$tab])) {
unset($this->fields[$tab]);
}
return true;
}
}
}
return false;
}
/**
* Returns true if any fields have been registered for these tabs
* @return boolean
*/
public function hasFields()
{
return count($this->fields) > 0;
}
/**
* Returns an array of the registered fields, including tabs.
* @return array
*/
public function getFields()
{
return $this->fields;
}
/**
* Returns an array of the registered fields, without tabs.
* @return array
*/
public function getAllFields()
{
$tablessFields = [];
foreach ($this->getFields() as $tab) {
$tablessFields += $tab;
}
return $tablessFields;
}
/**
* Returns an icon for the tab based on the tab's name.
* @param string $name
* @return string
*/
public function getIcon($name)
{
if (!empty($this->icons[$name])) {
return $this->icons[$name];
}
}
/**
* Returns a tab pane CSS class.
* @param string $index
* @param string $label
* @return string
*/
public function getPaneCssClass($index = null, $label = null)
{
if (is_string($this->paneCssClass)) {
return $this->paneCssClass;
}
if ($index !== null && isset($this->paneCssClass[$index])) {
return $this->paneCssClass[$index];
}
if ($label !== null && isset($this->paneCssClass[$label])) {
return $this->paneCssClass[$label];
}
}
/**
* Get an iterator for the items.
*/
public function getIterator(): Traversable
{
return new ArrayIterator(
$this->suppressTabs
? $this->getAllFields()
: $this->getFields()
);
}
/**
* ArrayAccess implementation
*/
public function offsetSet($offset, $value): void
{
$this->fields[$offset] = $value;
}
/**
* ArrayAccess implementation
*/
public function offsetExists($offset): bool
{
return isset($this->fields[$offset]);
}
/**
* ArrayAccess implementation
*/
public function offsetUnset($offset): void
{
unset($this->fields[$offset]);
}
/**
* ArrayAccess implementation
*/
public function offsetGet($offset): mixed
{
return $this->fields[$offset] ?? null;
}
}

View File

@@ -0,0 +1,152 @@
<?php namespace Backend\Classes;
use Winter\Storm\Html\Helper as HtmlHelper;
/**
* Form Widget base class
* Widgets used specifically for forms
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
abstract class FormWidgetBase extends WidgetBase
{
//
// Configurable properties
//
/**
* @var \Winter\Storm\Database\Model Form model object.
*/
public $model;
/**
* @var array Dataset containing field values, if none supplied model should be used.
*/
public $data;
/**
* @var string Active session key, used for editing forms and deferred bindings.
*/
public $sessionKey;
/**
* @var bool Render this form with uneditable preview data.
*/
public $previewMode = false;
/**
* @var bool Determines if this form field should display comments and labels.
*/
public $showLabels = true;
//
// Object properties
//
/**
* @var FormField Object containing general form field information.
*/
protected $formField;
/**
* @var Backend\Widgets\Form The parent form that contains this field
*/
protected $parentForm = null;
/**
* @var string Form field name.
*/
protected $fieldName;
/**
* @var string Model attribute to get/set value from.
*/
protected $valueFrom;
/**
* Constructor
* @param $controller Controller Active controller object.
* @param $formField FormField Object containing general form field information.
* @param $configuration array Configuration the relates to this widget.
*/
public function __construct($controller, $formField, $configuration = [])
{
$this->formField = $formField;
$this->fieldName = $formField->fieldName;
$this->valueFrom = $formField->valueFrom;
$this->config = $this->makeConfig($configuration);
$this->fillFromConfig([
'model',
'data',
'sessionKey',
'previewMode',
'showLabels',
'parentForm',
]);
parent::__construct($controller, $configuration);
}
/**
* Retrieve the parent form for this formwidget
*
* @return Backend\Widgets\Form|null
*/
public function getParentForm()
{
return $this->parentForm;
}
/**
* Returns the HTML element field name for this widget, used for capturing
* user input, passed back to the getSaveValue method when saving.
* @return string HTML element name
*/
public function getFieldName()
{
return $this->formField->getName();
}
/**
* Returns a unique ID for this widget. Useful in creating HTML markup.
*/
public function getId($suffix = null)
{
$id = parent::getId($suffix);
$id .= '-' . $this->fieldName;
return HtmlHelper::nameToId($id);
}
/**
* Process the postback value for this widget. If the value is omitted from
* postback data, it will be NULL, otherwise it will be an empty string.
* @param mixed $value The existing value for this widget.
* @return string The new value for this widget.
*/
public function getSaveValue($value)
{
return $value;
}
/**
* Returns the value for this form field,
* supports nesting via HTML array.
* @return string
*/
public function getLoadValue()
{
if ($this->formField->value !== null) {
return $this->formField->value;
}
$defaultValue = !$this->model->exists
? $this->formField->getDefaultFromData($this->data ?: $this->model)
: null;
return $this->formField->getValueFromData($this->data ?: $this->model, $defaultValue);
}
}

View File

@@ -0,0 +1,291 @@
<?php namespace Backend\Classes;
use Winter\Storm\Database\Model;
use Winter\Storm\Html\Helper as HtmlHelper;
/**
* List Columns definition
* A translation of the list column configuration
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
class ListColumn
{
/**
* @var string List column name.
*/
public $columnName;
/**
* @var string List column label.
*/
public $label;
/**
* @var string Display mode. Text, number
*/
public $type = 'text';
/**
* @var bool Specifies if this column can be searched.
*/
public $searchable = false;
/**
* @var bool Specifies if this column is hidden by default.
*/
public $invisible = false;
/**
* @var bool Specifies if this column can be sorted.
*/
public $sortable = true;
/**
* @var bool Specifies if this column can be summed.
*/
public $summable = false;
/**
* @var bool If set to false, disables the default click behavior when the column is clicked.
*/
public $clickable = true;
/**
* @var string Model attribute to use for the display value, this will
* override any `$sqlSelect` definition.
*/
public $valueFrom;
/**
* @var string Specifies a default value when value is empty.
*/
public $defaults;
/**
* @var string Custom SQL for selecting this record display value,
* the `@` symbol is replaced with the table name.
*/
public $sqlSelect;
/**
* @var string Relation name, if this column represents a model relationship.
*/
public $relation;
/**
* @var string sets the column width, can be specified in percents (10%) or pixels (50px).
* There could be a single column without width specified, it will be stretched to take the
* available space.
*/
public $width;
/**
* @var string Specify a CSS class to attach to the list cell element.
*/
public $cssClass;
/**
* @var string Specify a CSS class to attach to the list header cell element.
*/
public $headCssClass;
/**
* @var string Specify a format or style for the column value, such as a Date.
*/
public $format;
/**
* @var string Specifies a path for partial-type fields.
*/
public $path;
/**
* @var string Specifies the alignment of this column.
*/
public $align;
/**
* @var array Raw field configuration.
*/
public $config;
/**
* Constructor.
* @param string $columnName
* @param string $label
*/
public function __construct($columnName, $label)
{
$this->columnName = $columnName;
$this->label = $label;
}
/**
* Specifies a list column rendering mode. Supported modes are:
* - text - text column, aligned left
* - number - numeric column, aligned right
* @param string $type Specifies a render mode as described above
*/
public function displayAs($type, $config)
{
$this->type = strtolower($type) ?: $this->type;
$this->config = $this->evalConfig($config);
return $this;
}
/**
* Process options and apply them to this object.
* @param array $config
* @return array
*/
protected function evalConfig($config)
{
if (isset($config['width'])) {
$this->width = $config['width'];
}
if (isset($config['cssClass'])) {
$this->cssClass = $config['cssClass'];
}
if (isset($config['headCssClass'])) {
$this->headCssClass = $config['headCssClass'];
}
if (isset($config['searchable'])) {
$this->searchable = $config['searchable'];
}
if (isset($config['sortable'])) {
$this->sortable = $config['sortable'];
}
if (isset($config['summable'])) {
$this->summable = $config['summable'];
}
if (isset($config['clickable'])) {
$this->clickable = $config['clickable'];
}
if (isset($config['invisible'])) {
$this->invisible = $config['invisible'];
}
if (isset($config['valueFrom'])) {
$this->valueFrom = $config['valueFrom'];
}
if (isset($config['default'])) {
$this->defaults = $config['default'];
}
if (isset($config['select'])) {
$this->sqlSelect = $config['select'];
}
if (isset($config['relation'])) {
$this->relation = $config['relation'];
}
if (isset($config['format'])) {
$this->format = $config['format'];
}
if (isset($config['path'])) {
$this->path = $config['path'];
}
if (isset($config['align']) && \in_array($config['align'], ['left', 'right', 'center'])) {
$this->align = $config['align'];
}
return $config;
}
/**
* Returns a HTML valid name for the column name.
* @return string
*/
public function getName()
{
return HtmlHelper::nameToId($this->columnName);
}
/**
* Returns a value suitable for the column id property.
* @param string $suffix Specify a suffix string
* @return string
*/
public function getId($suffix = null)
{
$id = 'column';
$id .= '-'.$this->columnName;
if ($suffix) {
$id .= '-'.$suffix;
}
return HtmlHelper::nameToId($id);
}
/**
* Returns the column specific aligment css class.
* @return string
*/
public function getAlignClass()
{
return $this->align ? 'list-cell-align-' . $this->align : '';
}
/**
* Returns a raw config item value.
* @param string $value
* @param string $default
* @return mixed
*/
public function getConfig($value, $default = null)
{
return array_get($this->config, $value, $default);
}
/**
* Returns this columns value from a supplied data set, which can be
* an array or a model or another generic collection.
* @param mixed $data
* @param mixed $default
* @return mixed
*/
public function getValueFromData($data, $default = null)
{
$columnName = $this->valueFrom ?: $this->columnName;
return $this->getColumnNameFromData($columnName, $data, $default);
}
/**
* Internal method to extract the value of a column name from a data set.
* @param string $columnName
* @param mixed $data
* @param mixed $default
* @return mixed
*/
protected function getColumnNameFromData($columnName, $data, $default = null)
{
/*
* Array column name, eg: column[key][key2][key3]
*/
$keyParts = HtmlHelper::nameToArray($columnName);
$result = $data;
/*
* Loop the column key parts and build a value.
* To support relations only the last column should return the
* relation value, all others will look up the relation object as normal.
*/
foreach ($keyParts as $key) {
if ($result instanceof Model && $result->hasRelation($key)) {
$result = $result->{$key};
}
else {
if (is_array($result) && array_key_exists($key, $result)) {
$result = $result[$key];
} elseif (!isset($result->{$key})) {
return $default;
} else {
$result = $result->{$key};
}
}
}
return $result;
}
}

View File

@@ -0,0 +1,131 @@
<?php namespace Backend\Classes;
use Winter\Storm\Exception\SystemException;
/**
* Class MainMenuItem
*
* @package Backend\Classes
*/
class MainMenuItem
{
/**
* @var string
*/
public $code;
/**
* @var string
*/
public $owner;
/**
* @var string
*/
public $label;
/**
* @var null|string
*/
public $icon;
/**
* @var null|string
*/
public $iconSvg;
/**
* @var mixed
*/
public $counter;
/**
* @var null|string
*/
public $counterLabel;
/**
* @var null|string
*/
public $badge;
/**
* @var string
*/
public $url;
/**
* @var array
*/
public $permissions = [];
/**
* @var int
*/
public $order = 500;
/**
* @var SideMenuItem[]
*/
public $sideMenu = [];
/**
* @param string $permission
* @param array $definition
*/
public function addPermission(string $permission, array $definition)
{
$this->permissions[$permission] = $definition;
}
/**
* @param SideMenuItem $sideMenu
*/
public function addSideMenuItem(SideMenuItem $sideMenu)
{
$this->sideMenu[$sideMenu->code] = $sideMenu;
}
/**
* @param string $code
* @return SideMenuItem
* @throws SystemException
*/
public function getSideMenuItem(string $code)
{
if (!array_key_exists($code, $this->sideMenu)) {
throw new SystemException('No sidenavigation item available with code ' . $code);
}
return $this->sideMenu[$code];
}
/**
* @param string $code
*/
public function removeSideMenuItem(string $code)
{
unset($this->sideMenu[$code]);
}
/**
* @param array $data
* @return static
*/
public static function createFromArray(array $data)
{
$instance = new static();
$instance->code = $data['code'];
$instance->owner = $data['owner'];
$instance->label = $data['label'];
$instance->url = $data['url'];
$instance->icon = $data['icon'] ?? null;
$instance->iconSvg = $data['iconSvg'] ?? null;
$instance->counter = $data['counter'] ?? null;
$instance->counterLabel = $data['counterLabel'] ?? null;
$instance->badge = $data['badge'] ?? null;
$instance->permissions = $data['permissions'] ?? $instance->permissions;
$instance->order = (!empty($data['order']) || @$data['order'] === 0) ? (int) $data['order'] : $instance->order;
return $instance;
}
}

View File

@@ -0,0 +1,796 @@
<?php namespace Backend\Classes;
use Event;
use BackendAuth;
use System\Classes\PluginManager;
use Validator;
use SystemException;
use Log;
use Config;
/**
* Manages the backend navigation.
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
class NavigationManager
{
use \Winter\Storm\Support\Traits\Singleton;
use \System\Traits\LazyOwnerAlias;
/**
* @var array Cache of registration callbacks.
*/
protected $callbacks = [];
/**
* @var array List of owner aliases. ['Aliased.Owner' => 'Real.Owner']
*/
protected $aliases = [];
/**
* @var MainMenuItem[] List of registered items.
*/
protected $items;
/**
* @var QuickActionItem[] List of registered quick actions.
*/
protected $quickActions;
protected $contextSidenavPartials = [];
protected $contextOwner;
protected $contextMainMenuItemCode;
protected $contextSideMenuItemCode;
/**
* @var PluginManager
*/
protected $pluginManager;
/**
* Initialize this singleton.
*/
protected function init()
{
foreach (static::$lazyAliases as $alias => $owner) {
$this->registerOwnerAlias($owner, $alias);
}
$this->pluginManager = PluginManager::instance();
}
/**
* Loads the menu items from modules and plugins
* @return void
* @throws SystemException
*/
protected function loadItems()
{
$this->items = [];
$this->quickActions = [];
/*
* Load module items
*/
foreach ($this->callbacks as $callback) {
$callback($this);
}
/*
* Load plugin items
*/
$plugins = $this->pluginManager->getPlugins();
foreach ($plugins as $id => $plugin) {
$items = $plugin->registerNavigation();
$quickActions = $plugin->registerQuickActions();
if (!is_array($items) && !is_array($quickActions)) {
continue;
}
if (is_array($items)) {
$this->registerMenuItems($id, $items);
}
if (is_array($quickActions)) {
$this->registerQuickActions($id, $quickActions);
}
}
/**
* @event backend.menu.extendItems
* Provides an opportunity to manipulate the backend navigation
*
* Example usage:
*
* Event::listen('backend.menu.extendItems', function ((\Backend\Classes\NavigationManager) $navigationManager) {
* $navigationManager->addMainMenuItems(...)
* $navigationManager->addSideMenuItems(...)
* $navigationManager->removeMainMenuItem(...)
* });
*
*/
Event::fire('backend.menu.extendItems', [$this]);
/*
* Sort menu items and quick actions
*/
$this->applyDefaultOrders($this->items);
uasort($this->items, static function ($a, $b) {
return $a->order - $b->order;
});
$this->applyDefaultOrders($this->quickActions);
uasort($this->quickActions, static function ($a, $b) {
return $a->order - $b->order;
});
/*
* Filter items and quick actions that the user lacks permission for
*/
$user = BackendAuth::getUser();
$this->items = $this->filterItemPermissions($user, $this->items);
$this->quickActions = $this->filterItemPermissions($user, $this->quickActions);
foreach ($this->items as $item) {
if (!$item->sideMenu || !count($item->sideMenu)) {
continue;
}
$this->applyDefaultOrders($item->sideMenu);
/*
* Sort side menu items
*/
uasort($item->sideMenu, static function ($a, $b) {
return $a->order - $b->order;
});
/*
* Filter items user lacks permission for
*/
$item->sideMenu = $this->filterItemPermissions($user, $item->sideMenu);
}
}
/**
* Apply incremental default orders to items with the explicit auto-order value (-1)
* or that have invalid order values (non-integer).
*
* @param array $items Array of MainMenuItem, SideMenuItem, or QuickActionItem objects
* @return void
*/
protected function applyDefaultOrders(array $items)
{
$orderCount = 0;
foreach ($items as $item) {
if ($item->order !== -1 && is_integer($item->order)) {
continue;
}
$item->order = ($orderCount += 100);
}
}
/**
* Registers a callback function that defines menu items.
* The callback function should register menu items by calling the manager's
* `registerMenuItems` method. The manager instance is passed to the callback
* function as an argument. Usage:
*
* BackendMenu::registerCallback(function ($manager) {
* $manager->registerMenuItems([...]);
* });
*
* @param callable $callback A callable function.
*/
public function registerCallback(callable $callback)
{
$this->callbacks[] = $callback;
}
/**
* Registers the back-end menu items.
* The argument is an array of the main menu items. The array keys represent the
* menu item codes, specific for the plugin/module. Each element in the
* array should be an associative array with the following keys:
* - label - specifies the menu label localization string key, required.
* - icon - an icon name from the Font Awesome icon collection, required.
* - url - the back-end relative URL the menu item should point to, required.
* - permissions - an array of permissions the back-end user should have, optional.
* The item will be displayed if the user has any of the specified permissions.
* - order - a position of the item in the menu, optional.
* - counter - an optional numeric value to output near the menu icon. The value should be
* a number or a callable returning a number.
* - counterLabel - an optional string value to describe the numeric reference in counter.
* - sideMenu - an array of side menu items, optional. If provided, the array items
* should represent the side menu item code, and each value should be an associative
* array with the following keys:
* - label - specifies the menu label localization string key, required.
* - icon - an icon name from the Font Awesome icon collection, required.
* - url - the back-end relative URL the menu item should point to, required.
* - attributes - an array of attributes and values to apply to the menu item, optional.
* - permissions - an array of permissions the back-end user should have, optional.
* - counter - an optional numeric value to output near the menu icon. The value should be
* a number or a callable returning a number.
* - counterLabel - an optional string value to describe the numeric reference in counter.
* - badge - an optional string value to output near the menu icon. The value should be
* a string. This value will override the counter if set.
* @param string $owner Specifies the menu items owner plugin or module in the format Author.Plugin.
* @param array $definitions An array of the menu item definitions.
* @throws SystemException
*/
public function registerMenuItems($owner, array $definitions)
{
$validator = Validator::make($definitions, [
'*.label' => 'required',
'*.icon' => 'required_without:*.iconSvg',
'*.url' => 'required',
'*.sideMenu.*.label' => 'nullable|required',
'*.sideMenu.*.icon' => 'nullable|required_without:*.sideMenu.*.iconSvg',
'*.sideMenu.*.url' => 'nullable|required',
]);
if ($validator->fails()) {
$errorMessage = 'Invalid menu item detected in ' . $owner . '. Contact the plugin author to fix (' . $validator->errors()->first() . ')';
if (Config::get('app.debug', false)) {
throw new SystemException($errorMessage);
}
Log::error($errorMessage);
}
$this->addMainMenuItems($owner, $definitions);
}
/**
* Register an owner alias
*
* @param string $owner The owner to register an alias for. Example: Real.Owner
* @param string $alias The alias to register. Example: Aliased.Owner
* @return void
*/
public function registerOwnerAlias(string $owner, string $alias)
{
$this->aliases[strtoupper($alias)] = strtoupper($owner);
}
/**
* Dynamically add an array of main menu items
* @param string $owner
* @param array $definitions
*/
public function addMainMenuItems($owner, array $definitions)
{
foreach ($definitions as $code => $definition) {
$this->addMainMenuItem($owner, $code, $definition);
}
}
/**
* Dynamically add a single main menu item
* @param string $owner
* @param string $code
* @param array $definition
*/
public function addMainMenuItem($owner, $code, array $definition)
{
$itemKey = $this->makeItemKey($owner, $code);
if (isset($this->items[$itemKey])) {
$definition = array_merge((array) $this->items[$itemKey], $definition);
}
$item = array_merge($definition, [
'code' => $code,
'owner' => $owner
]);
$this->items[$itemKey] = MainMenuItem::createFromArray($item);
if (array_key_exists('sideMenu', $item)) {
$this->addSideMenuItems($owner, $code, $item['sideMenu']);
}
}
/**
* @param string $owner
* @param string $code
* @return MainMenuItem
* @throws SystemException
*/
public function getMainMenuItem(string $owner, string $code)
{
$itemKey = $this->makeItemKey($owner, $code);
if (!array_key_exists($itemKey, $this->items)) {
throw new SystemException('No main menu item found with key ' . $itemKey);
}
return $this->items[$itemKey];
}
/**
* Removes a single main menu item
* @param $owner
* @param $code
*/
public function removeMainMenuItem($owner, $code)
{
$itemKey = $this->makeItemKey($owner, $code);
unset($this->items[$itemKey]);
}
/**
* Dynamically add an array of side menu items
* @param string $owner
* @param string $code
* @param array $definitions
*/
public function addSideMenuItems($owner, $code, array $definitions)
{
foreach ($definitions as $sideCode => $definition) {
$this->addSideMenuItem($owner, $code, $sideCode, (array) $definition);
}
}
/**
* Dynamically add a single side menu item
* @param string $owner
* @param string $code
* @param string $sideCode
* @param array $definition
* @return bool
*/
public function addSideMenuItem($owner, $code, $sideCode, array $definition)
{
$itemKey = $this->makeItemKey($owner, $code);
if (!isset($this->items[$itemKey])) {
return false;
}
$mainItem = $this->items[$itemKey];
$definition = array_merge($definition, [
'code' => $sideCode,
'owner' => $owner
]);
if (isset($mainItem->sideMenu[$sideCode])) {
$definition = array_merge((array) $mainItem->sideMenu[$sideCode], $definition);
}
$item = SideMenuItem::createFromArray($definition);
$this->items[$itemKey]->addSideMenuItem($item);
return true;
}
/**
* Remove multiple side menu items
*
* @param string $owner
* @param string $code
* @param array $sideCodes
* @return void
*/
public function removeSideMenuItems($owner, $code, $sideCodes)
{
foreach ($sideCodes as $sideCode) {
$this->removeSideMenuItem($owner, $code, $sideCode);
}
}
/**
* Removes a single main menu item
* @param string $owner
* @param string $code
* @param string $sideCode
* @return bool
*/
public function removeSideMenuItem($owner, $code, $sideCode)
{
$itemKey = $this->makeItemKey($owner, $code);
if (!isset($this->items[$itemKey])) {
return false;
}
$mainItem = $this->items[$itemKey];
$mainItem->removeSideMenuItem($sideCode);
return true;
}
/**
* Returns a list of the main menu items.
* @return array
* @throws SystemException
*/
public function listMainMenuItems()
{
if ($this->items === null && $this->quickActions === null) {
$this->loadItems();
}
if ($this->items === null) {
return [];
}
foreach ($this->items as $item) {
if ($item->badge) {
$item->counter = (string) $item->badge;
continue;
}
if ($item->counter === false) {
continue;
}
if ($item->counter !== null && is_callable($item->counter)) {
$item->counter = call_user_func($item->counter, $item);
} elseif (!empty((int) $item->counter)) {
$item->counter = (int) $item->counter;
} elseif (!empty($sideItems = $this->listSideMenuItems($item->owner, $item->code))) {
$item->counter = 0;
foreach ($sideItems as $sideItem) {
if ($sideItem->badge) {
continue;
}
$item->counter += $sideItem->counter;
}
}
if (empty($item->counter) || !is_numeric($item->counter)) {
$item->counter = null;
}
}
return $this->items;
}
/**
* Returns a list of side menu items for the currently active main menu item.
* The currently active main menu item is set with the setContext methods.
* @param null $owner
* @param null $code
* @return SideMenuItem[]
* @throws SystemException
*/
public function listSideMenuItems($owner = null, $code = null)
{
$activeItem = null;
if ($owner !== null && $code !== null) {
$activeItem = @$this->items[$this->makeItemKey($owner, $code)];
} else {
foreach ($this->listMainMenuItems() as $item) {
if ($this->isMainMenuItemActive($item)) {
$activeItem = $item;
break;
}
}
}
if (!$activeItem) {
return [];
}
$items = $activeItem->sideMenu;
foreach ($items as $item) {
if ($item->badge) {
$item->counter = (string) $item->badge;
continue;
}
if ($item->counter !== null && is_callable($item->counter)) {
$item->counter = call_user_func($item->counter, $item);
if (empty($item->counter)) {
$item->counter = null;
}
}
if (!is_null($item->counter) && !is_numeric($item->counter)) {
throw new SystemException("The menu item {$activeItem->code}.{$item->code}'s counter property is invalid. Check to make sure it's numeric or callable. Value: " . var_export($item->counter, true));
}
}
return $items;
}
/**
* Registers quick actions in the main navigation.
*
* Quick actions are single purpose links displayed to the left of the user menu in the
* backend main navigation.
*
* The argument is an array of the quick action items. The array keys represent the
* quick action item codes, specific for the plugin/module. Each element in the
* array should be an associative array with the following keys:
* - label - specifies the action label localization string key, used as a tooltip, required.
* - icon - an icon name from the Font Awesome icon collection, required if iconSvg is unspecified.
* - iconSvg - a custom SVG icon to use for the icon, required if icon is unspecified.
* - url - the back-end relative URL the quick action item should point to, required.
* - permissions - an array of permissions the back-end user should have, optional.
* The item will be displayed if the user has any of the specified permissions.
* - order - a position of the item in the menu, optional.
*
* @param string $owner Specifies the quick action items owner plugin or module in the format Author.Plugin.
* @param array $definitions An array of the quick action item definitions.
* @return void
* @throws SystemException If the validation of the quick action configuration fails
*/
public function registerQuickActions($owner, array $definitions)
{
$validator = Validator::make($definitions, [
'*.label' => 'required',
'*.icon' => 'required_without:*.iconSvg',
'*.url' => 'required'
]);
if ($validator->fails()) {
$errorMessage = 'Invalid quick action item detected in ' . $owner . '. Contact the plugin author to fix (' . $validator->errors()->first() . ')';
if (Config::get('app.debug', false)) {
throw new SystemException($errorMessage);
}
Log::error($errorMessage);
}
$this->addQuickActionItems($owner, $definitions);
}
/**
* Dynamically add an array of quick action items
*
* @param string $owner
* @param array $definitions
* @return void
*/
public function addQuickActionItems($owner, array $definitions)
{
foreach ($definitions as $code => $definition) {
$this->addQuickActionItem($owner, $code, $definition);
}
}
/**
* Dynamically add a single quick action item
*
* @param string $owner
* @param string $code
* @param array $definition
* @return void
*/
public function addQuickActionItem($owner, $code, array $definition)
{
$itemKey = $this->makeItemKey($owner, $code);
if (isset($this->quickActions[$itemKey])) {
$definition = array_merge((array) $this->quickActions[$itemKey], $definition);
}
$item = array_merge($definition, [
'code' => $code,
'owner' => $owner
]);
$this->quickActions[$itemKey] = QuickActionItem::createFromArray($item);
}
/**
* Gets the instance of a specified quick action item.
*
* @param string $owner
* @param string $code
* @return QuickActionItem
* @throws SystemException
*/
public function getQuickActionItem(string $owner, string $code)
{
$itemKey = $this->makeItemKey($owner, $code);
if (!array_key_exists($itemKey, $this->quickActions)) {
throw new SystemException('No quick action item found with key ' . $itemKey);
}
return $this->quickActions[$itemKey];
}
/**
* Removes a single quick action item
*
* @param $owner
* @param $code
* @return void
*/
public function removeQuickActionItem($owner, $code)
{
$itemKey = $this->makeItemKey($owner, $code);
unset($this->quickActions[$itemKey]);
}
/**
* Returns a list of quick action items.
*
* @return array
* @throws SystemException
*/
public function listQuickActionItems()
{
if ($this->items === null && $this->quickActions === null) {
$this->loadItems();
}
if ($this->quickActions === null) {
return [];
}
return $this->quickActions;
}
/**
* Sets the navigation context.
* The function sets the navigation owner, main menu item code and the side menu item code.
* @param string $owner Specifies the navigation owner in the format Vendor/Module
* @param string $mainMenuItemCode Specifies the main menu item code
* @param string $sideMenuItemCode Specifies the side menu item code
*/
public function setContext($owner, $mainMenuItemCode, $sideMenuItemCode = null)
{
$this->setContextOwner($owner);
$this->setContextMainMenu($mainMenuItemCode);
$this->setContextSideMenu($sideMenuItemCode);
}
/**
* Sets the navigation context owner.
*
* @param string $owner Specifies the navigation owner in the format Vendor/Module
*/
public function setContextOwner($owner)
{
$this->contextOwner = strtoupper($owner);
}
/**
* Gets the navigation context owner
*/
public function getContextOwner()
{
return $this->aliases[$this->contextOwner] ?? $this->contextOwner;
}
/**
* Specifies a code of the main menu item in the current navigation context.
* @param string $mainMenuItemCode Specifies the main menu item code
*/
public function setContextMainMenu($mainMenuItemCode)
{
$this->contextMainMenuItemCode = $mainMenuItemCode;
}
/**
* Returns information about the current navigation context.
* @return mixed Returns an object with the following fields:
* - mainMenuCode
* - sideMenuCode
* - owner
*/
public function getContext()
{
return (object)[
'mainMenuCode' => $this->contextMainMenuItemCode,
'sideMenuCode' => $this->contextSideMenuItemCode,
'owner' => $this->getContextOwner(),
];
}
/**
* Specifies a code of the side menu item in the current navigation context.
* If the code is set to TRUE, the first item will be flagged as active.
* @param string $sideMenuItemCode Specifies the side menu item code
*/
public function setContextSideMenu($sideMenuItemCode)
{
$this->contextSideMenuItemCode = $sideMenuItemCode;
}
/**
* Determines if a main menu item is active.
* @param MainMenuItem $item Specifies the item object.
* @return boolean Returns true if the menu item is active.
*/
public function isMainMenuItemActive($item)
{
return $this->getContextOwner() === strtoupper($item->owner) && $this->contextMainMenuItemCode === $item->code;
}
/**
* Returns the currently active main menu item
* @return null|MainMenuItem $item Returns the item object or null.
* @throws SystemException
*/
public function getActiveMainMenuItem()
{
foreach ($this->listMainMenuItems() as $item) {
if ($this->isMainMenuItemActive($item)) {
return $item;
}
}
return null;
}
/**
* Determines if a side menu item is active.
* @param SideMenuItem $item Specifies the item object.
* @return boolean Returns true if the side item is active.
*/
public function isSideMenuItemActive($item)
{
if ($this->contextSideMenuItemCode === true) {
$this->contextSideMenuItemCode = null;
return true;
}
return $this->getContextOwner() === strtoupper($item->owner) && $this->contextSideMenuItemCode === $item->code;
}
/**
* Registers a special side navigation partial for a specific main menu.
* The sidenav partial replaces the standard side navigation.
* @param string $owner Specifies the navigation owner in the format Vendor/Module.
* @param string $mainMenuItemCode Specifies the main menu item code.
* @param string $partial Specifies the partial name.
*/
public function registerContextSidenavPartial($owner, $mainMenuItemCode, $partial)
{
$this->contextSidenavPartials[$this->makeItemKey($owner, $mainMenuItemCode)] = $partial;
}
/**
* Returns the side navigation partial for a specific main menu previously registered
* with the registerContextSidenavPartial() method.
*
* @param string $owner Specifies the navigation owner in the format Vendor/Module.
* @param string $mainMenuItemCode Specifies the main menu item code.
* @return mixed Returns the partial name or null.
*/
public function getContextSidenavPartial($owner, $mainMenuItemCode)
{
return $this->contextSidenavPartials[$this->makeItemKey($owner, $mainMenuItemCode)] ?? null;
}
/**
* Removes menu items from an array if the supplied user lacks permission.
* @param \Backend\Models\User $user A user object
* @param MainMenuItem[]|SideMenuItem[] $items A collection of menu items
* @return array The filtered menu items
*/
protected function filterItemPermissions($user, array $items)
{
if (!$user) {
return $items;
}
$items = array_filter($items, static function ($item) use ($user) {
if (!$item->permissions || !count($item->permissions)) {
return true;
}
return $user->hasAnyAccess($item->permissions);
});
return $items;
}
/**
* Internal method to make a unique key for an item.
* @param string $owner
* @param string $code
* @return string
*/
protected function makeItemKey($owner, $code)
{
$owner = strtoupper($owner);
return ($this->aliases[$owner] ?? $owner) . '.' . strtoupper($code);
}
}

View File

@@ -0,0 +1,105 @@
<?php namespace Backend\Classes;
/**
* Class QuickActionItem
*
* @package Backend\Classes
*/
class QuickActionItem
{
/**
* @var string
*/
public $code;
/**
* @var string
*/
public $owner;
/**
* @var string
*/
public $label;
/**
* @var null|string
*/
public $icon;
/**
* @var null|string
*/
public $iconSvg;
/**
* @var string
*/
public $url;
/**
* @var int
*/
public $order = -1;
/**
* @var array
*/
public $attributes = [];
/**
* @var array
*/
public $permissions = [];
/**
* @param null|string|int $attribute
* @param null|string|array $value
*/
public function addAttribute($attribute, $value)
{
$this->attributes[$attribute] = $value;
}
public function removeAttribute($attribute)
{
unset($this->attributes[$attribute]);
}
/**
* @param string $permission
* @param array $definition
*/
public function addPermission(string $permission, array $definition)
{
$this->permissions[$permission] = $definition;
}
/**
* @param string $permission
* @return void
*/
public function removePermission(string $permission)
{
unset($this->permissions[$permission]);
}
/**
* @param array $data
* @return static
*/
public static function createFromArray(array $data)
{
$instance = new static();
$instance->code = $data['code'];
$instance->owner = $data['owner'];
$instance->label = $data['label'];
$instance->url = $data['url'];
$instance->icon = $data['icon'] ?? null;
$instance->iconSvg = $data['iconSvg'] ?? null;
$instance->attributes = $data['attributes'] ?? $instance->attributes;
$instance->permissions = $data['permissions'] ?? $instance->permissions;
$instance->order = (!empty($data['order']) || @$data['order'] === 0) ? (int) $data['order'] : $instance->order;
return $instance;
}
}

View File

@@ -0,0 +1,28 @@
<?php namespace Backend\Classes;
/**
* Report Widget base class
* Report widgets are used inside the ReportContainer.
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
class ReportWidgetBase extends WidgetBase
{
use \System\Traits\PropertyContainer;
public function __construct($controller, $properties = [])
{
$this->properties = $this->validateProperties($properties);
/*
* Ensure the provided alias (if present) takes effect as the widget configuration is
* not passed to the WidgetBase constructor which would normally take care of that
*/
if (!isset($this->alias)) {
$this->alias = $properties['alias'] ?? $this->defaultAlias;
}
parent::__construct($controller);
}
}

View File

@@ -0,0 +1,123 @@
<?php namespace Backend\Classes;
/**
* Class SideMenuItem
*
* @package Backend\Classes
*/
class SideMenuItem
{
/**
* @var string
*/
public $code;
/**
* @var string
*/
public $owner;
/**
* @var string
*/
public $label;
/**
* @var null|string
*/
public $icon;
/**
* @var null|string
*/
public $iconSvg;
/**
* @var string
*/
public $url;
/**
* @var null|int|callable
*/
public $counter;
/**
* @var null|string
*/
public $counterLabel;
/**
* @var null|string
*/
public $badge;
/**
* @var int
*/
public $order = -1;
/**
* @var array
*/
public $attributes = [];
/**
* @var array
*/
public $permissions = [];
/**
* @param null|string|int $attribute
* @param null|string|array $value
*/
public function addAttribute($attribute, $value)
{
$this->attributes[$attribute] = $value;
}
public function removeAttribute($attribute)
{
unset($this->attributes[$attribute]);
}
/**
* @param string $permission
* @param array $definition
*/
public function addPermission(string $permission, array $definition)
{
$this->permissions[$permission] = $definition;
}
/**
* @param string $permission
* @return void
*/
public function removePermission(string $permission)
{
unset($this->permissions[$permission]);
}
/**
* @param array $data
* @return static
*/
public static function createFromArray(array $data)
{
$instance = new static();
$instance->code = $data['code'];
$instance->owner = $data['owner'];
$instance->label = $data['label'];
$instance->url = $data['url'];
$instance->icon = $data['icon'] ?? null;
$instance->iconSvg = $data['iconSvg'] ?? null;
$instance->counter = $data['counter'] ?? null;
$instance->counterLabel = $data['counterLabel'] ?? null;
$instance->attributes = $data['attributes'] ?? $instance->attributes;
$instance->badge = $data['badge'] ?? null;
$instance->permissions = $data['permissions'] ?? $instance->permissions;
$instance->order = (!empty($data['order']) || @$data['order'] === 0) ? (int) $data['order'] : $instance->order;
return $instance;
}
}

View File

@@ -0,0 +1,111 @@
<?php namespace Backend\Classes;
use File;
use Config;
use Winter\Storm\Router\Helper as RouterHelper;
/**
* Skin Base class
* Used for defining skins.
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
abstract class Skin
{
/**
* Returns information about this skin, including name and description.
*/
abstract public function skinDetails();
/**
* @var string The absolute path to this skin.
*/
public $skinPath;
/**
* @var string The public path to this skin.
*/
public $publicSkinPath;
/**
* @var string The default skin path, usually the root level of modules/backend.
*/
public $defaultSkinPath;
/**
* @var string The default public skin path.
*/
public $defaultPublicSkinPath;
/**
* @var Self Cache of the active skin.
*/
private static $skinCache;
/**
* Constructor.
*/
public function __construct()
{
$this->defaultSkinPath = base_path() . '/modules/backend';
/*
* Guess the skin path
*/
$class = get_called_class();
$classFolder = strtolower(class_basename($class));
$classFile = realpath(dirname(File::fromClass($class)));
$this->skinPath = $classFile
? $classFile . '/' . $classFolder
: $this->defaultSkinPath;
$this->publicSkinPath = File::localToPublic($this->skinPath);
$this->defaultPublicSkinPath = File::localToPublic($this->defaultSkinPath);
}
/**
* Looks up a path to a skin-based file, if it doesn't exist, the default path is used.
* @param string $path
* @param boolean $isPublic
* @return string
*/
public function getPath($path = null, $isPublic = false)
{
$path = RouterHelper::normalizeUrl($path);
$assetFile = $this->skinPath . $path;
if (File::isFile($assetFile)) {
return $isPublic
? $this->publicSkinPath . $path
: $this->skinPath . $path;
}
return $isPublic
? $this->defaultPublicSkinPath . $path
: $this->defaultSkinPath . $path;
}
/**
* Returns an array of paths where skin layouts can be found.
* @return array
*/
public function getLayoutPaths()
{
return [$this->skinPath.'/layouts', $this->defaultSkinPath.'/layouts'];
}
/**
* Returns the active skin.
*/
public static function getActive()
{
if (self::$skinCache !== null) {
return self::$skinCache;
}
$skinClass = Config::get('cms.backendSkin');
$skinObject = new $skinClass();
return self::$skinCache = $skinObject;
}
}

View File

@@ -0,0 +1,216 @@
<?php namespace Backend\Classes;
use Winter\Storm\Html\Helper as HtmlHelper;
use Winter\Storm\Extension\Extendable;
use stdClass;
/**
* Widget base class.
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
abstract class WidgetBase extends Extendable
{
use \System\Traits\ViewMaker;
use \System\Traits\AssetMaker;
use \System\Traits\ConfigMaker;
use \System\Traits\EventEmitter;
use \Backend\Traits\ErrorMaker;
use \Backend\Traits\WidgetMaker;
use \Backend\Traits\SessionMaker;
/**
* @var object Supplied configuration.
*/
public $config;
/**
* @var \Backend\Classes\Controller Backend controller object.
*/
protected $controller;
/**
* @var string Defined alias used for this widget.
*/
public $alias;
/**
* @var string A unique alias to identify this widget.
*/
protected $defaultAlias = 'widget';
/**
* Constructor
* @param \Backend\Classes\Controller $controller
* @param array $configuration Proactive configuration definition.
*/
public function __construct($controller, $configuration = [])
{
$this->controller = $controller;
$this->viewPath = $this->configPath = $this->guessViewPath('/partials');
$this->assetPath = $this->guessViewPath('/assets', true);
/*
* Apply configuration values to a new config object, if a parent
* constructor hasn't done it already.
*/
if ($this->config === null) {
$this->config = $this->makeConfig($configuration);
}
/*
* If no alias is set by the configuration.
*/
if (!isset($this->alias)) {
$this->alias = $this->config->alias ?? $this->defaultAlias;
}
/*
* Prepare assets used by this widget.
*/
$this->loadAssets();
parent::__construct();
/*
* Initialize the widget.
*/
if (!$this->getConfig('noInit', false)) {
$this->init();
}
}
/**
* Initialize the widget, called by the constructor and free from its parameters.
* @return void
*/
public function init()
{
}
/**
* Renders the widget's primary contents.
* @return string HTML markup supplied by this widget.
*/
public function render()
{
}
/**
* Adds widget specific asset files. Use $this->addJs() and $this->addCss()
* to register new assets to include on the page.
* @return void
*/
protected function loadAssets()
{
}
/**
* Binds a widget to the controller for safe use.
* @return void
*/
public function bindToController()
{
if ($this->controller->widget === null) {
$this->controller->widget = new stdClass;
}
$this->controller->widget->{$this->alias} = $this;
}
/**
* Transfers config values stored inside the $config property directly
* on to the root object properties. If no properties are defined
* all config will be transferred if it finds a matching property.
* @param array $properties
* @return void
*/
protected function fillFromConfig($properties = null)
{
if ($properties === null) {
$properties = array_keys((array) $this->config);
}
foreach ($properties as $property) {
if (property_exists($this, $property)) {
$this->{$property} = $this->getConfig($property, $this->{$property});
}
}
}
/**
* Returns a unique ID for this widget. Useful in creating HTML markup.
* @param string $suffix An extra string to append to the ID.
* @return string A unique identifier.
*/
public function getId($suffix = null)
{
$id = class_basename(get_called_class());
if ($this->alias != $this->defaultAlias) {
$id .= '-' . $this->alias;
}
if ($suffix !== null) {
$id .= '-' . $suffix;
}
return HtmlHelper::nameToId($id);
}
/**
* Returns a fully qualified event handler name for this widget.
* @param string $name The ajax event handler name.
* @return string
*/
public function getEventHandler($name)
{
return $this->alias . '::' . $name;
}
/**
* Safe accessor for configuration values.
* @param string $name Config name, supports array names like "field[key]"
* @param string $default Default value if nothing is found
* @return string
*/
public function getConfig($name, $default = null)
{
/*
* Array field name, eg: field[key][key2][key3]
*/
$keyParts = HtmlHelper::nameToArray($name);
/*
* First part will be the field name, pop it off
*/
$fieldName = array_shift($keyParts);
if (!isset($this->config->{$fieldName})) {
return $default;
}
$result = $this->config->{$fieldName};
/*
* Loop the remaining key parts and build a result
*/
foreach ($keyParts as $key) {
if (!array_key_exists($key, $result)) {
return $default;
}
$result = $result[$key];
}
return $result;
}
/**
* Returns the controller using this widget.
*/
public function getController()
{
return $this->controller;
}
}

View File

@@ -0,0 +1,268 @@
<?php namespace Backend\Classes;
use Str;
use BackendAuth;
use SystemException;
use System\Classes\PluginManager;
use Event;
/**
* Widget manager
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*/
class WidgetManager
{
use \Winter\Storm\Support\Traits\Singleton;
/**
* @var array An array of form widgets. Stored in the form of ['FormWidgetClass' => $formWidgetInfo].
*/
protected $formWidgets;
/**
* @var array Cache of form widget registration callbacks.
*/
protected $formWidgetCallbacks = [];
/**
* @var array An array of form widgets keyed by their code. Stored in the form of ['formwidgetcode' => 'FormWidgetClass'].
*/
protected $formWidgetHints;
/**
* @var array An array of report widgets.
*/
protected $reportWidgets;
/**
* @var array Cache of report widget registration callbacks.
*/
protected $reportWidgetCallbacks = [];
/**
* @var System\Classes\PluginManager
*/
protected $pluginManager;
/**
* Initialize this singleton.
*/
protected function init()
{
$this->pluginManager = PluginManager::instance();
}
//
// Form Widgets
//
/**
* Returns a list of registered form widgets.
* @return array Array keys are class names.
*/
public function listFormWidgets()
{
if ($this->formWidgets === null) {
$this->formWidgets = [];
/*
* Load module widgets
*/
foreach ($this->formWidgetCallbacks as $callback) {
$callback($this);
}
/*
* Load plugin widgets
*/
$plugins = $this->pluginManager->getPlugins();
foreach ($plugins as $plugin) {
if (!is_array($widgets = $plugin->registerFormWidgets())) {
continue;
}
foreach ($widgets as $className => $widgetInfo) {
$this->registerFormWidget($className, $widgetInfo);
}
}
}
return $this->formWidgets;
}
/**
* Registers a single form widget.
* @param string $className Widget class name.
* @param array $widgetInfo Registration information, can contain a `code` key.
* @return void
*/
public function registerFormWidget($className, $widgetInfo = null)
{
if (!is_array($widgetInfo)) {
$widgetInfo = ['code' => $widgetInfo];
}
$widgetCode = $widgetInfo['code'] ?? null;
if (!$widgetCode) {
$widgetCode = Str::getClassId($className);
}
$this->formWidgets[$className] = $widgetInfo;
$this->formWidgetHints[$widgetCode] = $className;
}
/**
* Manually registers form widget for consideration. Usage:
*
* WidgetManager::registerFormWidgets(function ($manager) {
* $manager->registerFormWidget('Backend\FormWidgets\CodeEditor', 'codeeditor');
* });
*
*/
public function registerFormWidgets(callable $definitions)
{
$this->formWidgetCallbacks[] = $definitions;
}
/**
* Returns a class name from a form widget code
* Normalizes a class name or converts an code to its class name.
* @param string $name Class name or form widget code.
* @return string The class name resolved, or the original name.
*/
public function resolveFormWidget($name)
{
if ($this->formWidgets === null) {
$this->listFormWidgets();
}
$hints = $this->formWidgetHints;
if (isset($hints[$name])) {
return $hints[$name];
}
$_name = Str::normalizeClassName($name);
if (isset($this->formWidgets[$_name])) {
return $_name;
}
return $name;
}
//
// Report Widgets
//
/**
* Returns a list of registered report widgets.
* @return array Array keys are class names.
*/
public function listReportWidgets()
{
if ($this->reportWidgets === null) {
$this->reportWidgets = [];
/*
* Load module widgets
*/
foreach ($this->reportWidgetCallbacks as $callback) {
$callback($this);
}
/*
* Load plugin widgets
*/
$plugins = $this->pluginManager->getPlugins();
foreach ($plugins as $plugin) {
if (!is_array($widgets = $plugin->registerReportWidgets())) {
continue;
}
foreach ($widgets as $className => $widgetInfo) {
$this->registerReportWidget($className, $widgetInfo);
}
}
}
/**
* @event system.reportwidgets.extendItems
* Enables adding or removing report widgets.
*
* You will have access to the WidgetManager instance and be able to call the appropiate methods
* $manager->registerReportWidget();
* $manager->removeReportWidget();
*
* Example usage:
*
* Event::listen('system.reportwidgets.extendItems', function ($manager) {
* $manager->removeReportWidget('Acme\ReportWidgets\YourWidget');
* });
*
*/
Event::fire('system.reportwidgets.extendItems', [$this]);
$user = BackendAuth::getUser();
foreach ($this->reportWidgets as $widget => $config) {
if (!empty($config['permissions'])) {
if (!$user->hasAccess($config['permissions'], false)) {
unset($this->reportWidgets[$widget]);
}
}
}
return $this->reportWidgets;
}
/**
* Returns the raw array of registered report widgets.
* @return array Array keys are class names.
*/
public function getReportWidgets()
{
return $this->reportWidgets;
}
/*
* Registers a single report widget.
*/
public function registerReportWidget($className, $widgetInfo)
{
$this->reportWidgets[$className] = $widgetInfo;
}
/**
* Manually registers report widget for consideration. Usage:
*
* WidgetManager::registerReportWidgets(function ($manager) {
* $manager->registerReportWidget('Winter\GoogleAnalytics\ReportWidgets\TrafficOverview', [
* 'name' => 'Google Analytics traffic overview',
* 'context' => 'dashboard'
* ]);
* });
*
*/
public function registerReportWidgets(callable $definitions)
{
$this->reportWidgetCallbacks[] = $definitions;
}
/**
* Remove a registered ReportWidget.
* @param string $className Widget class name.
* @return void
*/
public function removeReportWidget($className)
{
if (!$this->reportWidgets) {
throw new SystemException('Unable to remove a widget before widgets are loaded.');
}
unset($this->reportWidgets[$className]);
}
}