artabro/wire/core/User.php
2024-08-27 11:35:37 +02:00

630 lines
18 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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 users 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)) {
if($page) {
$method = $permissions->getDelegatedMethod($name, $page);
if($method) return $page->$method(); // i.e. $page->editable()
}
$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 users 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);
}
}