Rev 22 | Blame | Compare with Previous | Last modification | View Log | Download
<?php namespace ProcessWire;/*** ProcessWire Login Process** Provides Login capability for ProcessWire Admin** For more details about how Process modules work, please see:* /wire/core/Process.php** ProcessWire 3.x, Copyright 2023 by Ryan Cramer* https://processwire.com** @property bool $allowForgot Whether the ProcessForgotPassword module is installed.* @property bool|int $allowEmail Whether or not email login is allowed (0|false=off, 1|true=Yes, 2=Yes or name also allowed)* @property string $emailField Field name used for email login (when enabled).* @property array $tfaRecRoleIDs Role IDs where admin prompts/recommends them to enable TFA.* @property int $tfaRememberDays Allow user to remember their browser and bypass TFA for this many days (-1=no limit, 0=disabled)* @property array $tfaRememberFingerprints Means by which to fingerprint user’s browser* @property string $tfaAutoType Auto-enable type, aka module name (default='')* @property array $tfaAutoRoleIDs Auto-enable for these role IDs, or blank for all roles. Applies only if $tfaAutoType selected (default=[])** @method void beforeLogin() #pw-hooker* @method void afterLogin() #pw-hooker* @method string executeLogout() #pw-hooker* @method string executeLoggedOut() #pw-hooker* @method string afterLoginOutput() #pw-hooker* @method void afterLoginRedirect($url = '') #pw-hooker* @method string afterLoginURL($url = '') #pw-hooker* @method string renderLoginForm() #pw-hooker* @method InputfieldForm buildLoginForm() #pw-hooker* @method void login($name, $pass) #pw-hooker* @method void loginFailed($name, $message = '') #pw-hooker* @method void loginSuccess(User $user) #pw-hooker* @method void loginAttemptReady($name) #pw-hooker 3.0.223+* @method void loginAttempted($user, $name) #pw-hooker 3.0.223+* @method void loginFormProcessReady($form) #pw-hooker 3.0.223+* @method void loginFormProcessed($form, $name) #pw-hooker 3.0.223+* @method array getBeforeLoginVars() #pw-hooker* @method array getLoginLinks() #pw-hooker* @method string renderLoginLinks() #pw-hooker***/class ProcessLogin extends Process implements ConfigurableModule {public static function getModuleInfo() {return array('title' => 'Login','summary' => 'Login to ProcessWire','version' => 109,'permanent' => true,'permission' => 'page-view',);}/*** @var InputfieldText|InputfieldEmail**/protected $nameField;/*** @var InputfieldText**/protected $passField;/*** @var InputfieldSubmit**/protected $submitField;/*** @var InputfieldForm**/protected $form;/*** Requested page edit ID (no longer used, but kept in case anything else monitoring it)** @var int**/protected $id;/*** Is this login form being used for admin login?** @var bool**/protected $isAdmin = false;/*** URL to redirect to after login** @var string**/protected $loginURL = '';/*** URL to redirect to after logout** @var string**/protected $logoutURL = '';/*** Did user login with two factor authentication?** @var bool**/protected $tfaLoginSuccess = false;/*** Cached value from useEmailLogin method** @var bool|null**/protected $useEmailLogin = null;/*** Custom labels that override defaults, indexed by label name** @var array**/protected $customLabels = array();/*** Login name as submitted (after sanitize)** @var string**/protected $submitLoginName = '';/*** Configurable markup for this module** @var array**/protected $customMarkup = array('error' => '<p class="ui-state-error-text">{out}</p>','login-link' => '<a href="{url}">{out}</a>','login-links' => '<p class="pw-login-links">{out}</p>','login-links-split' => ' <br />','forgot-icon' => '', // in constructor'home-icon' => '', // in constructor);/*** Construct**/public function __construct() {$this->set('tfaRecRoleIDs', array());$this->set('tfaRememberDays', 90);$this->set('tfaRememberFingerprints', array('agentVL', 'accept', 'scheme', 'host'));$this->set('tfaAutoType', '');$this->set('tfaAutoRoleIDs', array());$this->set('allowEmail', false);$this->set('emailField', 'email');$this->customMarkup['forgot-icon'] = wireIconMarkup('question-circle', 'fw');$this->customMarkup['home-icon'] = wireIconMarkup('home', 'fw');parent::__construct();}/*** Build the login form**/public function init() {$this->id = isset($_GET['id']) ? (int) $_GET['id'] : ''; // id no longer used as anything but a toggle (on/off)$this->set('allowForgot', $this->modules->isInstalled('ProcessForgotPassword'));$this->isAdmin = $this->wire()->page->template == 'admin';$this->useEmailLogin = $this->useEmailLogin();parent::init();}/*** Get or set named label text** @param string $name Label name* @param null|string $value Specify value to replace label with custom value at runtime, otherwise omit* @return string* @since 3.0.154**/public function labels($name, $value = null) {if($value !== null) $this->customLabels[$name] = $value;if(isset($this->customLabels[$name])) return $this->customLabels[$name];switch($name) { // alpha ordercase 'continue': $label = $this->_('Continue'); break;case 'edit-profile': $label = $this->_('Edit Profile'); break;case 'email': $label = $this->_('Email'); break; // Email input labelcase 'email-not-supported': $label = $this->_('Login is not supported for that email address.'); break;case 'fail-cookie': $label = $this->_('Cookie check failed: please enable cookies to login.'); break;case 'fail-javascript': $label = $this->_('Javascript check failed: please enable Javascript to login.'); break;case 'forgot-password': $label = $this->_('Forgot your password?'); break;case 'invalid-name': $label = $this->_('Invalid login name'); break;case 'login': $label = $this->_('Login'); break; // Login submit button labelcase 'login-failed': $label = $this->_('Login failed'); break;case 'login-headline': $label = $this->_x('Login', 'headline'); break; // Login form headlinecase 'logged-in': $label = $this->_('You are logged in.'); break;case 'logged-out': $label = $this->_('You have logged out'); break;case 'password': $label = $this->_('Password'); break; // Password input labelcase 'username': $label = $this->_('Username'); break; // Username input labelcase 'username-or-email': $label = $this->_('Username or Email'); break; // Name/email input labeldefault: $label = "Unknown label name: $name";}return $label;}/*** Get or set custom markup** @param string $name* @param null|string $value* @return string* @since 3.0.154**/public function markup($name, $value = null) {if($value !== null) $this->customMarkup[$name] = $value;return isset($this->customMarkup[$name]) ? $this->customMarkup[$name] : "Unknown markup name: $name";}/*** Use login by email?** Returns false if no, int 1 of yes, int 2 if either email or name allowed** @return bool|int* @since 3.0.151**/public function useEmailLogin() {if($this->useEmailLogin !== null) return $this->useEmailLogin;if(!$this->allowEmail) return false;if(!$this->emailField) return false;/** @var Field $field */$field = $this->wire()->fields->get($this->emailField);if(!$field) return false;if(!$field->type instanceof FieldtypeEmail) return false;if(!$field->hasFlag(Field::flagUnique)) return false;/** @var Template $template */$template = $this->wire()->templates->get($this->wire()->config->userTemplateID);if(!$template || !$template->hasField($field)) return false;return (int) $this->allowEmail;}/*** Set URL to redirect to after login success** If not set, redirect will be back to the current page with a "login=1" GET variable.* However, you should only check if the user is logged in with if($user->isLoggedin()).** @param $url* @return $this* @throws WireException if given invalid URL**/public function setLoginURL($url) {$url = $this->wire()->sanitizer->url($url, array('throw' => true));$this->loginURL = $url;return $this;}/*** Set URL to redirect to after logout success** If not set, redirect will be back to the current page with a "logout=2" GET variable.** @param $url* @return $this* @throws WireException if given invalid URL**/public function setLogoutURL($url) {$url = $this->wire()->sanitizer->url($url, array('throw' => true));$this->logoutURL = $url;return $this;}/*** Set cache control headers to prevent caching** Note that PHP already does this, but if someone has overridden PHP’s default settings* then these ones will apply. This is in order to prevent a cached copy of the login form* from being used since the login form is rendered prior to login session.**/protected function setCacheHeaders() {header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");header("Cache-Control: post-check=0, pre-check=0", false);header("Pragma: no-cache");}/*** Check if login posted and attempt login, otherwise render the login form** @return string**/public function ___execute() {$session = $this->wire()->session;$input = $this->wire()->input;$user = $this->wire()->user;if($user->isLoggedin()) {if($this->loginURL && !$input->get('login')) {$this->afterLoginRedirect($this->loginURL);}if($input->get('layout')) return ''; // blank placeholder page option for admin themes$this->message($this->labels('logged-in'));if($this->isAdmin && $user->hasPermission('page-edit') && !$input->get('login')) {$this->afterLoginRedirect();}// fallback if nothing setreturn $this->afterLoginOutput();} else if($input->urlSegmentStr() === 'logout') {$session->location('../');}$tfa = $this->getTfa();if($tfa && $tfa->active()) {// two factor authenticationif($tfa->success()) {$this->tfaLoginSuccess = true;$this->loginSuccess($this->wire()->user);$this->afterLoginRedirect('./');} else {return $tfa->render();}} else if($input->get('forgot') && $this->allowForgot) {/** @var ProcessForgotPassword $process */$process = $this->modules->get('ProcessForgotPassword');if($this->useEmailLogin()) $process->askEmail = true;return $process->execute();}$this->buildLoginForm();$loginSubmit = $input->post('login_submit');if(!$loginSubmit) {$this->beforeLogin();return $this->renderLoginForm();}$this->loginFormProcessReady($this->form);$this->form->processInput($input->post);// at this point login form has been submitted$name = $this->getLoginName();$pass = substr($this->passField->attr('value'), 0, 128);$this->loginFormProcessed($this->form, $name);if(!$name || !$pass) return $this->renderLoginForm();// vars to copy from non-logged in session to logged-in session$session->setFor($this, 'copyVars', array('hidpi' => $input->post('login_hidpi') ? true : false,'touch' => $input->post('login_touch') ? true : false,'clientWidth' => (int) $input->post('login_width'),));if($tfa) $tfa->start($name, $pass);$this->login($name, $pass);return $this->renderLoginForm();}/*** Get Tfa instance or null if not applicable** @return null|Tfa* @since 3.0.160**/public function getTfa() {$tfas = $this->wire()->modules->findByPrefix('Tfa');if(!count($tfas)) return null;$tfa = new Tfa();$this->wire($tfa);$tfa->rememberDays = $this->tfaRememberDays;$tfa->rememberFingerprints = $this->tfaRememberFingerprints;$tfa->autoType = $this->tfaAutoType && $this->tfaAutoType !== '0' ? $this->tfaAutoType : '';$tfa->autoRoleIDs = $this->tfaAutoRoleIDs;return $tfa;}/*** Get login username (whether email or name used)** @return string|bool* @since 3.0.151**/protected function getLoginName() {$sanitizer = $this->wire()->sanitizer;$config = $this->wire()->config;$users = $this->wire()->users;$value = $this->nameField->attr('value');if(!strlen($value)) return false;$originalValue = $value;if(!$this->useEmailLogin() || !strpos($value, '@')) {$value = $this->sanitizer->pageNameUTF8($value);$this->submitLoginName = $value;if($originalValue !== $value && strtolower($originalValue) !== $value) {// if sanitizer changed anything about the value (other than case) do not accept it$this->loginFailed($value, $this->labels('invalid-name'));$value = false;}return $value;}// at this point we are dealing with an email login$value = strtolower($sanitizer->email($value));$this->submitLoginName = $value;if(empty($value)) return false;if(strtolower($originalValue) !== $value) {// if sanitizer changed anything about the email (not likely) do not accept it$this->loginFailed($value, $this->labels('invalid-name'));return false;}$error = $this->labels('email-not-supported');$items = $users->find("include=all, $this->emailField=" . $sanitizer->selectorValue($value));if(!$items->count()) {// fail: no matches$this->loginFailed($value);return false;} else if($items->count() > 1) {// fail: more than one matchif($config->debug) $error .= ' (not unique)';$this->loginFailed($value, $error);return false;}// success: single match$user = $items->first();if($user->status > Page::statusHidden) {// hidden, unpublished, trashif($config->debug) $error .= ' (inactive)';$this->loginFailed($value, $error);return false;}return $user->name;}/*** Determine the after login URL and page, then populate to session to pick up after login** @since 3.0.167**/protected function determineAfterLoginUrl() {$input = $this->wire()->input;$session = $this->wire()->session;// if we’ve already done this then exit earlyif($session->getFor($this, 'afterLoginPageId') !== null) return;// ProcessPageView sets a loginRequestPageID variable to its session// this is the ID of the page that access control blocked and sent user to login$requestPageId = (int) $session->getFor('ProcessPageView', 'loginRequestPageID');if(!$requestPageId || $requestPageId === $this->wire()->page->id) return;// load the requested page$requestPage = $this->wire()->pages->get($requestPageId);if(!$requestPage->id) {$session->setFor($this, 'afterLoginPageId', 0);return;}if($requestPage->process) {// admin: this is likely an admin page with a Process module$process = (string) $requestPage->process;if(!$process || $process === "ProcessLogin") return;$className = $this->wire()->modules->getModuleClass($process, true);if(!$className || !class_exists($className)) return;$url = call_user_func_array("$className::getAfterLoginUrl", array($requestPage));} else {// front-end$url = '';}if(empty($url)) {$url = $session->getFor('ProcessPageView', 'loginRequestURL');} else {// common integer GET vars in admin to identify if present and not already in $urlforeach(array('id', 'modal') as $name) {$value = $input->get($name);if($value === null || !ctype_digit("$value")) continue; // not an integerif(preg_match('/[&?]' . $name . '=/', $url)) continue; // already present$url .= (strpos($url, '?') ? '&' : '?') . "$name=" . ((int) $value);}}// set what we found to session$session->setFor($this, 'afterLoginUrl', $url);$session->setFor($this, 'afterLoginPageId', $requestPage->id);}/*** Perform login and redirect on success** @param string $name* @param string $pass* @return bool Returns false on fail, performs redirect on success**/public function ___login($name, $pass) {$session = $this->wire()->session;$input = $this->wire()->input;$this->loginAttemptReady($name);if($name && $pass) {$loginUser = $session->login($name, $pass);} else {$loginUser = false;}$this->loginAttempted($loginUser, $name);if($loginUser && $loginUser->id) {$this->loginSuccess($loginUser);$url = $input->urlSegment1 === 'navJSON' ? '../' : './';$this->afterLoginRedirect($url);} else {$this->loginFailed($this->submitLoginName ? $this->submitLoginName : $name);}return false;}/*** Log the user out** @return string**/public function ___executeLogout() {if($this->logoutURL) {$url = $this->logoutURL;} else if($this->isAdmin || $this->wire()->page->template->name === 'admin') {$url = $this->wire()->config->urls->admin . './?loggedout=1';} else {$url = "./?logout=2";}$session = $this->wire()->session;$session->logout();$session->redirect($url, false);return '';}/*** Check that sessions can be initiated and attempt to rectify situation if not** Happens only on the admin login form.**/protected function ___beforeLogin() {$session = $this->wire()->session;$beforeLoginVars = $this->getBeforeLoginVars();$session->setFor($this, 'beforeLoginVars', $beforeLoginVars);// check if Process module provides an after login URL that it wants us to use$this->determineAfterLoginUrl();// if checks already completed don't run them againif($session->getFor($this, 'beforeLoginChecks')) return;$modules = $this->wire()->modules;$config = $this->wire()->config;$input = $this->wire()->input;$files = $this->wire()->files;$log = $this->wire()->log;// any remaining checks only if currently in the adminif(!$this->isAdmin) return;if( ini_get('session.save_handler') == 'files'&& !$modules->isInstalled('SessionHandlerDB')&& !$input->get('db')) {$installSessionDB = false;$path = $config->paths->sessions;$error = '';if(!file_exists($path)) {$files->mkdir($path);clearstatcache();if(file_exists($path)) {$log->message("Created session path $path");} else {$installSessionDB = true;$error = "Session path $path does not exist and we are unable to create it.";}}if(!is_writable($path)) {$files->chmod($path);clearstatcache();if(is_writable($path)) {$log->message("Updated session path to be writable $path");} else {$installSessionDB = true;$error = "Unable to write to session path $path, and unable to fix the permissions.";}}// if we can't get file-based sessions going, switch to database sessions to ensure admin can loginif($installSessionDB) {if($error) $log->error($error);if($modules->get('SessionHandlerDB')) {$log->error("Installed SessionHandlerDB as an alternate session handler. If you wish to uninstall this, do so after correcting the session path error.");$session->redirect("./?db=1"); // db param to prevent potential infinite redirect} else {$log->error("Unable to install alternate session handler module SessionHandlerDB");$this->error("Session write error. Login may not be possible.");}}}$session->setFor($this, 'beforeLoginChecks', 1);}/*** Hook called after login** Notify admin if there are any issues that need their attention.* Happens only on the admin login form after superuser login.**/protected function ___afterLogin() {if($this->wire()->user->isSuperuser()) {/** @var SystemUpdater $systemUpdater */$systemUpdater = $this->wire()->modules->get('SystemUpdater');if($systemUpdater) {$updatesApplied = $systemUpdater->getUpdatesApplied();$checks = $systemUpdater->getChecks();$checks->setShowNotices(true);//$checks->setTestAll(true);if(count($updatesApplied)) {$checks->checkWelcome();$this->message(sprintf($this->_('Skipping after-login system checks because updates were applied (%s)'),implode(', ', $updatesApplied)),Notice::debug);} else {$checks->execute();}}}}/*** Build the login form** @return InputfieldForm**/protected function ___buildLoginForm() {$modules = $this->wire()->modules;$useEmailLogin = $this->useEmailLogin();$nameInputType = 'InputfieldText';$nameInputLabel = $this->labels('username'); // Login form: username field labelif($useEmailLogin === 1) {$nameInputType = 'InputfieldEmail';$nameInputLabel = $this->labels('email'); // Login form: email field label} else if($useEmailLogin === 2) {$nameInputLabel = $this->labels('username-or-email'); // Login form: username OR email field label}$this->nameField = $modules->get($nameInputType);$this->nameField->label = $nameInputLabel;$this->nameField->attr('id+name', 'login_name');$this->nameField->attr('class', $this->className() . 'Name');$this->nameField->addClass('InputfieldFocusFirst');$this->nameField->collapsed = Inputfield::collapsedNever;$this->passField = $modules->get('InputfieldText');$this->passField->set('label', $this->labels('password')); // Login form: password field label$this->passField->attr('id+name', 'login_pass');$this->passField->attr('type', 'password');$this->passField->attr('class', $this->className() . 'Pass');$this->passField->collapsed = Inputfield::collapsedNever;$this->submitField = $modules->get('InputfieldSubmit');$this->submitField->attr('name', 'login_submit');$this->submitField->attr('value', $this->labels('login')); // Login form: submit login button$this->form = $modules->get('InputfieldForm');// we'll retain an ID field in the GET url, if it was there (note: no longer used as anything but a toggle on/off)$this->form->attr('action', "./" . ($this->id ? "?id=$this->id" : ''));$this->form->addClass('InputfieldFormFocusFirst');$this->form->attr('id', $this->className() . 'Form');$this->form->add($this->nameField);$this->form->add($this->passField);$this->form->add($this->submitField);if($this->isAdmin) {// detect hidpi at login (populated from js)/** @var InputfieldHidden $f */$f = $modules->get('InputfieldHidden');$f->attr('id+name', 'login_hidpi');$f->attr('value', 0);$this->form->add($f);// detect touch device login (populated from js)/** @var InputfieldHidden $f */$f = $modules->get('InputfieldHidden');$f->attr('id+name', 'login_touch');$f->attr('value', 0);$this->form->add($f);// detect touch device login (populated from js)/** @var InputfieldHidden $f */$f = $modules->get('InputfieldHidden');$f->attr('id+name', 'login_width');$f->attr('value', 0);$this->form->add($f);}/** @var InputfieldHidden $f */$f = $modules->get('InputfieldHidden');$f->attr('id+name', 'login_start');$f->val(gmdate('U')); // GMT/UTC unix timestamp of when login form was rendered$this->form->add($f);$s = 'script';$jsError = str_replace('{out}', $this->labels('fail-javascript'), $this->markup('error'));$cookieError = str_replace(array('{out}', "'"), array($this->labels('fail-cookie'), '"'), $this->markup('error'));$this->form->prependMarkup .= "<$s>if(!navigator.cookieEnabled) document.write('$cookieError');</$s>";if($this->isAdmin) $this->form->prependMarkup .= "<no$s>$jsError</no$s>";return $this->form;}/*** Render the login form** @return string**/protected function ___renderLoginForm() {$loggedIn = $this->wire()->user->isLoggedin();$out = '';if($this->wire()->input->get('login') && $loggedIn) {// redirect to page after login$this->afterLoginRedirect();} else if($loggedIn) {// user is already logged in, do nothing} else {// render login formif($this->isAdmin) $this->setCacheHeaders();// note the space after 'Login ' is intentional to separate it from the Login button for translation purposes$this->headline($this->labels('login-headline')); // Headline for login form page$this->passField->attr('value', '');$out = $this->form->render();$links = $this->getLoginLinks();if(count($links)) {$out .= str_replace('{out}', implode($this->markup('login-links-split'), $links), $this->markup('login-links'));}if(!$this->wire()->modules->isInstalled('InputDetect')) {$config = $this->wire()->config;$config->scripts->prepend($config->urls('ProcessLogin') . 'what-input.min.js');}}return $out;}/*** Get array of links to display under login form** Each item in returned array must be entire `<a>` tag for link** #pw-hooker** @return array* @since 3.0.154**/protected function ___getLoginLinks() {$links = array();$markup = $this->markup('login-link');if($this->allowForgot) {$icon = $this->markup('forgot-icon');$label = $this->labels('forgot-password');$links['forgot'] = str_replace(array('{url}', '{out}'),array('./?forgot=1', "$icon $label"),$markup);}$home = $this->pages->get('/');$icon = $this->markup('home-icon');$label = $this->wire()->sanitizer->entities($home->getUnformatted('title'));$links['home'] = str_replace(array('{url}', '{out}'),array($home->url, "$icon $label"),$markup);return $links;}/*** Output that appears if there is nowhere to redirect to after login** Called only if login originated from the actual login page, OR if user does not have page-edit permission* and thus can’t browse around in the admin.** This method is not often used since it’s more common and recommended to redirect after login.** @return string**/protected function ___afterLoginOutput() {$config = $this->wire()->config;/** @var InputfieldButton $btn */$btn = $this->wire()->modules->get('InputfieldButton');if($this->wire()->user->hasPermission('profile-edit')) {$btn->value = $this->labels('edit-profile');$btn->href = $config->urls->admin . 'profile/';} else {$btn->value = $this->labels('continue');$btn->href = $config->urls->root;}return "<p>" . $btn->render() . "</p>";}/*** Redirect to admin root after login** @param string $url**/protected function ___afterLoginRedirect($url = '') {$url = $this->afterLoginURL($url);$session = $this->wire()->session;$session->removeFor($this, 'beforeLoginVars');$session->removeFor($this, 'beforeLoginChecks');$session->removeFor($this, 'afterLoginUrl');$session->removeFor($this, 'afterLoginPageId');$session->removeFor('ProcessPageView', 'loginRequestPageID');$session->removeFor('ProcessPageView', 'loginRequestURL');if(strpos($url, '/navJSON') !== false) $url = str_replace(array('/navJSON/', '/navJSON'), '/', $url);$session->redirect($url, false);}/*** Hooks can modify the redirect URL with this hook** #pw-hooker* #pw-internal** @param string $url* @return string**/public function ___afterLoginURL($url = '') {$session = $this->wire()->session;$afterLoginUrl = $session->getFor($this, 'afterLoginUrl');$id = (int) $session->getFor($this, 'afterLoginPageId');if($afterLoginUrl) {$page = $id ? $this->wire()->pages->get($id) : new NullPage();if(!$page->id || $page->viewable()) return $afterLoginUrl;}if(empty($url)) {$user = $this->wire()->user;if($this->loginURL) {$url = $this->loginURL;} else if($this->isAdmin && $user->isLoggedin() && $user->hasPermission('page-edit')) {if($this->id || ((string) $this->wire()->process) !== $this->className()) {$url = './';} else {$url = $this->wire()->config->urls->admin . 'page/';}} else {$url = './';}}$beforeLoginVars = $session->getFor($this, 'beforeLoginVars');if(!is_array($beforeLoginVars)) $beforeLoginVars = array();if(!isset($beforeLoginVars['login'])) $beforeLoginVars['login'] = 1;$a = array();foreach($beforeLoginVars as $name => $value) {if(strpos($url, "?$name=") !== false || strpos($url, "&$name=") !== false) continue; // skip if overridden$a[$name] = $value;}if(count($a)) {$url .= strpos($url, '?') !== false ? '&' : '?';$url .= http_build_query($a);}return $url;}/*** Get validated/sanitized variables in the query string for not logged-in user to retain after login** Hook this if you need to add more than 'id' but make sure anything populated* to the return value is fully validated and sanitized.** @return array Associative array of variables**/public function ___getBeforeLoginVars() {$session = $this->wire()->session;$vars = $session->getFor($this, 'beforeLoginVars');if(!is_array($vars)) $vars = array();$id = $this->wire()->input->get('id');if($id !== null) $vars['id'] = $this->wire()->sanitizer->intUnsigned($id);return $vars;}/*** Hook called right before a login is attempted** #pw-hooker** @param string $name* @since 3.0.223**/protected function ___loginAttemptReady($name) {}/*** Hook called immediately after a login was attempted** #pw-hooker** @param User|false $user This will be User object on success, or false on fail* @param string $name Attempted login name* @since 3.0.223**/protected function ___loginAttempted($user, $name) {}/*** Hook called immediately before the login form in processed** #pw-hooker** @param InputfieldForm $form* @since 3.0.223**/protected function ___loginFormProcessReady($form) {}/*** Hook called immediately after login form processed and user name/pass identified as present** #pw-hooker** @param InputfieldForm $form* @param string $name Attempted user name* @since 3.0.223**/protected function ___loginFormProcessed($form, $name) {}/*** Hook called on login fail** @param string $name* @param string $message Specify only to override default error message (since 3.0.151)**/protected function ___loginFailed($name, $message = '') {if(empty($message)) $message = "$name - " . $this->labels('login-failed');$this->error($message);}/*** Hook called on login success** @param User $user**/protected function ___loginSuccess(User $user) {$session = $this->wire()->session;if($this->isAdmin) {$copyVars = $session->getFor($this, 'copyVars');if(!is_array($copyVars)) $copyVars = array();foreach($copyVars as $key => $value) {$session->set($key, $value);}$session->remove('error');$session->removeFor($this, 'copyVars');}if(!$user->hasTfa() && count($this->tfaRecRoleIDs) && !$this->tfaLoginSuccess) {// determine if Tfa module is installed and user has role requiring Tfa$requireTfa = false;$roles = $this->wire()->roles;if(count($this->wire()->modules->findByPrefix('Tfa'))) {foreach($this->tfaRecRoleIDs as $roleID) {$role = $roles->get((int) $roleID);if($role && $user->hasRole($role)) {$requireTfa = true;break;}}}if($requireTfa) {$url = $this->wire()->config->urls('admin') . 'profile/#wrap_Inputfield_tfa_type';$session->setFor('_user', 'requireTfa', $url);}}if($this->isAdmin) $this->afterLogin();}/*** Configure module settings** @param InputfieldWrapper $inputfields**/public function getModuleConfigInputfields(InputfieldWrapper $inputfields) {$modules = $this->wire()->modules;/** @var InputfieldRadios $f */$f = $modules->get('InputfieldRadios');$emailAllow = true;$emailField = $this->wire()->fields->get($this->emailField); /** @var Field $field */$emailAttrs = array();$emailLabel = $this->_('Email address');$emailNotes = array();if($emailField && ((int) $this->allowEmail) !== 1) {if(!$emailField->hasFlag(Field::flagUnique)) {$emailAllow = false;$emailNotes[] = sprintf($this->_('You must [enable the “unique” setting](%s) for your email field'),$emailField->editUrl('flagUnique'));}$role = $this->wire()->roles->get($this->wire()->config->superUserRolePageID);if($this->wire()->users->count("roles=$role, email=''") > 0) {$emailAllow = false;$emailNotes[] = $this->_('All superusers must have an email address defined');}if(!$emailAllow) {$emailAttrs['disabled'] = 'disabled';$emailLabel .= ' - ' . $this->_('See notes below to enable');}}$f->attr('name', 'allowEmail');$f->label = $this->_('Login type');$f->addOption(0, $this->_('User name'));$f->addOption(1, $emailLabel, $emailAttrs);$f->addOption(2, $this->_('Either'), $emailAttrs);$f->icon = 'sign-in';$f->val((int) $this->allowEmail);if(count($emailNotes)) $f->notes = $this->_('To use login-by-email: ') . implode(', ', $emailNotes);$inputfields->add($f);$inputfields->add($this->getTfaConfigInputfields());}/*** Get Inputfields to configure Tfa settings** @param array $data* @return InputfieldFieldset* @since 3.0.163**/public function getTfaConfigInputfields(array $data = array()) {$defaults = array('tfaAutoType' => $this->tfaAutoType,'tfaAutoRoleIDs' => $this->tfaAutoRoleIDs,'tfaRecRoleIDs' => $this->tfaRecRoleIDs,'tfaRememberDays' => $this->tfaRememberDays,'tfaRememberFingerprints' => $this->tfaRememberFingerprints,);$data = array_merge($defaults, $data);$modules = $this->wire()->modules;$items = array();$autos = array();/** @var InputfieldFieldset $fieldset */$fieldset = $modules->get('InputfieldFieldset');$fieldset->attr('id', 'tfaConfigFieldset');$fieldset->label = $this->_('Two-factor authentication');$fieldset->icon = 'user-secret';$fieldset->appendMarkup ="<p><a target='_blank' href='https://modules.processwire.com/categories/tfa/'>" .$this->_('Tfa modules in the ProcessWire modules directory') . ' ' .wireIconMarkup('external-link') . "</a></p>";$tfaModules = $modules->findByPrefix('Tfa');if(!count($tfaModules)) {$fieldset->description = $this->_('To configure this you must first install one or more Tfa modules and then return here.');$fieldset->collapsed = Inputfield::collapsedYes;return $fieldset;}foreach($tfaModules as $name) {$items[] = "[$name](" . $modules->getModuleEditUrl($name) . ")";/** @var Tfa $tfaModule */$tfaModule = $modules->getModule($name, array('noCache' => true, 'noInit' => true));if($tfaModule && $tfaModule->autoEnableSupported()) {$autos[$name] = $modules->getModuleInfoProperty($name, 'title');}}$fieldset->description =$this->_('Found the following Tfa modules:') . ' ' . implode(', ', $items) . ' ' .$this->_('(click to configure)');if(count($autos)) {$forceLabel = $this->_('Force two-factor authentication');/** @var InputfieldRadios $f */$f = $modules->get('InputfieldRadios');$f->attr('name', 'tfaAutoType');$f->label = $forceLabel . ' - ' . $this->_x('Type', 'Module name/type');$f->description = $this->_('When a Tfa module is selected here, it will be enabled automatically (at login) for users that are not using two-factor authentication.');$f->addOption('0', $this->_('Disabled'));foreach($autos as $name => $title) {$f->addOption($name, "$title ($name)");}$f->icon = 'gavel';$f->val(!empty($data['tfaAutoType']) ? $data['tfaAutoType'] : '0');$fieldset->add($f);/** @var InputfieldCheckboxes $f */$f = $modules->get('InputfieldCheckboxes');$f->attr('name', 'tfaAutoRoleIDs');$f->label = $forceLabel . ' - ' . $this->_x('Roles', 'Roles selection');$f->description = $this->_('Check roles to force two-factor authentication for, or leave all unchecked to force for ALL roles (when/where possible).');foreach($this->wire('roles') as $role) {if($role->name == 'guest') continue;$f->addOption($role->id, $role->name);}$f->icon = 'gavel';$f->attr('value', $data['tfaAutoRoleIDs']);$f->showIf = 'tfaAutoType!=0';$f->collapsed = Inputfield::collapsedBlank;$fieldset->add($f);}/** @var InputfieldCheckboxes $f */$f = $modules->get('InputfieldCheckboxes');$f->attr('name', 'tfaRecRoleIDs');$f->icon = 'gears';$f->label = $this->_('Strongly suggest two-factor authentication for these roles');$f->description =$this->_('After logging in to the admin, ProcessWire will prompt users in the roles you select here to use two-factor authentication for their accounts.');foreach($this->wire('roles') as $role) {if($role->name == 'guest') continue;$f->addOption($role->id, $role->name);}$f->attr('value', $data['tfaRecRoleIDs']);$f->collapsed = Inputfield::collapsedBlank;$fieldset->add($f);/** @var InputfieldInteger $f */$f = $modules->get('InputfieldInteger');$f->attr('name', 'tfaRememberDays');$f->label = $this->_('Allow users the option to skip code entry when their browser/location is remembered?');$f->description =$this->_('This presents users with a “Remember this computer?” option on the code entry screen at login.') . ' ' .$this->_('Enter the number of days that a user’s browser/location can be remembered for, or 0 to disable.');$f->attr('value', (int) $data['tfaRememberDays']);$f->icon = 'unlock-alt';$fieldset->add($f);$fingerprints = array('agent' => $this->_('User agent (browser, platform, and versions of each)'),'agentVL' => $this->_('Non-versioned user agent (browser and platform, but no versions—less likely to change often)'),'accept' => $this->_('Accept header (content types user’s browser accepts)'),'scheme' => $this->_('Current request scheme whether HTTP or HTTPS'),'host' => $this->_('Server hostname (value of $config->httpHost)'),'ip' => $this->_('User’s IP address (REMOTE_ADDR)'),'fwip' => $this->_('User’s forwarded or client IP address (HTTP_X_FORWARDED_FOR or HTTP_CLIENT_IP)'),);/** @var InputfieldCheckboxes $f */$f = $modules->get('InputfieldCheckboxes');$f->attr('name', 'tfaRememberFingerprints');$f->label = $this->_('Do not allow user to skip code entry when any of these properties change');$f->description =$this->_('Changes to password, name, email, or a random cookie in the user’s browser, will always require code entry at login.') . ' ' .$this->_('In addition, changes to any checked items below will also require code entry at login.') . ' ' .$this->_('These properties form a fingerprint of the user’s browser beyond the random cookie that we set.');$f->notes = $this->_('This setting only applies when the option to remember browser/location is enabled.');foreach($fingerprints as $name => $label) {$f->addOption($name, $label);}$f->showIf = 'tfaRememberDays!=0';$f->attr('value', $data['tfaRememberFingerprints']);$f->icon = 'lock';$fieldset->add($f);return $fieldset;}}