626 lines
18 KiB
PHP
626 lines
18 KiB
PHP
<?php namespace ProcessWire;
|
||
|
||
/**
|
||
* ProcessWire UserPage
|
||
*
|
||
* A type of Page used for storing an individual User
|
||
*
|
||
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
|
||
* https://processwire.com
|
||
*
|
||
* #pw-summary The $user API variable is a type of page representing the current user, and the User class is Page type used for all users.
|
||
*
|
||
* @link http://processwire.com/api/variables/user/ Offical $user API variable Documentation
|
||
*
|
||
* @property string $email Get or set email address for this user.
|
||
* @property string|Password $pass Set the user’s password.
|
||
* @property PageArray $roles Get the roles this user has. #pw-group-common #pw-group-access
|
||
* @property Language $language User language, applicable only if LanguageSupport installed. #pw-group-languages
|
||
* @property string $admin_theme Admin theme class name (when applicable).
|
||
*
|
||
* @method bool hasPagePermission($name, Page $page = null) #pw-internal
|
||
* @method bool hasTemplatePermission($name, $template) #pw-internal
|
||
*
|
||
* Additional notes regarding the $user->pass property:
|
||
* Note that when getting, this returns a hashed version of the password, so it is not typically useful to get this property.
|
||
* However, it is useful to set this property if you want to change the password. When you change a password, it is assumed
|
||
* to be the non-hashed/non-encrypted version. ProcessWire will hash it automatically when the user is saved.
|
||
*
|
||
*/
|
||
|
||
class User extends Page {
|
||
|
||
/**
|
||
* Cached value for $user->isSuperuser() checks
|
||
*
|
||
* @var null|bool
|
||
*
|
||
*/
|
||
protected $isSuperuser = null;
|
||
|
||
/**
|
||
* Create a new User page in memory.
|
||
*
|
||
* @param Template $tpl Template object this page should use.
|
||
*
|
||
*/
|
||
public function __construct(Template $tpl = null) {
|
||
if(!$tpl) $this->template = $this->wire()->templates->get('user');
|
||
$this->_parent_id = $this->wire()->config->usersPageID;
|
||
parent::__construct($tpl);
|
||
}
|
||
|
||
/**
|
||
* Wired to API
|
||
*
|
||
* #pw-internal
|
||
*
|
||
*/
|
||
public function wired() {
|
||
parent::wired();
|
||
// intentionally duplicated from __construct() in case a multi-instance environment
|
||
// and we got the wrong instance in __construct()
|
||
$template = $this->wire()->templates->get('user');
|
||
if($template !== $this->template && (!$this->template || $this->template->name === 'user')) {
|
||
$this->template = $template;
|
||
}
|
||
$this->_parent_id = $this->wire()->config->usersPageID;
|
||
}
|
||
|
||
/**
|
||
* Does this user have the given Role?
|
||
*
|
||
* #pw-group-access
|
||
*
|
||
* ~~~~~
|
||
* if($user->hasRole('editor')) {
|
||
* // user has the editor role
|
||
* }
|
||
* ~~~~~
|
||
*
|
||
* @param string|Role|int $role May be Role name, object or ID.
|
||
* @return bool
|
||
*
|
||
*/
|
||
public function hasRole($role) {
|
||
|
||
/** @var PageArray $roles */
|
||
$roles = $this->get('roles');
|
||
$has = false;
|
||
|
||
if(empty($roles)) {
|
||
// do nothing
|
||
|
||
} else if($role instanceof Page) {
|
||
$has = $roles->has($role);
|
||
|
||
} else if(ctype_digit("$role")) {
|
||
$role = (int) $role;
|
||
foreach($roles as $r) {
|
||
if(((int) $r->id) === $role) {
|
||
$has = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
} else if(is_string($role)) {
|
||
foreach($roles as $r) {
|
||
if($r->name === $role) {
|
||
$has = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
return $has;
|
||
}
|
||
|
||
/**
|
||
* Add Role to this user
|
||
*
|
||
* This is the same as `$user->roles->add($role)` except this one will also accept ID or name.
|
||
*
|
||
* ~~~~~
|
||
* // Add the "editor" role to the $user
|
||
* $user->addRole('editor');
|
||
* $user->save();
|
||
* ~~~~~
|
||
*
|
||
* #pw-group-access
|
||
*
|
||
* @param string|int|Role $role May be Role name, object, or ID.
|
||
* @return bool Returns false if role not recognized, true otherwise
|
||
*
|
||
*/
|
||
public function addRole($role) {
|
||
if(is_string($role) || is_int($role)) {
|
||
$role = $this->wire()->roles->get($role);
|
||
}
|
||
if($role instanceof Role) {
|
||
$this->get('roles')->add($role);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Remove Role from this user
|
||
*
|
||
* This is the same as `$user->roles->remove($role)` except this one will accept ID or name.
|
||
*
|
||
* ~~~~~
|
||
* // Remove the "editor" role from the $user
|
||
* $user->removeRole('editor');
|
||
* $user->save();
|
||
* ~~~~~
|
||
*
|
||
* #pw-group-access
|
||
*
|
||
* @param string|int|Role $role May be Role name, object or ID.
|
||
* @return bool false if role not recognized, true otherwise
|
||
*
|
||
*/
|
||
public function removeRole($role) {
|
||
if(is_string($role) || is_int($role)) {
|
||
$role = $this->wire()->roles->get($role);
|
||
}
|
||
if($role instanceof Role) {
|
||
$this->get('roles')->remove($role);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Does the user have the given permission?
|
||
*
|
||
* - Optionally accepts a `Page` or `Template` context for the permission.
|
||
* - This method accounts for the user's permissions across all their roles.
|
||
*
|
||
* ~~~~~
|
||
* if($user->hasPermission('page-publish')) {
|
||
* // user has the page-publish permission in one of their roles
|
||
* }
|
||
* if($user->hasPermission('page-publish', $page)) {
|
||
* // user has page-publish permission for $page
|
||
* }
|
||
* ~~~~~
|
||
*
|
||
* #pw-group-access
|
||
*
|
||
* @param string|Permission $name Permission name, object or id.
|
||
* @param Page|Template|bool|string $context Page or Template...
|
||
* - or specify boolean true to return if user has permission OR if it was added at any template
|
||
* - or specify string "templates" to return array of Template objects where user has permission
|
||
* @return bool|array
|
||
*
|
||
*/
|
||
public function hasPermission($name, $context = null) {
|
||
// This method serves as the public interface to the hasPagePermission and hasTemplatePermission methods.
|
||
$hooks = $this->wire()->hooks;
|
||
|
||
if($context === null || $context instanceof Page) {
|
||
$hook = $hooks->isHooked('hasPagePermission()');
|
||
return $hook ? $this->hasPagePermission($name, $context) : $this->___hasPagePermission($name, $context);
|
||
}
|
||
|
||
$hook = $hooks->isHooked('hasTemplatePermission()');
|
||
|
||
if($context instanceof Template) {
|
||
return $hook ? $this->hasTemplatePermission($name, $context) : $this->___hasTemplatePermission($name, $context);
|
||
}
|
||
|
||
if($context === true || $context === 'templates') {
|
||
$addedTemplates = array();
|
||
foreach($this->wire()->templates as $t) {
|
||
if(!$t->useRoles) continue;
|
||
$has = $hook ? $this->hasTemplatePermission($name, $t) : $this->___hasTemplatePermission($name, $t);
|
||
if($has) $addedTemplates[] = $t;
|
||
if($has && $context === true) break; // we only need to know if there is at least one, so break now
|
||
}
|
||
return $context === true ? count($addedTemplates) > 0 : $addedTemplates;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Does this user have named permission for the given Page?
|
||
*
|
||
* This is a basic permission check and it is recommended that you use those from the PagePermissions module instead.
|
||
* You use the PagePermissions module by calling the editable(), addable(), etc., functions on a page object.
|
||
* The PagePermissions does use this function for some of it's checking.
|
||
*
|
||
* #pw-group-access
|
||
*
|
||
* @param string|Permission
|
||
* @param Page $page Optional page to check against
|
||
* @return bool
|
||
*
|
||
*/
|
||
protected function ___hasPagePermission($name, Page $page = null) {
|
||
|
||
if($this->isSuperuser()) return true;
|
||
$permissions = $this->wire()->permissions;
|
||
|
||
// convert $name to a Permission object (if it isn't already)
|
||
if($name instanceof Page) {
|
||
$permission = $name;
|
||
} else if(ctype_digit("$name")) {
|
||
$permission = $permissions->get((int) $name);
|
||
} else if($name == 'page-rename') {
|
||
// optional permission that, if not installed, page-edit is substituted for
|
||
if($permissions->has('page-rename')) {
|
||
$permission = $permissions->get('page-rename');
|
||
} else {
|
||
$permission = $permissions->get('page-edit');
|
||
}
|
||
} else {
|
||
if($name == 'page-add' || $name == 'page-create') {
|
||
// page-add and page-create don't actually exist in the DB, so we substitute page-edit for them
|
||
// code later on will make sure they exist in the template's addRoles/createRoles
|
||
$p = 'page-edit';
|
||
} else if(!$permissions->has($name)) {
|
||
$delegated = $permissions->getDelegatedPermissions();
|
||
$p = isset($delegated[$name]) ? $delegated[$name] : $name;
|
||
} else {
|
||
$p = $name;
|
||
}
|
||
$permission = $permissions->get($p);
|
||
}
|
||
|
||
if(!$permission || !$permission->id) return false;
|
||
|
||
/** @var PageArray $userRoles */
|
||
$userRoles = $this->getUnformatted('roles');
|
||
if(empty($userRoles) || !$userRoles instanceof PageArray) return false;
|
||
$has = false;
|
||
$accessTemplate = is_null($page) ? false : $page->getAccessTemplate($permission->name);
|
||
if(is_null($accessTemplate)) return false;
|
||
|
||
foreach($userRoles as $role) {
|
||
/** @var Role $role */
|
||
|
||
if(!$role || !$role->id) continue;
|
||
$context = null;
|
||
|
||
if($page !== null) {
|
||
// @todo some of this logic has been duplicated in Role::hasPermission, so code within this if() may be partially redundant
|
||
if(!$page->id) continue;
|
||
|
||
// if page doesn't have the 'view' role, then no access
|
||
if(!$page->hasAccessRole($role, $name)) continue;
|
||
|
||
// all page- permissions except page-view and page-add require page-edit access on $page, so check against that
|
||
if(strpos($name, 'page-') === 0 && $name != 'page-view' && $name != 'page-add') {
|
||
if($accessTemplate && !in_array($role->id, $accessTemplate->editRoles)) continue;
|
||
}
|
||
|
||
// check against addRoles, createRoles if the permission requires it
|
||
if($name == 'page-add') {
|
||
if($accessTemplate && !in_array($role->id, $accessTemplate->addRoles)) continue;
|
||
} else if($name == 'page-create') {
|
||
if($accessTemplate && !in_array($role->id, $accessTemplate->createRoles)) continue;
|
||
} else {
|
||
// some other page-* permission, check against context of access template
|
||
$context = $accessTemplate ? $accessTemplate : $page;
|
||
}
|
||
}
|
||
|
||
if($role->hasPermission($permission, $context)) {
|
||
$has = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
return $has;
|
||
}
|
||
|
||
|
||
/**
|
||
* Does this user have the given permission on the given template?
|
||
*
|
||
* #pw-group-access
|
||
*
|
||
* @param string|Permission $name Permission name
|
||
* @param Template|int|string $template Template object, name or ID
|
||
* @return bool
|
||
* @throws WireException
|
||
*
|
||
*/
|
||
protected function ___hasTemplatePermission($name, $template) {
|
||
|
||
if($this->isSuperuser()) return true;
|
||
|
||
if(is_object($name)) $name = $name->name;
|
||
|
||
if($template instanceof Template) {
|
||
// fantastic then
|
||
} else if(is_string($template) || is_int($template)) {
|
||
$template = $this->wire()->templates->get($this->wire()->sanitizer->name($template));
|
||
if(!$template) return false;
|
||
} else {
|
||
return false;
|
||
}
|
||
|
||
// if the template is not defining roles, we have to say 'no' to permission
|
||
// because we don't have any page context to inherit from at this point
|
||
// if(!$template->useRoles) return false;
|
||
|
||
/** @var PageArray $userRoles */
|
||
$userRoles = $this->get('roles');
|
||
if(empty($userRoles)) return false;
|
||
$has = false;
|
||
|
||
foreach($userRoles as $role) {
|
||
/** @var Role $role */
|
||
|
||
// @todo much of this logic has been duplicated in Role::hasPermission, so code within this foreach() may be partially redundant
|
||
|
||
if(!$template->hasRole($role)) continue;
|
||
|
||
if($name == 'page-create') {
|
||
if(!in_array($role->id, $template->createRoles)) continue;
|
||
$name = 'page-edit'; // swap permission to page-edit since create managed at template and requires page-edit
|
||
}
|
||
|
||
if($name == 'page-edit' && !in_array($role->id, $template->editRoles)) {
|
||
continue;
|
||
}
|
||
|
||
if($name == 'page-add') {
|
||
if(!in_array($role->id, $template->addRoles)) continue;
|
||
$name = 'page-edit';
|
||
}
|
||
|
||
$context = null;
|
||
if($name != 'page-edit' && $name != 'page-add' && $name != 'page-create' && $name != 'page-view') {
|
||
if(strpos($name, "page-") === 0) $context = $template;
|
||
}
|
||
|
||
if($role->hasPermission($name, $context)) {
|
||
$has = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
return $has;
|
||
}
|
||
|
||
/**
|
||
* Get this user’s permissions, optionally within the context of a Page.
|
||
*
|
||
* ~~~~~
|
||
* // Get all permissions the user has across their roles
|
||
* $permissions = $user->getPermissions();
|
||
*
|
||
* // Get all permissions the user has for $page
|
||
* $permissions = $user->getPermissions($page);
|
||
* ~~~~~
|
||
*
|
||
* #pw-group-access
|
||
*
|
||
* @param Page $page Optional page to check against
|
||
* @return PageArray of Permission objects
|
||
*
|
||
*/
|
||
public function getPermissions(Page $page = null) {
|
||
// Does not currently include page-add or page-create permissions (runtime).
|
||
if($this->isSuperuser()) return $this->wire()->permissions->getIterator(); // all permissions
|
||
$userPermissions = $this->wire()->pages->newPageArray();
|
||
/** @var PageArray $userRoles */
|
||
$userRoles = $this->get('roles');
|
||
if(empty($userRoles)) return $userPermissions;
|
||
foreach($userRoles as $role) {
|
||
if($page && !$page->hasAccessRole($role)) continue;
|
||
foreach($role->permissions as $permission) {
|
||
if($page && $permission->name == 'page-edit') {
|
||
$accessTemplate = $page->getAccessTemplate('edit');
|
||
if(!$accessTemplate) continue;
|
||
if(!in_array($role->id, $accessTemplate->editRoles)) continue;
|
||
}
|
||
$userPermissions->add($permission);
|
||
}
|
||
}
|
||
return $userPermissions;
|
||
}
|
||
|
||
/**
|
||
* Does this user have the superuser role?
|
||
*
|
||
* Same as calling `$user->roles->has('name=superuser');` but potentially faster.
|
||
*
|
||
* #pw-group-access
|
||
*
|
||
* @return bool
|
||
*
|
||
*/
|
||
public function isSuperuser() {
|
||
if(is_bool($this->isSuperuser)) return $this->isSuperuser;
|
||
$config = $this->wire()->config;
|
||
if($this->id === $config->superUserPageID) {
|
||
$is = true;
|
||
} else if($this->id === $config->guestUserPageID) {
|
||
$is = false;
|
||
} else {
|
||
$superuserRoleID = (int) $config->superUserRolePageID;
|
||
/** @var PageArray $userRoles */
|
||
$userRoles = $this->getUnformatted('roles');
|
||
if(empty($userRoles)) return false; // no cache intentional
|
||
$is = false;
|
||
foreach($userRoles as $role) {
|
||
/** @var Role $role */
|
||
if(((int) $role->id) === $superuserRoleID) {
|
||
$is = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
$this->isSuperuser = $is;
|
||
return $is;
|
||
}
|
||
|
||
/**
|
||
* Is this the non-logged in guest user?
|
||
*
|
||
* #pw-group-access
|
||
*
|
||
* @return bool
|
||
*
|
||
*/
|
||
public function isGuest() {
|
||
return $this->id === $this->wire()->config->guestUserPageID;
|
||
}
|
||
|
||
/**
|
||
* Is the current $user logged in and the same as this user?
|
||
*
|
||
* When this method returns true, it means the current $user (API variable) is
|
||
* this user and that they are logged in.
|
||
*
|
||
* #pw-group-access
|
||
*
|
||
* @return bool
|
||
*
|
||
*/
|
||
public function isLoggedin() {
|
||
if($this->isGuest()) return false;
|
||
$user = $this->wire()->user;
|
||
$userId = $user ? $user->id : 0;
|
||
return $userId && "$userId" === "$this->id";
|
||
}
|
||
|
||
/**
|
||
* Set language for user (quietly)
|
||
*
|
||
* - Sets the language without tracking it as a change to the user.
|
||
* - If language support is not installed this method silently does nothing.
|
||
*
|
||
* #pw-group-languages
|
||
*
|
||
* @param Language|string|int $language Language object, name, or ID
|
||
* @return self
|
||
* @throws WireException if language support is installed and given an invalid/unknown language
|
||
* @since 3.0.186
|
||
*
|
||
*/
|
||
public function setLanguage($language) {
|
||
|
||
if(!is_object($language)) {
|
||
$languages = $this->wire()->languages;
|
||
// if multi-language support not available exit now
|
||
if(!$languages) return $this;
|
||
// convert string or int to Language object
|
||
$language = $languages->get($language);
|
||
if(!is_object($language)) $language = null;
|
||
}
|
||
|
||
if($language && ($language->className() === 'Language' || wireInstanceOf($language, 'Language'))) {
|
||
return $this->setQuietly('language', $language);
|
||
} else {
|
||
throw new WireException("Unknown language set to user $this->name");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get value
|
||
*
|
||
* @param string $key
|
||
* @return null|mixed
|
||
*
|
||
*/
|
||
public function get($key) {
|
||
$value = parent::get($key);
|
||
if(!$value && $key === 'language') {
|
||
$languages = $this->wire()->languages;
|
||
if($languages) $value = $languages->getDefault();
|
||
}
|
||
return $value;
|
||
}
|
||
|
||
/**
|
||
* Return the URL necessary to edit this user
|
||
*
|
||
* In this case we adjust the default page editor URL to ensure users are edited
|
||
* only from the Access section.
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param array|bool $options Specify boolean true to force URL to include scheme and hostname, or use $options array:
|
||
* - `http` (bool): True to force scheme and hostname in URL (default=auto detect).
|
||
* @return string URL for editing this user
|
||
*
|
||
*/
|
||
public function editUrl($options = array()) {
|
||
return str_replace('/page/edit/', '/access/users/edit/', parent::editUrl($options));
|
||
}
|
||
|
||
/**
|
||
* Set the Process module (WirePageEditor) that is editing this User
|
||
*
|
||
* We use this to detect when the User is being edited somewhere outside of /access/users/
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param WirePageEditor $editor
|
||
*
|
||
*/
|
||
public function ___setEditor(WirePageEditor $editor) {
|
||
parent::___setEditor($editor);
|
||
if(!$editor instanceof ProcessUser) $this->wire()->session->redirect($this->editUrl());
|
||
}
|
||
|
||
/**
|
||
* Return the API variable used for managing pages of this type
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @return Users
|
||
*
|
||
*/
|
||
public function getPagesManager() {
|
||
return $this->wire()->users;
|
||
}
|
||
|
||
/**
|
||
* Does user have two-factor authentication (Tfa) enabled? (and what type?)
|
||
*
|
||
* - Returns boolean false if not enabled.
|
||
* - Returns string with Tfa module name (string) if enabled.
|
||
* - When `$getInstance` argument is true, returns Tfa module instance rather than module name.
|
||
*
|
||
* The benefit of using this method is that it can identify if Tfa is enabled without fully
|
||
* initializing a Tfa module that would attach hooks, etc. So when you only need to know if
|
||
* Tfa is enabled for a user, this method is more efficient than accessing `$user->tfa_type`.
|
||
*
|
||
* When using `$getInstance` to return module instance, note that the module instance might not
|
||
* be initialized (hooks not added, etc.). To retrieve an initialized instance, you can get it
|
||
* from `$user->tfa_type` rather than calling this method.
|
||
*
|
||
* #pw-group-access
|
||
*
|
||
* @param bool $getInstance Get Tfa module instance when available? (default=false)
|
||
* @return bool|string|Tfa
|
||
* @since 3.0.162
|
||
*
|
||
*/
|
||
public function hasTfa($getInstance = false) {
|
||
return Tfa::getUserTfaType($this, $getInstance);
|
||
}
|
||
|
||
/**
|
||
* Hook called when field has changed
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param string $what
|
||
* @param mixed $old
|
||
* @param mixed $new
|
||
*
|
||
*/
|
||
public function ___changed($what, $old = null, $new = null) {
|
||
if($what === 'roles' && is_bool($this->isSuperuser)) $this->isSuperuser = null;
|
||
parent::___changed($what, $old, $new);
|
||
}
|
||
|
||
}
|