feat: VivesPOS landing on Winter CMS 1.2 — theme + plugin + Dockerfile
Some checks are pending
Module sub-split / Sub-split (push) Waiting to run
Some checks are pending
Module sub-split / Sub-split (push) Waiting to run
- Base: wintercms/winter branch 1.2 (full framework) - Theme vivespos: Canvas 7 + Bootstrap 5 CDN, custom CSS - Layout: deferred GTM/GA4 tracking, JSON-LD SoftwareApplication - Partials: hero (offline-first), features, modes (offline/nube toggle), screenshots, pricing (3 planes), comparison, FAQ, CTA - Plugin VivesPOS.Site with ContactForm - Dockerfile: PHP 8.2 Apache, port 80, healthcheck - Added winter/wn-pages, blog, sitemap, seo plugins - Active theme set to vivespos
This commit is contained in:
289
modules/backend/classes/AuthManager.php
Normal file
289
modules/backend/classes/AuthManager.php
Normal 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);
|
||||
}
|
||||
}
|
||||
347
modules/backend/classes/BackendController.php
Normal file
347
modules/backend/classes/BackendController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
823
modules/backend/classes/Controller.php
Normal file
823
modules/backend/classes/Controller.php
Normal 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);
|
||||
}
|
||||
}
|
||||
168
modules/backend/classes/ControllerBehavior.php
Normal file
168
modules/backend/classes/ControllerBehavior.php
Normal 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);
|
||||
}
|
||||
}
|
||||
168
modules/backend/classes/FilterScope.php
Normal file
168
modules/backend/classes/FilterScope.php
Normal 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);
|
||||
}
|
||||
}
|
||||
764
modules/backend/classes/FormField.php
Normal file
764
modules/backend/classes/FormField.php
Normal 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});
|
||||
}
|
||||
}
|
||||
279
modules/backend/classes/FormTabs.php
Normal file
279
modules/backend/classes/FormTabs.php
Normal 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;
|
||||
}
|
||||
}
|
||||
152
modules/backend/classes/FormWidgetBase.php
Normal file
152
modules/backend/classes/FormWidgetBase.php
Normal 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);
|
||||
}
|
||||
}
|
||||
291
modules/backend/classes/ListColumn.php
Normal file
291
modules/backend/classes/ListColumn.php
Normal 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;
|
||||
}
|
||||
}
|
||||
131
modules/backend/classes/MainMenuItem.php
Normal file
131
modules/backend/classes/MainMenuItem.php
Normal 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;
|
||||
}
|
||||
}
|
||||
796
modules/backend/classes/NavigationManager.php
Normal file
796
modules/backend/classes/NavigationManager.php
Normal 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);
|
||||
}
|
||||
}
|
||||
105
modules/backend/classes/QuickActionItem.php
Normal file
105
modules/backend/classes/QuickActionItem.php
Normal 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;
|
||||
}
|
||||
}
|
||||
28
modules/backend/classes/ReportWidgetBase.php
Normal file
28
modules/backend/classes/ReportWidgetBase.php
Normal 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);
|
||||
}
|
||||
}
|
||||
123
modules/backend/classes/SideMenuItem.php
Normal file
123
modules/backend/classes/SideMenuItem.php
Normal 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;
|
||||
}
|
||||
}
|
||||
111
modules/backend/classes/Skin.php
Normal file
111
modules/backend/classes/Skin.php
Normal 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;
|
||||
}
|
||||
}
|
||||
216
modules/backend/classes/WidgetBase.php
Normal file
216
modules/backend/classes/WidgetBase.php
Normal 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;
|
||||
}
|
||||
}
|
||||
268
modules/backend/classes/WidgetManager.php
Normal file
268
modules/backend/classes/WidgetManager.php
Normal 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]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user