praiadeseselle/wire/core/AdminThemeFramework.php
2022-03-08 15:55:41 +01:00

827 lines
24 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php namespace ProcessWire;
/**
* AdminTheme Framework
*
* The methods in this class may eventually be merged to AdminTheme.php,
* but are isolated to this class during development.
*
* This file is licensed under the MIT license.
* https://processwire.com/about/license/mit/
*
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
* https://processwire.com
*
* @property bool $isSuperuser
* @property bool $isEditor
* @property bool $isLoggedIn
* @property bool|string $isModal
* @property bool|int $useAsLogin
* @method array getUserNavArray()
* @method array getPrimaryNavArray()
*
*/
abstract class AdminThemeFramework extends AdminTheme {
/**
* Is there currently a logged in user?
*
* @var bool
*
*/
protected $isLoggedIn = false;
/**
* Is user logged in with page-edit permission?
*
* @var bool
*
*/
protected $isEditor = false;
/**
* Is current user a superuser?
*
* @var bool
*
*/
protected $isSuperuser = false;
/**
* Is the current request a modal request?
*
* @var bool|string Either false, true, or "inline"
*
*/
protected $isModal = false;
/**
* @var Sanitizer
*
*/
protected $sanitizer;
/**
* Construct
*
*/
public function __construct() {
parent::__construct();
$this->set('useAsLogin', false);
}
public function wired() {
$this->sanitizer = $this->wire('sanitizer');
$user = $this->wire()->user;
$this->isLoggedIn = $user && $user->isLoggedin();
parent::wired();
}
/**
* Override get() method from WireData to support additional properties
*
* @param string $key
* @return bool|int|mixed|null|string
*
*/
public function get($key) {
switch($key) {
case 'isSuperuser': $value = $this->isSuperuser; break;
case 'isEditor': $value = $this->isEditor; break;
case 'isLoggedIn': $value = $this->isLoggedIn; break;
case 'isModal': $value = $this->isModal; break;
default: $value = parent::get($key);
}
return $value;
}
/**
* Initialize and attach hooks
*
* Note: descending classes should call this after API ready
*
*/
public function init() {
$user = $this->wire('user');
if(!$this->isLoggedIn && $this->useAsLogin) $this->setCurrent();
parent::init();
// if this is not the current admin theme, exit now so no hooks are attached
if(!$this->isCurrent()) return;
$this->isSuperuser = $this->isLoggedIn && $user->isSuperuser();
$this->isEditor = $this->isLoggedIn && ($this->isSuperuser || $user->hasPermission('page-edit'));
$this->includeInitFile();
$modal = $this->wire('input')->get('modal');
if($modal) $this->isModal = $modal == 'inline' ? 'inline' : true;
// test notices when requested
if($this->wire('input')->get('test_notices') && $this->isLoggedIn) $this->testNotices();
}
/**
* Include the admin theme init file
*
*/
public function includeInitFile() {
$config = $this->wire('config');
$initFile = $this->path() . 'init.php';
if(file_exists($initFile)) {
if(strpos($initFile, $config->paths->site) === 0) {
// admin themes in /site/modules/ may be compiled
$initFile = $this->wire('files')->compile($initFile);
}
/** @noinspection PhpIncludeInspection */
include_once($initFile);
}
}
/**
* Perform a translation, based on text from shared admin file: /wire/templates-admin/default.php
*
* @param string $text
* @return string
*
*/
public function _($text) {
static $translate = null;
if(is_null($translate)) $translate = $this->wire('languages') !== null;
if($translate === false) return $text;
$value = __($text, $this->wire('config')->paths->root . 'wire/templates-admin/default.php');
if($value === $text) $value = parent::_($text);
return $value;
}
/**
* Get the current page headline
*
* @return string
*
*/
public function getHeadline() {
$headline = $this->wire('processHeadline');
if(!$headline) $headline = $this->wire('page')->get('title|name');
if($headline !== 'en' && $this->wire('languages')) $headline = $this->_($headline);
return $this->sanitizer->entities1($headline);
}
/**
* Get navigation title for the given page, return blank if page should not be shown
*
* @param Page $p
* @return string
*
*/
public function getPageTitle(Page $p) {
if($p->name == 'add' && $p->parent->name == 'page') {
$title = $this->getAddNewLabel();
} else {
$title = $this->_($p->title);
}
$title = $this->sanitizer->entities1($title);
return $title;
}
/**
* Get icon used by the given page
*
* @param Page $p
* @return mixed|null|string
*
*/
public function getPageIcon(Page $p) {
$icon = '';
if($p->template == 'admin') {
$info = $this->wire('modules')->getModuleInfo($p->process);
if(!empty($info['icon'])) $icon = $info['icon'];
}
// allow for option of an admin field overriding the module icon
$pageIcon = $p->get('page_icon');
if($pageIcon) $icon = $pageIcon;
if(!$icon) switch($p->id) {
case 22: $icon = 'gears'; break; // Setup
case 21: $icon = 'plug'; break; // Modules
case 28: $icon = 'key'; break; // Access
}
if(!$icon && $p->parent->id != $this->wire('config')->adminRootPageID) {
$icon = 'file-o ui-priority-secondary';
}
return $icon;
}
/**
* Get “Add New” button actions
*
* - Returns array of arrays, each with 'url', 'label' and 'icon' properties.
* - Returns empty array if Add New button should not be displayed.
*
* @return array
*
*/
public function getAddNewActions() {
$page = $this->wire('page');
$process = $this->wire('process');
$input = $this->wire('input');
if(!$this->isEditor) return array();
if($page->name != 'page' || $this->wire('input')->urlSegment1) return array();
if($input->urlSegment1 || $input->get('modal')) return array();
if(strpos($process, 'ProcessPageList') !== 0) return array();
/** @var ProcessPageAdd $module */
$module = $this->wire('modules')->getModule('ProcessPageAdd', array('noInit' => true));
$data = $module->executeNavJSON(array('getArray' => true));
$actions = array();
foreach($data['list'] as $item) {
$item['url'] = $data['url'] . $item['url'];
$actions[] = $item;
}
return $actions;
}
/**
* Get the translated “Add New” label thats used in a couple spots
*
* @return string
*
*/
public function getAddNewLabel() {
return $this->_('Add New');
}
/**
* Get the classes that will be used in the <body class=''> tag
*
* @return string
*
*/
public function getBodyClass() {
$page = $this->wire('page');
$process = $this->wire('process');
$classes = array(
"id-{$page->id}",
"template-{$page->template->name}",
"pw-init",
parent::getBodyClass(),
);
if($this->isModal) $classes[] = 'modal';
if($this->isModal === 'inline') $classes[] = 'modal-inline';
if($this->wire('input')->urlSegment1) $classes[] = 'hasUrlSegments';
if($process) $classes[] = $process->className();
if(!$this->isLoggedIn) $classes[] = 'pw-guest';
return implode(' ', $classes);
}
/**
* Get Javascript that must be present in the document <head>
*
* @return string
*
*/
public function getHeadJS() {
$config = $this->wire()->config;
return
"var ProcessWire = { config: " . wireEncodeJSON($config->js(), true, $config->debug) . " }; " .
"var config = ProcessWire.config;\n"; // legacy support
}
/**
* Allow the given Page to appear in admin theme navigation?
*
* @param Page $p Page to test
* @param PageArray|array $children Children of page, if applicable (optional)
* @param string|null $permission Specify required permission (optional)
* @return bool
*
*/
public function allowPageInNav(Page $p, $children = array(), $permission = null) {
if($this->isSuperuser) return true;
$pageViewable = $p->viewable();
if(!$pageViewable) return false;
$allow = false;
$numChildren = count($children);
if($p->process == 'ProcessPageAdd') {
// ProcessPageAdd: avoid showing this menu item if there are no predefined family settings to use
$numAddable = $this->wire('session')->getFor('ProcessPageAdd', 'numAddable');
if($numAddable === null) {
/** @var ProcessPageAdd $processPageAdd */
$processPageAdd = $this->wire('modules')->getModule('ProcessPageAdd', array('noInit' => true));
if($processPageAdd) {
$addData = $processPageAdd->executeNavJSON(array('getArray' => true));
$numAddable = $addData['list'];
$this->wire('session')->setFor('ProcessPageAdd', 'numAddable', $numAddable);
}
}
// no addable options, so do not show the "Add New" item
if(!$numAddable) return false;
} else if(empty($permission)) {
// no permission specified
if(!$p->process) {
// no process module present, so we delegate to just the page viewable state if no children to check
if($pageViewable && !$numChildren) return true;
} else if($p->process == 'ProcessList') {
// page just serves as a list for children
} else {
// determine permission from Process module, if present
$moduleInfo = $this->wire('modules')->getModuleInfo($p->process);
if(!empty($moduleInfo['permission'])) $permission = $moduleInfo['permission'];
}
}
if($permission) {
// specific permission required to determine view access
$allow = $this->wire('user')->hasPermission($permission);
} else if($pageViewable && $p->parent_id == $this->wire('config')->adminRootPageID) {
// primary nav page requires that at least one child is viewable
foreach($children as $child) {
if($this->allowPageInNav($child)) {
$allow = true;
break;
}
}
}
return $allow;
}
/**
* Return nav array of primary navigation
*
* @return array
*
*/
public function ___getPrimaryNavArray() {
$items = array();
$config = $this->wire('config');
$admin = $this->wire('pages')->get($config->adminRootPageID);
foreach($admin->children("check_access=0") as $p) {
$item = $this->pageToNavArray($p);
if($item) $items[] = $item;
}
return $items;
}
/**
* Get navigation array from a Process module
*
* @param array|Module|string $module Module info array or Module object or string
* @param Page $p Page upon which the Process module is contained
* @return array
*
*/
public function moduleToNavArray($module, Page $p) {
$config = $this->wire('config');
$modules = $this->wire('modules');
$textdomain = str_replace($config->paths->root, '/', $modules->getModuleFile($p->process));
$user = $this->wire('user');
$navArray = array();
if(is_array($module)) {
$moduleInfo = $module;
} else {
$moduleInfo = $modules->getModuleInfo($module);
}
foreach($moduleInfo['nav'] as $navItem) {
$permission = empty($navItem['permission']) ? '' : $navItem['permission'];
if($permission && !$user->hasPermission($permission)) continue;
$navArray[] = array(
'id' => 0,
'parent_id' => $p->id,
'title' => $this->sanitizer->entities1(__($navItem['label'], $textdomain)), // translate from context of Process module
'name' => '',
'url' => $p->url . $navItem['url'],
'icon' => empty($navItem['icon']) ? '' : $navItem['icon'],
'children' => array(),
'navJSON' => empty($navItem['navJSON']) ? '' : $p->url . $navItem['navJSON'],
);
}
return $navArray;
}
/**
* Get a navigation array the given Page, or null if page not allowed in nav
*
* @param Page $p
* @return array|null
*
*/
public function pageToNavArray(Page $p) {
$children = $p->numChildren ? $p->children("check_access=0") : array();
if(!$this->allowPageInNav($p, $children)) return null;
$navArray = array(
'id' => $p->id,
'parent_id' => $p->parent_id,
'url' => $p->url,
'name' => $p->name,
'title' => $this->getPageTitle($p),
'icon' => $this->getPageIcon($p),
'children' => array(),
'navJSON' => '',
);
if(!count($children)) {
// no children available
if($p->template == 'admin' && $p->process) {
// see if process module defines its own navigation
$moduleInfo = $this->wire('modules')->getModuleInfo($p->process);
if(!empty($moduleInfo['nav'])) {
$navArray['children'] = $this->moduleToNavArray($moduleInfo, $p);
}
} else {
// The /page/ and /page/list/ are the same process, so just keep them on /page/ instead.
if(strpos($navArray['url'], '/page/list/') !== false) {
$navArray['url'] = str_replace('/page/list/', '/page/', $navArray['url']);
}
}
return $navArray;
}
// if we reach this point, then we have a PageArray of children
$modules = $this->wire('modules');
foreach($children as $c) {
if(!$c->process) continue;
$moduleInfo = $modules->getModuleInfo($c->process);
$permission = empty($moduleInfo['permission']) ? '' : $moduleInfo['permission'];
if(!$this->allowPageInNav($c, array(), $permission)) continue;
$childItem = array(
'id' => $c->id,
'parent_id' => $c->parent_id,
'title' => $this->getPageTitle($c),
'name' => $c->name,
'url' => $c->url,
'icon' => $this->getPageIcon($c),
'children' => array(),
'navJSON' => empty($moduleInfo['useNavJSON']) ? '' : $c->url . 'navJSON/',
);
if(!empty($moduleInfo['nav'])) {
$childItem['children'] = $this->moduleToNavArray($moduleInfo, $c);
}
$navArray['children'][] = $childItem;
} // foreach
return $navArray;
}
/**
* Get navigation items for the “user” menu
*
* This is hookable so that something else could add stuff to it.
* See the method body for details on format used.
*
* @return array
*
*/
public function ___getUserNavArray() {
$urls = $this->wire('urls');
$navArray = array();
$navArray[] = array(
'url' => $urls->root,
'title' => $this->_('View site'),
'target' => '_top',
'icon' => 'eye',
);
if($this->wire('user')->hasPermission('profile-edit')) $navArray[] = array(
'url' => $urls->admin . 'profile/',
'title' => $this->_('Profile'),
'icon' => 'user',
'permission' => 'profile-edit',
);
$navArray[] = array(
'url' => $urls->admin . 'login/logout/',
'title' => $this->_('Logout'),
'target' => '_top',
'icon' => 'power-off',
);
return $navArray;
}
/**
* Get the browser <title>
*
* @return string
*
*/
public function getBrowserTitle() {
$browserTitle = $this->wire('processBrowserTitle');
$modal = $this->wire('input')->get('modal');
if(!$browserTitle) {
if($modal) return $this->wire('processHeadline');
$browserTitle = $this->_(strip_tags($this->wire('page')->get('title|name'))) . ' • ProcessWire';
}
if(!$modal) {
$httpHost = $this->wire('config')->httpHost;
if(strpos($httpHost, 'www.') === 0) $httpHost = substr($httpHost, 4); // remove www
if(strpos($httpHost, ':')) $httpHost = preg_replace('/:\d+/', '', $httpHost); // remove port
$browserTitle .= "$httpHost";
}
return $this->sanitizer->entities1($browserTitle);
}
/**
* Test all notice types
*
* @return bool
*
*/
public function testNotices() {
if(!$this->wire('user')->isLoggedin()) return false;
$this->message('Message test');
$this->message('Message test debug', Notice::debug);
$this->message('Message test markup <a href="#">example</a>', Notice::allowMarkup);
$this->warning('Warning test');
$this->warning('Warning test debug', Notice::debug);
$this->warning('Warning test markup <a href="#">example</a>', Notice::allowMarkup);
$this->error('Error test');
$this->error('Error test debug', Notice::debug);
$this->error('Error test markup <a href="#">example</a>', Notice::allowMarkup);
return true;
}
/**
* Render runtime notices div#notices
*
* @param Notices|bool $notices Notices object or specify boolean true to return array of all available $options
* @param array $options See defaults in method
* @return string|array Returns string unless you specify true for $notices argument, then it returns an array.
*
*/
public function renderNotices($notices, array $options = array()) {
$defaults = array(
'messageClass' => 'NoticeMessage', // class for messages
'messageIcon' => 'check-square', // default icon to show with notices
'warningClass' => 'NoticeWarning', // class for warnings
'warningIcon' => 'exclamation-circle', // icon for warnings
'errorClass' => 'NoticeError', // class for errors
'errorIcon' => 'exclamation-triangle', // icon for errors
'debugClass' => 'NoticeDebug', // class for debug items (appended)
'debugIcon' => 'bug', // icon for debug notices
'closeClass' => 'pw-notice-remove notice-remove', // class for close notices link <a>
'closeIcon' => 'times', // icon for close notices link
'listMarkup' => "<ul class='pw-notices' id='notices'>{out}</ul><!--/notices-->",
'itemMarkup' => "<li class='{class}'>{remove}{icon}{text}</li>",
// the following apply only when groupByType==true
'groupByType' => true, // Group notices by type
'groupParentClass' => 'pw-notice-group-parent', // class for parent notices
'groupChildClass' => 'pw-notice-group-child', // class for children (of parent notices)
'groupToggleMarkup' => "<a class='pw-notice-group-toggle' href='#'>{label}" .
"<i class='fa fa-fw fa-bell-o' data-toggle='fa-bell-o fa-bell'></i>" .
"<i class='fa fa-fw fa-angle-right' data-toggle='fa-angle-right fa-angle-down'></i></a>",
'groupToggleLabel' => $this->_("+{n-1}"),
);
$options = array_merge($defaults, $options);
if($notices === true) return $options;
$config = $this->wire('config');
$noticesArray = array();
$out = '';
$removeIcon = $this->renderIcon($options['closeIcon']);
$removeLabel = $this->_('Close all');
$removeLink = "<a class='$options[closeClass]' href='#' title='$removeLabel'>$removeIcon</a>";
if($this->isLoggedIn && $this->wire('modules')->isInstalled('SystemNotifications')) {
$defaults['groupByType'] = false;
//$systemNotifications = $this->wire('modules')->get('SystemNotifications');
//if(!$systemNotifications->placement) return '';
}
foreach($notices as $n => $notice) {
$text = $notice->text;
$allowMarkup = $notice->flags & Notice::allowMarkup;
$groupByType = $options['groupByType'] && !($notice->flags & Notice::noGroup) && !($notice instanceof NoticeError);
if($allowMarkup) {
// leave $text alone
} else {
// unencode + re-encode entities, just in case module already entity some or all of output
if(strpos($text, '&') !== false) $text = $this->sanitizer->unentities($text);
$text = $this->sanitizer->entities($text);
$text = nl2br($text);
}
if($notice instanceof NoticeError) {
$class = $options['errorClass'];
$icon = $options['errorIcon'];
$noticeType = 'errors';
} else if($notice instanceof NoticeWarning) {
$class = $options['warningClass'];
$icon = $options['warningIcon'];
$noticeType = 'warnings';
} else {
$class = $options['messageClass'];
$icon = $options['messageIcon'];
$noticeType = 'messages';
}
if($notice->flags & Notice::debug) {
$class .= " " . $options['debugClass'];
$icon = $options['debugIcon'];
// ensure non-debug version is set as well
if(!isset($noticesArray[$noticeType])) $noticesArray[$noticeType] = array();
$noticeType .= "-debug";
}
// indicate which class the notice originated from in debug mode
if($notice->class && $config->debug) $text = "{$notice->class}: $text";
$replacements = array(
'{class}' => $class,
'{remove}' => '',
'{icon}' => $this->renderNavIcon($notice->icon ? $notice->icon : $icon),
'{text}' => $text,
);
if($groupByType) {
if(!isset($noticesArray[$noticeType])) $noticesArray[$noticeType] = array();
$noticesArray[$noticeType][] = $replacements;
} else {
if($n === 0) $replacements['{remove}'] = $removeLink;
$out .= str_replace(array_keys($replacements), array_values($replacements), $options['itemMarkup']);
}
}
if($options['groupByType']) {
$cnt = 0;
foreach($noticesArray as $noticeType => $noticeReplacements) {
if(strpos($noticeType, '-debug')) continue;
if(isset($noticesArray["$noticeType-debug"])) {
$noticeReplacements = array_merge($noticeReplacements, $noticesArray["$noticeType-debug"]);
}
$n = count($noticeReplacements);
if($n > 1) {
$notice =& $noticeReplacements[0];
$label = str_replace(array('{n}', '{n-1}'), array($n, $n-1), $options['groupToggleLabel']);
$notice['{text}'] .= ' ' . str_replace(array('{label}'), array($label), $options['groupToggleMarkup']);
$notice['{class}'] .= ' ' . $options['groupParentClass'];
$childClass = $options['groupChildClass'];
} else {
$childClass = '';
}
foreach($noticeReplacements as $i => $replacements) {
if(!$cnt) $replacements['{remove}'] = $removeLink;
if($childClass && $i > 0) $replacements['{class}'] .= ' ' . $childClass;
$out .= str_replace(array_keys($replacements), array_values($replacements), $options['itemMarkup']);
$cnt++;
}
}
}
$out = str_replace('{out}', $out, $options['listMarkup']);
$out .= $this->renderExtraMarkup('notices');
return $out;
}
/**
* Render markup for a font-awesome icon
*
* @param string $icon Name of icon to render, excluding the “fa-” prefix
* @param bool $fw Specify true to make fixed width (default=false).
* @return string
*
*/
public function renderIcon($icon, $fw = false) {
if($fw) $icon .= ' fa-fw';
return "<i class='fa fa-$icon'></i>";
}
/**
* Render markup for a font-awesome icon that precedes a navigation label
*
* This is the same as renderIcon() except that fixed-width is assumed and a "nav-nav-icon"
* class is added to it.
*
* @param string $icon Name of icon to render, excluding the “fa-” prefix
* @return string
*
*/
public function renderNavIcon($icon) {
return $this->renderIcon("$icon pw-nav-icon", true);
}
/**
* Render an extra markup region
*
* @param string $for
* @return mixed|string
*
*/
public function renderExtraMarkup($for) {
static $extras = array();
if(empty($extras)) $extras = $this->getExtraMarkup();
return isset($extras[$for]) ? $extras[$for] : '';
}
/**
* Module Configuration
*
* @param InputfieldWrapper $inputfields
*
*/
public function getModuleConfigInputfields(InputfieldWrapper $inputfields) {
$modules = $this->wire()->modules;
$input = $this->wire()->input;
$roles = $this->wire()->roles;
/** @var InputfieldCheckbox $f */
$f = $modules->get('InputfieldCheckbox');
$f->name = 'useAsLogin';
$f->label = $this->_('Use this admin theme for login screen?');
$f->description = $this->_('When checked, this admin theme will be used on the user login screen.');
$f->icon = 'sign-in';
$f->columnWidth = 50;
if($this->get('useAsLogin')) $f->attr('checked', 'checked');
$inputfields->add($f);
if($f->attr('checked') && $input->requestMethod('GET')) {
$themes = $modules->findByPrefix('AdminTheme');
$class = $this->className();
foreach($themes as $name) {
if($name === $class) continue;
$cfg = $modules->getConfig($name);
if(!empty($cfg['useAsLogin'])) {
unset($cfg['useAsLogin']);
$modules->saveConfig($name, $cfg);
$this->message("Removed 'useAsLogin' setting from $name", Notice::debug);
}
}
}
/** @var InputfieldSelect $f */
$f = $modules->get('InputfieldSelect');
$f->name = '_setAdminThemeRoleId';
$f->label = $this->_('Change users to this admin theme');
$f->description = $this->_('Select user role to update matching users to this admin theme.');
$f->columnWidth = 50;
$f->icon = 'users';
foreach($roles as $role) {
if($role->name === 'guest') continue;
$f->addOption($role->id, sprintf($this->_('Role: %s'), $role->name));
}
$inputfields->add($f);
$roleId = (int) $input->post('_setAdminThemeRoleId');
$role = $roleId ? $roles->get($roleId) : null;
if($role) {
$n = $this->wire()->users->setAdminThemeByRole($this, $role);
$this->message(sprintf($this->_('Set %d user(s) to have this admin theme'), $n), Notice::noGroup);
}
}
}