__('Users', __FILE__), // getModuleInfo title 'version' => 107, 'summary' => __('Manage system users', __FILE__), // getModuleInfo summary 'permanent' => true, 'permission' => 'user-admin', 'icon' => 'group', 'useNavJSON' => true, 'searchable' => 'users' ); } /** * Construct and set default config values * */ public function __construct() { $this->set("maxAjaxQty", 25); return parent::__construct(); } /** * Init and prepare for execute methods * */ public function init() { $this->wire('pages')->addHookBefore('save', $this, 'hookPageSave'); parent::init(); if($this->lister) $this->lister->addHookBefore('execute', $this, 'hookListerExecute'); // make this field translatable $roles = $this->wire('fields')->get('roles'); $roles->label = $this->_('Roles'); $roles->description = $this->_("User will inherit the permissions assigned to each role. You may assign multiple roles to a user. When accessing a page, the user will only inherit permissions from the roles that are also assigned to the page's template."); // Roles description } /** * Determine whether Lister should be used or not * * @return bool * */ protected function useLister() { return $this->wire('user')->hasPermission('page-lister'); } /** * Output JSON list of navigation items for this (intended to for ajax use) * * @param array $options * @return string|array * */ public function ___executeNavJSON(array $options = array()) { $max = $this->maxAjaxQty > 0 && $this->maxAjaxQty <= 100 ? (int) $this->maxAjaxQty : 50; if(empty($options) && $this->pages->count("id>0") > $max) { $user = $this->wire('user'); $userAdminAll = $user->isSuperuser() ? $this->wire('pages')->newNullPage() : $this->wire('permissions')->get('user-admin-all'); /** @var PagePermissions $pagePermissions */ $pagePermissions = $this->wire('modules')->get('PagePermissions'); $items = array(); foreach($this->wire('roles') as $role) { if($userAdminAll->id && !$pagePermissions->userCanAssignRole($role)) continue; $cnt = $this->pages->count("roles=$role"); $item = array( 'id' => $role->id, 'name' => $role->name, 'cnt' => sprintf($this->_n('%d user', '%d users', $cnt), $cnt) ); $items[] = $item; } $options['itemLabel'] = 'name'; $options['itemLabel2'] = 'cnt'; $options['add'] = 'add/'; $options['items'] = $items; $options['edit'] = "?roles={id}"; } return parent::___executeNavJSON($options); } /** * Add item of this page type * * @return string * */ public function ___executeAdd() { // use parent method if there are no custom user parents or templates $config = $this->wire()->config; if(count($config->usersPageIDs) < 2 && count($config->userTemplateIDs) < 2) return parent::___executeAdd(); $input = $this->wire()->input; $pages = $this->wire()->pages; $parentId = (int) $input->get('parent_id'); $parent = $parentId ? $pages->get($parentId) : null; $userTemplates = $this->pages->getTemplates(); $userParentIds = $this->pages->getParentIDs(); // if requested parent not one allowed by config.usersPageIDs then disregard it if($parent && !in_array($parent->id, $userParentIds, true)) $parent = null; // delegate to ProcessPageAdd $editor = $this->wire()->modules->getModule('ProcessPageAdd'); /** @var ProcessPageAdd $editor */ $this->editor = $editor; $editor->setEditor($this); // set us as the parent editor // identify parent(s) allowed if(count($userParentIds) > 1 && $parent) { // more than one parent allowed but only one requested $parents = $pages->newPageArray(); $parents->add($parent); $editor->parent_id = $parent->id; $editor->setPredefinedParents($parents); } else { // one or more parents allowed $editor->setPredefinedParents($pages->getById($userParentIds)); } // identify template(s) allowed if(count($userTemplates) < 2) { // only one user template allowed $editor->template = $this->template; } else if($parent) { // parent specified, reduce to only allowed templates for parent $childTemplates = $parent->template->childTemplates; if(count($childTemplates)) { foreach($userTemplates as $key => $template) { /** @var Template $template */ if(!in_array($template->id, $childTemplates)) unset($userTemplates[$key]); } } if(!count($userTemplates)) $userTemplates = array($this->template); } $editor->setPredefinedTemplates($userTemplates); try { $out = $editor->execute(); } catch(\Exception $e) { $out = ''; $this->error($e->getMessage()); if($input->is('POST')) { $this->wire()->session->location("./" . ($parentId ? "?parent_id=$parentId" : "")); } } $this->browserTitle($this->page->get('title|name') . " > $this->addLabel"); return $out; } /** * Hook to ProcessPageLister::execute method to adjust selector to show specific roles * * @param HookEvent $event * */ public function hookListerExecute($event) { $role = (int) $this->wire()->session->getFor($this, 'listerRole'); if(!$role) return; /** @var ProcessPageLister $lister */ $lister = $event->object; $defaultSelector = $lister->defaultSelector; if(strpos($defaultSelector, 'roles=') !== false) { $defaultSelector = preg_replace('/\broles=\d*/', "roles=$role", $defaultSelector); } else { $defaultSelector .= ", roles=$role"; } $lister->defaultSelector = $defaultSelector; } /** * Get settings to be used for Lister * * @param ProcessPageLister $lister * @param string $selector * @return array * */ protected function getListerSettings(ProcessPageLister $lister, $selector) { $settings = parent::getListerSettings($lister, $selector); $selector = ''; $role = (int) $this->wire()->input->get('roles'); $user = $this->wire()->user; $session = $this->wire()->session; $ajax = $this->wire('config')->ajax; if($role) { $lister->resetLister(); $session->setFor($this, 'listerRole', $role); } else if(!$ajax) { if((int) $session->getFor($this, 'listerRole') > 0) { $lister->resetLister(); $session->setFor($this, 'listerRole', 0); } } else { $role = $session->getFor($this, 'listerRole'); } if(!$role && !$user->isSuperuser()) { $userAdminAll = $this->wire()->permissions->get('user-admin-all'); if($userAdminAll->id && !$user->hasPermission($userAdminAll)) { // system has user-admin-all permission, and user doesn't have it // so limit them only to the permission user-admin-[role] roles that they have assigned $roles = array(); foreach($user->getPermissions() as $permission) { if(strpos($permission->name, 'user-admin-') !== 0) continue; $roleName = str_replace('user-admin-', '', $permission->name); $rolePage = $this->wire('roles')->get($roleName); if($rolePage->id) $roles[] = $rolePage->id; } // allow them to view users that only have guest role $config = $this->wire()->config; $guestRoleID = $config->guestUserRolePageID; $guestUserID = $config->guestUserPageID; $selector .= ", id!=$guestUserID, roles=(roles.count=1, roles=$guestRoleID)"; if(count($roles)) { $selector .= ", roles=(roles=" . implode('|', $roles) . ")"; // string of | separated role IDs } } } $settings['initSelector'] .= $selector; $settings['defaultSelector'] = "name%=, roles=" . ($role ? $role : ''); $settings['delimiters'] = array('roles' => ', '); $settings['allowBookmarks'] = true; return $settings; } /** * Get the Page being edited, when applicable * * @return NullPage|Page * */ public function getPage() { $page = parent::getPage(); if($page->id && !$page->get('_rolesPrevious') && $this->wire('input')->post('roles') !== null) { $page->setQuietly('_rolesPrevious', clone $page->roles); } return $page; } /** * Return array of roles editable by current user for user $page * * @param User $page * @return array of role names indexed by role ID * */ protected function getEditableRoles(User $page) { /** @var User $user */ $user = $this->wire('user'); $superuser = $user->isSuperuser(); $editableRoles = array(); $userAdminAll = $this->wire('permissions')->get('user-admin-all'); foreach($this->wire('roles') as $role) { if($role->name == 'guest') continue; // if non-superuser editing a user, don't allow them to assign new roles with user-admin permission, // unless the user already has the role checked, OR the non-superuser has user-admin-all permission if(!$superuser && $role->hasPermission('user-admin') && !$page->hasPermission('user-admin')) { if($userAdminAll->id && $user->hasPermission($userAdminAll)) { // allow it if the non-superuser making edits has user-admin-all } else { // do not allow continue; } } $editableRoles[$role->id] = $role->name; } if(!$superuser) { if($userAdminAll->id && !$user->hasPermission($userAdminAll)) { foreach($editableRoles as $roleID => $roleName) { if(!$user->hasPermission("user-admin-$roleName")) { unset($editableRoles[$roleID]); } } } /* $numEditableRoles = 0; foreach($page->roles as $role) { if(isset($editableRoles[$role->id])) $numEditableRoles++; } if($numEditableRoles == 1 && count($editableRoles) == 2) { // if there is only one editable role here, then removal of it would // prevent this user from being able to make further edits, so we // count it as not-editable in that case foreach($page->roles as $role) { if($role->name == 'guest') continue; unset($editableRoles[$role->id]); } } */ } return $editableRoles; } /** * Edit user * * @return array|string * */ public function ___executeEdit() { /** @var User $user */ $user = $this->wire('user'); if(!$user->isSuperuser()) { // prevent showing superuser role at all $this->addHookAfter('InputfieldPage::getSelectablePages', $this, 'hookGetSelectablePages'); } $this->addHookAfter('ProcessPageEdit::buildForm', $this, 'hookPageEditBuildForm'); $out = parent::___executeEdit(); /** @var User $page Available only after executeEdit() */ $page = $this->getPage(); $this->wire('config')->js('ProcessUser', array( 'editableRoles' => array_keys($this->getEditableRoles($page)), 'notEditableAlert' => $this->_('You may not change this role'), )); return $out; } /** * Hook to InputfieldPage::getSelectablePages to target the "roles" field * * @param HookEvent $event * */ public function hookGetSelectablePages($event) { /** @var InputfieldPage $inputfield */ $inputfield = $event->object; if($inputfield->attr('name') != 'roles') return; $suRoleID = $this->wire()->config->superUserRolePageID; foreach($event->return as $role) { if($role->id == $suRoleID) $event->return->remove($role); } } /** * Hook to ProcessPageEdit::buildForm to adjust User edit form before presenting to user * * @param HookEvent $event * */ public function hookPageEditBuildForm(HookEvent $event) { $form = $event->return; /** @var InputfieldSelect $theme */ $theme = $form->getChildByName('admin_theme'); if(!$theme) return; if(!$theme->attr('value')) { $theme->attr('value', $this->wire('config')->defaultAdminTheme); } } /** * Hook before Pages::save() * * Perform a security check to make sure that a non-superuser isn't assigning superuser access to * themselves or someone else. Plus perform addition role add/remove checks. * * @param HookEvent $event * @throws WireException * */ public function hookPageSave(HookEvent $event) { $arguments = $event->arguments; $page = $arguments[0]; if(!$page instanceof User && !in_array($page->template->id, $this->wire('config')->userTemplateIDs)) { // don't handle anything other than User page saves return; } $pages = $this->wire('pages'); $user = $this->wire('user'); $superuser = $user->isSuperuser(); $suRole = $this->wire('roles')->get($this->wire('config')->superUserRolePageID); // don't allow removal of the guest role if(!$page->roles->has("name=guest")) { $page->roles->add($this->wire('roles')->get('guest')); } // check if user is editing themself if($user->id == $page->id) { // if so, we have to get a fresh copy of their account to see what it had before they changed it $copy = clone $page; // keep a copy that doesn't go through the uncache process $pages->uncache($page); // take it out of the cache $user = $pages->get($page->id); // get a fresh copy of their account from the DB (pre-modified) $pages->cache($copy); // put the modified version back into the cache $arguments[0] = $copy; // restore it to the arguments sent to $pages->save $event->arguments = $arguments; $page = $copy; // don't let superusers remove their superuser role if($superuser && !$page->roles->has($suRole)) { throw new WireException($this->_("You may not remove the superuser role from yourself")); } } if(!$superuser) { if($page->roles->has("name=superuser") || $page->roles->has($suRole)) { throw new WireException($this->_("You may not assign the superuser role")); } $this->checkSaveRoles($user, $page); $this->checkSaveUserAdminAll($user, $page); } } /** * Perform a general check for saving the roles field, making sure added/removed roles are okay * * @param User $user * @param User|Page $page * */ protected function checkSaveRoles(User $user, Page $page) { if($user->isSuperuser()) return; /** @var PageArray $rolesPrevious Set to page by the ProcessUser::getPage() method */ $rolesPrevious = $page->get('_rolesPrevious'); if(!$rolesPrevious || ((string) $rolesPrevious) === ((string) $page->roles)) return; $editableRoles = $this->getEditableRoles($page); $addedRoles = array(); $removedRoles = array(); // determine added and removed roles foreach($page->roles as $role) { if(!$rolesPrevious->has($role)) $addedRoles[$role->id] = $role; } foreach($rolesPrevious as $role) { if(!$page->roles->has($role)) $removedRoles[$role->id] = $role; } // if any added or removed roles are not consistent with editable roles, then reverse the change // this is not likely to ever occur but is here for redundancy foreach($addedRoles as $role) { if($role->name == 'guest') continue; if(!isset($editableRoles[$role->id])) { $page->roles->remove($role); $this->error("Role $role->name may not be added"); } } foreach($removedRoles as $role) { if(!isset($editableRoles[$role->id])) { $page->roles->add($role); $this->error("Role $role->name may not be removed"); } } } /** * Perform checks for when "user-admin-all" permission is installed and user does not have it * * @param User $user * @param User|Page $page * */ protected function checkSaveUserAdminAll(User $user, Page $page) { if($user->isSuperuser()) return; $userAdminAll = $this->wire('permissions')->get('user-admin-all'); if(!$userAdminAll->id || $user->hasPermission($userAdminAll)) return; // user-admin-all permission is installed and user doesn't have it // check that the role assignments are valid /** @var Pages $pages */ $pages = $this->wire('pages'); $changedUser = $page; $pages->uncache($page, array('shallow' => true)); $originalUser = $this->wire('users')->get($page->id); // get a fresh, unmodified copy if(!$originalUser->id) return; /** @var PagePermissions $pagePermissions */ $pagePermissions = $this->wire('modules')->get('PagePermissions'); $removedRoles = array(); foreach($originalUser->roles as $role) { if(!$changedUser->roles->has($role)) { // role was removed if(!$pagePermissions->userCanAssignRole($role)) { $changedUser->roles->add($role); $this->error(sprintf($this->_('You are not allowed to remove role: %s'), $role->name)); } else { $removedRoles[] = $role; } } } foreach($changedUser->roles as $role) { if(!$originalUser->roles->has($role)) { // role was added if(!$pagePermissions->userCanAssignRole($role)) { $changedUser->roles->remove($role); $this->error(sprintf($this->_('You are not allowed to add role: %s'), $role->name)); } } } if(count($removedRoles) && !$changedUser->editable()) { $this->error($this->_('You removed role(s) that that will prevent your edit access to this user. Roles have been restored.')); foreach($removedRoles as $role) $changedUser->roles->add($role); } } }