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; $input = $this->wire()->input; 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 = $input->get('modal'); if($modal) $this->isModal = $modal == 'inline' ? 'inline' : true; // test notices when requested if($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; static $context = null; if($translate === null) $translate = $this->wire()->languages !== null; if($translate === false) return $text; if($context === null) $context = $this->wire()->config->paths->root . 'wire/templates-admin/default.php'; $value = __($text, $context); 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' || $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 that’s used in a couple spots * * @return string * */ public function getAddNewLabel() { return $this->_('Add New'); } /** * Get the classes that will be used in the 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 * * @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 $session = $this->wire()->session; $numAddable = $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']; $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(!$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($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; $user = $this->wire()->user; $textdomain = str_replace($config->paths->root, '/', $modules->getModuleFile($p->process)); $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 * * @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) { /** @var Notice $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] : ''; } /** * Render a admin theme template file * * This method is only used if it is hooked * * #pw-hooker * * @param string $file Full path and filename * @param array $vars Associative array of variables to populate in rendered file * @return string Returns blank string when $echo is true * @since 3.0.196 * */ protected function ___renderFile($file, array $vars = array()) { extract($vars); ob_start(); include($file); $out = ob_get_contents(); ob_end_clean(); return $out; } /** * Include an admin theme file * * @param string $basename * @param array $vars * @since 3.0.196 * */ public function includeFile($basename, array $vars = array()) { $file = ''; if($this->renderFileHooked === null) { $this->renderFileHooked = $this->hasHook('renderFile()'); } if($this->defaultTemplatePath === null) { $this->defaultTemplatePath = $this->wire()->config->paths($this); } if($this->customTemplatePath === null) { $path = $this->wire()->config->paths->templates . $this->className() . '/'; $this->customTemplatePath = is_dir($path) ? $path : ''; } if($this->customTemplatePath) { $file = $this->customTemplatePath . $basename; if(!file_exists($file)) $file = ''; } if($file === '') $file = $this->defaultTemplatePath . $basename; $fuel = $this->wire()->fuel; $vars['fuel'] = $fuel; $vars = array_merge($fuel->getArray(), $vars); unset($vars['file'], $vars['vars']); // just in case either was set if($this->renderFileHooked) { echo $this->renderFile($file, $vars); } else { extract($vars); include($file); } } /** * Set custom path for admin theme templates * * This is for modules to optionally set a custom template path. If not set then the default * in /site/templates/AdminTheme[Class]/ is used. * * @param string $path * @since 3.0.196 * */ public function setCustomTemplatePath($path) { $this->customTemplatePath = $path; } /** * 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); } } }