Rev 22 | Go to most recent revision | 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 2020 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 void executeLogout() #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 array getBeforeLoginVars() #pw-hooker* @method array getLoginLinks() #pw-hooker***/class ProcessLogin extends Process implements ConfigurableModule {public static function getModuleInfo() {return array('title' => 'Login','summary' => 'Login to ProcessWire','version' => 108,'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();return 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->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->templates->get($this->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($this->wire('input')->urlSegmentStr() === 'logout') {$session->redirect('../');}$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->form->processInput($input->post);} else {$this->beforeLogin();return $this->renderLoginForm();}// at this point login form has been submitted$name = $this->getLoginName();$pass = substr($this->passField->attr('value'), 0, 128);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() {$tfa = null;$tfas = $this->wire()->modules->findByPrefix('Tfa');if(!count($tfas)) return $tfa;$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() {$value = $this->nameField->attr('value');if(!strlen($value)) return false;$originalValue = $value;if(!$this->useEmailLogin() || !strpos($value, '@')) {$value = $this->sanitizer->pageName($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($this->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 = $this->users->find("include=all, $this->emailField=" . $this->sanitizer->selectorValue($value));if(!$items->count()) {// fail: no matches$this->loginFailed($value);return false;} else if($items->count() > 1) {// fail: more than one matchif($this->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($this->config->debug) $error .= ' (inactive)';$this->loginFailed($value, $error);return false;}return $user->name;}/*** 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;if($name && $pass) {$loginUser = $session->login($name, $pass);} else {$loginUser = false;}if($loginUser && $loginUser->id) {$this->loginSuccess($loginUser);$this->afterLoginRedirect('./');} else {$this->loginFailed($this->submitLoginName ? $this->submitLoginName : $name);}return false;}/*** Log the user out**/public function ___executeLogout() {if($this->logoutURL) {$url = $this->logoutURL;} else if($this->isAdmin || $this->wire('page')->template == 'admin') {$url = $this->config->urls->admin;$this->message($this->labels('logged-out'));} else {$url = "./?logout=2";}$this->session->logout();$this->session->redirect($url, false);}/*** Check that sessions can be initiated and attempt to rectify situation if not** Happens only on the admin login form.**/protected function ___beforeLogin() {/** @var Session $session */$session = $this->wire('session');$beforeLoginVars = $this->getBeforeLoginVars();$session->setFor($this, 'beforeLoginVars', $beforeLoginVars);// any remaining checks only if currently in the adminif(!$this->isAdmin) return;// if checks already completed don't run them againif($session->getFor($this, 'beforeLoginChecks')) return;if( ini_get('session.save_handler') == 'files'&& !$this->wire('modules')->isInstalled('SessionHandlerDB')&& !$this->wire('input')->get('db')) {$installSessionDB = false;$path = $this->config->paths->sessions;$error = '';if(!file_exists($path)) {$this->wire('files')->mkdir($path);clearstatcache();if(file_exists($path)) {$this->wire('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)) {$this->wire('files')->chmod($path);clearstatcache();if(is_writable($path)) {$this->wire('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) $this->wire('log')->error($error);if($this->wire('modules')->get('SessionHandlerDB')) {$this->wire('log')->error("Installed SessionHandlerDB as an alternate session handler. If you wish to uninstall this, do so after correcting the session path error.");$this->wire('session')->redirect("./?db=1"); // db param to prevent potential infinite redirect} else {$this->wire('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() {$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 = $this->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 = $this->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 = $this->modules->get('InputfieldSubmit');$this->submitField->attr('name', 'login_submit');$this->submitField->attr('value', $this->labels('login')); // Login form: submit login button$this->form = $this->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 = $this->modules->get('InputfieldHidden');$f->attr('id+name', 'login_hidpi');$f->attr('value', 0);$this->form->add($f);// detect touch device login (populated from js)$f = $this->modules->get('InputfieldHidden');$f->attr('id+name', 'login_touch');$f->attr('value', 0);$this->form->add($f);// detect touch device login (populated from js)$f = $this->modules->get('InputfieldHidden');$f->attr('id+name', 'login_width');$f->attr('value', 0);$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')) {/** @var Config $config */$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() {/** @var InputfieldButton $btn */$btn = $this->wire('modules')->get('InputfieldButton');if($this->wire('user')->hasPermission('profile-edit')) {$btn->value = $this->labels('edit-profile');$btn->href = $this->config->urls->admin . 'profile/';} else {$btn->value = $this->labels('continue');$btn->href = $this->wire('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);/** @var Session $session */$session = $this->wire('session');$session->removeFor($this, 'beforeLoginVars');$session->removeFor($this, 'beforeLoginChecks');$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 = '') {if(empty($url)) {/** @var User $user */$user = $this->wire('user');if($this->loginURL) {$url = $this->loginURL;} else if($this->isAdmin && $user->isLoggedin() && $user->hasPermission('page-edit')) {if($this->id || $this->wire('process') !== $this->className()) {$url = './';} else {$url = $this->wire('config')->urls->admin . 'page/';}} else {$url = './';}}$beforeLoginVars = $this->wire('session')->getFor($this, 'beforeLoginVars');if(!is_array($beforeLoginVars)) $beforeLoginVars = array();if(!isset($beforeLoginVars['login'])) $beforeLoginVars['login'] = 1;$url .= (strpos($url, '?') ? '&' : '?');foreach($beforeLoginVars as $name => $value) {if(strpos($url, "?$name=") !== false || strpos($url, "&$name=") !== false) continue; // skip if overriddenif(!is_int($value)) $value = $this->wire('sanitizer')->entities($value);$url .= "$name=$value&";}$url = rtrim($url, '&');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 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) {/** @var Session $session */$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;if(count($this->wire('modules')->findByPrefix('Tfa'))) {foreach($this->tfaRecRoleIDs as $roleID) {$role = $this->wire('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) {/** @var Modules $modules */$modules = $this->wire('modules');/** @var InputfieldRadios $f */$f = $modules->get('InputfieldRadios');$f->attr('name', 'allowEmail');$f->label = $this->_('Login type');$f->addOption(0, $this->_('User name'));$f->addOption(1, $this->_('Email address'));$f->addOption(2, $this->_('Either'));$f->icon = 'sign-in';$f->val((int) $this->allowEmail);$emailField = $this->fields->get($this->emailField); /** @var Field $field */if($emailField && !$emailField->hasFlag(Field::flagUnique)) {$f->notes = sprintf($this->_('To use email login, you must [enable the “unique” setting](%s) for your email field.'),$emailField->editUrl('flagUnique'));}$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;}}