548 lines
17 KiB
Text
548 lines
17 KiB
Text
<?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 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);
|
|
}
|
|
}
|
|
|
|
}
|
|
|