diff --git a/wire/modules/Process/ProcessLogin/ProcessLogin.module b/wire/modules/Process/ProcessLogin/ProcessLogin.module
index 2009ae8a..a1892e6b 100644
--- a/wire/modules/Process/ProcessLogin/ProcessLogin.module
+++ b/wire/modules/Process/ProcessLogin/ProcessLogin.module
@@ -12,7 +12,7 @@
* https://processwire.com
*
* @property bool $allowForgot Whether the ProcessForgotPassword module is installed.
- * @property bool $allowEmail Whether or not email login is allowed.
+ * @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.
*
@@ -28,8 +28,7 @@
* @method void loginFailed($name, $message = '') #pw-hooker
* @method void loginSuccess(User $user) #pw-hooker
* @method array getBeforeLoginVars() #pw-hooker
- *
- * @todo add option to select roles allowed to login here (when template is admin)
+ * @method array getLoginLinks() #pw-hooker
*
*
*/
@@ -40,20 +39,20 @@ class ProcessLogin extends Process implements ConfigurableModule {
return array(
'title' => 'Login',
'summary' => 'Login to ProcessWire',
- 'version' => 107,
+ 'version' => 108,
'permanent' => true,
'permission' => 'page-view',
);
}
/**
- * @var Inputfield
+ * @var InputfieldText|InputfieldEmail
*
*/
protected $nameField;
/**
- * @var Inputfield
+ * @var InputfieldText
*
*/
protected $passField;
@@ -118,6 +117,37 @@ class ProcessLogin extends Process implements ConfigurableModule {
*/
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' => '
{out}
',
+ 'login-link' => '{out}',
+ 'login-links' => '{out}
',
+ 'login-links-split' => '
',
+ 'forgot-icon' => '', // in constructor
+ 'home-icon' => '', // in constructor
+ );
+
/**
* Construct
*
@@ -126,6 +156,8 @@ class ProcessLogin extends Process implements ConfigurableModule {
$this->set('tfaRecRoleIDs', 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();
}
@@ -143,16 +175,66 @@ class ProcessLogin extends Process implements ConfigurableModule {
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 order
+ case 'continue': $label = $this->_('Continue'); break;
+ case 'edit-profile': $label = $this->_('Edit Profile'); break;
+ case 'email': $label = $this->_('Email'); break; // Email input label
+ case '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 label
+ case 'login-failed': $label = $this->_('Login failed'); break;
+ case 'login-headline': $label = $this->_x('Login', 'headline'); break; // Login form headline
+ case '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 label
+ case 'username': $label = $this->_('Username'); break; // Username input label
+ case 'username-or-email': $label = $this->_('Username or Email'); break; // Name/email input label
+ default: $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?
*
- * @return bool
+ * 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(is_bool($this->useEmailLogin)) return $this->useEmailLogin;
+ if($this->useEmailLogin !== null) return $this->useEmailLogin;
if(!$this->allowEmail) return false;
if(!$this->emailField) return false;
@@ -167,7 +249,7 @@ class ProcessLogin extends Process implements ConfigurableModule {
$template = $this->templates->get($this->config->userTemplateID);
if(!$template || !$template->hasField($field)) return false;
- return true;
+ return (int) $this->allowEmail;
}
/**
@@ -238,7 +320,7 @@ class ProcessLogin extends Process implements ConfigurableModule {
$this->afterLoginRedirect($this->loginURL);
}
if($input->get('layout')) return ''; // blank placeholder page option for admin themes
- $this->message($this->_("You are logged in."));
+ $this->message($this->labels('logged-in'));
if($this->isAdmin && $user->hasPermission('page-edit') && !$input->get('login')) {
$this->afterLoginRedirect();
}
@@ -268,7 +350,7 @@ class ProcessLogin extends Process implements ConfigurableModule {
} else if($input->get('forgot') && $this->allowForgot) {
/** @var ProcessForgotPassword $process */
- $process = $this->modules->get("ProcessForgotPassword");
+ $process = $this->modules->get('ProcessForgotPassword');
if($this->useEmailLogin()) $process->askEmail = true;
return $process->execute();
}
@@ -315,15 +397,31 @@ class ProcessLogin extends Process implements ConfigurableModule {
$value = $this->nameField->attr('value');
if(!strlen($value)) return false;
- if(!$this->useEmailLogin()) {
- return $this->sanitizer->pageName($value);
+ $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;
- $error = $this->_('Login is not supported for that email address.');
+ 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()) {
@@ -354,8 +452,8 @@ class ProcessLogin extends Process implements ConfigurableModule {
/**
* Perform login and redirect on success
*
- * @param $name
- * @param $pass
+ * @param string $name
+ * @param string $pass
* @return bool Returns false on fail, performs redirect on success
*
*/
@@ -374,7 +472,7 @@ class ProcessLogin extends Process implements ConfigurableModule {
$this->afterLoginRedirect('./');
} else {
- $this->loginFailed($name);
+ $this->loginFailed($this->submitLoginName ? $this->submitLoginName : $name);
}
return false;
@@ -389,7 +487,7 @@ class ProcessLogin extends Process implements ConfigurableModule {
$url = $this->logoutURL;
} else if($this->isAdmin || $this->wire('page')->template == 'admin') {
$url = $this->config->urls->admin;
- $this->message($this->_("You have logged out"));
+ $this->message($this->labels('logged-out'));
} else {
$url = "./?logout=2";
}
@@ -436,7 +534,6 @@ class ProcessLogin extends Process implements ConfigurableModule {
$installSessionDB = true;
$error = "Session path $path does not exist and we are unable to create it.";
}
-
}
if(!is_writable($path)) {
@@ -506,21 +603,27 @@ class ProcessLogin extends Process implements ConfigurableModule {
*
*/
protected function ___buildLoginForm() {
+
+ $useEmailLogin = $this->useEmailLogin();
+ $nameInputType = 'InputfieldText';
+ $nameInputLabel = $this->labels('username'); // Login form: username field label
- if($this->useEmailLogin()) {
- $this->nameField = $this->modules->get('InputfieldEmail');
- $this->nameField->set('label', $this->_('Email')); // Login form: email field label
- } else {
- $this->nameField = $this->modules->get('InputfieldText');
- $this->nameField->set('label', $this->_('Username')); // Login form: username field label
+ if($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->_('Password')); // Login form: password field label
+ $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');
@@ -528,7 +631,7 @@ class ProcessLogin extends Process implements ConfigurableModule {
$this->submitField = $this->modules->get('InputfieldSubmit');
$this->submitField->attr('name', 'login_submit');
- $this->submitField->attr('value', $this->_('Login')); // Login form: submit login button
+ $this->submitField->attr('value', $this->labels('login')); // Login form: submit login button
$this->form = $this->modules->get('InputfieldForm');
@@ -563,11 +666,10 @@ class ProcessLogin extends Process implements ConfigurableModule {
}
$s = 'script';
- $class = "class=ui-state-error-text";
- $jsError = $this->_('Javascript check failed: please enable Javascript to login.');
- $cookieError = $this->_('Cookie check failed: please enable cookies to login.');
- $this->form->prependMarkup .= "<$s>if(!navigator.cookieEnabled) document.write('$cookieError
');$s>";
- if($this->isAdmin) $this->form->prependMarkup .= "$jsError
";
+ $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 .= "$jsError";
return $this->form;
}
@@ -591,17 +693,13 @@ class ProcessLogin extends Process implements ConfigurableModule {
// render login form
if($this->isAdmin) $this->setCacheHeaders();
// note the space after 'Login ' is intentional to separate it from the Login button for translation purposes
- $this->headline($this->_('Login ')); // Headline for login form page
+ $this->headline($this->labels('login-headline')); // Headline for login form page
$this->passField->attr('value', '');
$out = $this->form->render();
- $links = '';
- if($this->allowForgot) {
- $links .= ""; // Forgot password link text
+ $links = $this->getLoginLinks();
+ if(count($links)) {
+ $out .= str_replace('{out}', implode($this->markup('login-links-split'), $links), $this->markup('login-links'));
}
- $home = $this->pages->get("/");
- $links .= "";
- if($links) $out .= "$links
";
-
if(!$this->wire('modules')->isInstalled('InputDetect')) {
/** @var Config $config */
$config = $this->wire('config');
@@ -612,22 +710,50 @@ class ProcessLogin extends Process implements ConfigurableModule {
return $out;
}
+ /**
+ * Get array of links to display under login form
+ *
+ * Each item in returned array must be entire `` 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');
+ $links['home'] = str_replace(array('{url}', '{out}'), array($home->url, "$icon $home->title"), $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->_('Edit Profile');
+ $btn->value = $this->labels('edit-profile');
$btn->href = $this->config->urls->admin . 'profile/';
} else {
- $btn->value = $this->_('Continue');
+ $btn->value = $this->labels('continue');
$btn->href = $this->wire('config')->urls->root;
}
return "" . $btn->render() . "
";
@@ -641,6 +767,7 @@ class ProcessLogin extends Process implements ConfigurableModule {
*/
protected function ___afterLoginRedirect($url = '') {
$url = $this->afterLoginURL($url);
+ /** @var Session $session */
$session = $this->wire('session');
$session->removeFor($this, 'beforeLoginVars');
$session->removeFor($this, 'beforeLoginChecks');
@@ -660,6 +787,7 @@ class ProcessLogin extends Process implements ConfigurableModule {
public function ___afterLoginURL($url = '') {
if(empty($url)) {
+ /** @var User $user */
$user = $this->wire('user');
if($this->loginURL) {
$url = $this->loginURL;
@@ -714,7 +842,7 @@ class ProcessLogin extends Process implements ConfigurableModule {
*
*/
protected function ___loginFailed($name, $message = '') {
- if(empty($message)) $message = "$name - " . $this->_('Login failed');
+ if(empty($message)) $message = "$name - " . $this->labels('login-failed');
$this->error($message);
}
@@ -728,7 +856,6 @@ class ProcessLogin extends Process implements ConfigurableModule {
/** @var Session $session */
$session = $this->wire('session');
- // $session->message($user->name . ' - ' . $this->_("Successful login"));
if($this->isAdmin) {
$copyVars = $session->getFor($this, 'copyVars');
@@ -779,6 +906,7 @@ class ProcessLogin extends Process implements ConfigurableModule {
$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 */