Rev 1 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | Download
<?php namespace ProcessWire;/*** ProcessWire User Process** For more details about how Process modules work, please see:* /wire/core/Process.php** ProcessWire 3.x, Copyright 2020 by Ryan Cramer* https://processwire.com** @property int $maxAjaxQty**/class ProcessUser extends ProcessPageType {static public function getModuleInfo() {return array('title' => __('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 itif($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) allowedif(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) allowedif(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 permissionif(!$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 allowcontinue;}}$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 caseforeach($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 savesreturn;}$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 roleif(!$page->roles->has("name=guest")) {$page->roles->add($this->wire('roles')->get('guest'));}// check if user is editing themselfif($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 roleif($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 rolesforeach($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 redundancyforeach($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 copyif(!$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 removedif(!$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 addedif(!$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);}}}