diff --git a/config/vufind/Alma.ini b/config/vufind/Alma.ini index 7c5b26fe8be91a1ed4d24f06430a54141b910418..c9a7d749854090a1523df76fc9316b4161099210 100644 --- a/config/vufind/Alma.ini +++ b/config/vufind/Alma.ini @@ -9,6 +9,8 @@ http_timeout = 30 ; vufind Use VuFind's user database for authentication -- patrons are retrieved ; from Alma without a password (default) ; password Use password authentication with Alma internal users +; email Username needs to be a valid email address for the user (an +; authentication link is sent by email) ;loginMethod = vufind [Holds] diff --git a/config/vufind/Demo.ini b/config/vufind/Demo.ini index a435f8d21a7356b37d22ceeae5453afe8b1cc376..71de1079c0aa7b4c2cc63427ad1507e4eb790d0a 100644 --- a/config/vufind/Demo.ini +++ b/config/vufind/Demo.ini @@ -10,6 +10,11 @@ storageRetrievalRequests = true ; Whether to support ILL requests ILLRequests = true +; Patron login method to use. The following options are available: +; password Normal username+password (the default) +; email Username is an email address +;loginMethod = email + ; Holds and holds-logic-related configuration options. [Holds] ; Max. no. of items displayed in the holdings tab. A paginator is used when there are diff --git a/config/vufind/config.ini b/config/vufind/config.ini index cd89e934d09fcf73529425e166ead27f974cf557..a8e4840c487e54f23de14d04ceeda3d5bdcca6b7 100644 --- a/config/vufind/config.ini +++ b/config/vufind/config.ini @@ -342,8 +342,16 @@ set_home_library = true ; You can use an LDAP directory, the local ILS (or multiple ILSes through ; the MultiILS option), the VuFind database (Database), a hard-coded list of ; access passwords (PasswordAccess), AlmaDatabase (combination -; of VuFind database and Alma account), Shibboleth, SIP2, CAS, Facebook or some -; combination of these (via the MultiAuth or ChoiceAuth options). +; of VuFind database and Alma account), Shibboleth, SIP2, CAS, Facebook, Email or +; some combination of these (via the MultiAuth or ChoiceAuth options). +; +; The Email method is special; it is intended to be used through ChoiceAuth in +; combination with Database authentication (or any other method that reliably stores +; the user's email address) to make it possible to log in by receiving an +; authentication link at the email address stored in VuFind's database. Email is +; also supported as the primary authentication mechanism for some ILS drivers (e.g. +; Alma). In these cases, ChoiceAuth is not needed, and ILS should be configured as +; the Authentication method; see the ILS driver's configuration for possible options. [Authentication] ;method = LDAP ;method = ILS @@ -357,6 +365,7 @@ method = Database ;method = MultiILS ;method = Facebook ;method = PasswordAccess +;method = Email ; This setting only applies when method is set to ILS. It determines which ; field of the ILS driver's patronLogin() return array is used as the username diff --git a/languages/en.ini b/languages/en.ini index fc56a294e6f7d4a9c10f9a81a1a7e9136d7b4a0c..92f69cd1b97a243be4d1661545c5270ee3ad1730 100644 --- a/languages/en.ini +++ b/languages/en.ini @@ -72,6 +72,7 @@ authentication_error_blank = "Login information cannot be blank." authentication_error_creation_blocked = "You do not have permission to create an account." authentication_error_denied = "Credentials do not match! Access denied." authentication_error_email_not_verified_html = "Your email address has not been verified yet. Please check your spam filter for the verification message. If necessary, we can <a href="%%url%%">Resend the Verification Email</a>." +authentication_error_in_progress = "Authentication request is already being processed. Please try again later if you need to start over." authentication_error_invalid = "Invalid login -- please try again." authentication_error_loggedout = "You have logged out." authentication_error_technical = "We cannot log you in at this time. Please try again later." @@ -333,6 +334,11 @@ Email this Search = "Email this Search" email_change_pending_html = "You have a pending email change to %%pending%%. Please click the link in the verification email sent to this address to complete the change. If necessary, we can <a href="%%url%%">Resend the Verification Email</a>." email_failure = "Error - Message Cannot Be Sent" email_link = "Link" +email_login_desc = "Please use the following link to log in. If you did not initiate login, you may safely ignore this message. Please note that the link is only valid for a limited time and only in the browser you entered the email address with." +email_login_link = "Link to login: <%%url%%>" +email_login_link_sent = "We have sent a login link to your email address. It may take a few moments for the link to arrive. If you don't receive the link shortly, please check also your spam filter." +email_login_requested = "Login has been requested with your email address at %%title%%." +email_login_subject = "Login to %%title%%" email_maximum_recipients_note = "At most %%max%% recipients are allowed." email_multiple_recipients_note = "You may specify multiple recipients separated by commas." email_selected = "Email Selected" diff --git a/languages/fi.ini b/languages/fi.ini index 6a5bc88948105dffb6f611ab7964e580af4364e7..6c7afcc2c3ba2a81ae38a92cb379e9338c5f226b 100644 --- a/languages/fi.ini +++ b/languages/fi.ini @@ -69,7 +69,9 @@ Audio = "Ääni" authentication_error_admin = "Sisäänkirjautuminen epäonnistui. Ota yhteyttä järjestelmän ylläpitäjään." authentication_error_blank = "Käyttäjätunnus/salasana ei voi olla tyhjä." authentication_error_creation_blocked = "Sinulla ei ole oikeutta luoda käyttäjätiliä." -authentication_error_denied = "Käyttäjätunnus/salasana ei täsmää! Ei pääsyä järjestelmään." +authentication_error_denied = "Käyttäjätiedot eivät täsmää. Ei pääsyä järjestelmään." +authentication_error_email_not_verified_html = "Sähköpostiosoitettasi ei ole vielä vahvistettu. Tarkista myös roskapostikansio siltä varalta, että varmistusviesti on mennyt sinne. Tarvittaessa voit <a href="%%url%%">pyytää uuden vahvistusviestin</a>." +authentication_error_in_progress = "Kirjautuminen on jo käynnissä. Yritä myöhemmin uudelleen, jos haluat aloittaa alusta." authentication_error_invalid = "Käyttäjätunnus/salasana ei täsmää. Yritä uudestaan." authentication_error_loggedout = "Olet kirjautunut ulos." authentication_error_technical = "Sisäänkirjautuminen epäonnistui. Yritä uudestaan hetken kuluttua." @@ -330,6 +332,11 @@ Email this = "Lähetä sähköpostilla" Email this Search = "Lähetä haku sähköpostilla" email_failure = "Virhe - viestiä ei voitu lähettää" email_link = "Linkki" +email_login_desc = "Käytä seuraavaa linkkiä kirjautuaksesi sisään. Jos et ole kirjautumassa sisään, voit huoletta jättää tämän viestin huomiotta. Huomaa, että linkki on voimassa rajoitetun ajan ja toimii vain selaimessa, jossa annoit sähköpostiosoitteesi." +email_login_link = "Linkki kirjautumiseen: <%%url%%>" +email_login_link_sent = "Lähetimme kirjautumislinkin sähköpostiisi. Linkin saapuminen voi kestää hetken. Jos linkkiä ei ala kuulua, kannattaa tarkistaa sähköpostin roskapostikansio." +email_login_requested = "Sähköpostiosoitteellasi on tehty kirjautumispyyntö palvelussa %%title%%." +email_login_subject = "Kirjaudu palveluun %%title%%" email_maximum_recipients_note = "Korkeintaan %%max%% vastaanottajaa sallittu." email_multiple_recipients_note = "Voit luetella useita vastaanottajia pilkuilla eroteltuina." email_selected = "Lähetä valitut sähköpostilla" diff --git a/languages/sv.ini b/languages/sv.ini index 7c20dcd635011b0c2781ea29e871c6a7a2684a60..885236d1a8efa01069c9cca87a08fc355db07e59 100644 --- a/languages/sv.ini +++ b/languages/sv.ini @@ -70,6 +70,7 @@ authentication_error_admin = "Inloggningen misslyckades. Kontakta systemadminist authentication_error_blank = "Användarnamn/lösenord kan inte vara tomt." authentication_error_creation_blocked = "Du har inte tillstånd att skapa ett konto." authentication_error_denied = "Användarnamn/lösenord stämmer inte! Ingen tillgång till systemet." +authentication_error_email_not_verified_html = "Din e-postadress har inte ännu verifierats. Kontrollera din skräppostmap för verifieringsmeddelandet. Du kan också begära <a href="%%url%%">ett nytt verifieringsmeddelande</a>." authentication_error_invalid = "Användarnamn/lösenord stämmer inte. Försök igen." authentication_error_loggedout = "Du har loggat ut." authentication_error_technical = "Inloggningen misslyckades. Försök igen efter en stund." @@ -325,6 +326,11 @@ Email this = "Skicka per e-post" Email this Search = "Skicka sökningen per e-post" email_failure = "Fel - meddelandet kunde inte skickas" email_link = "Länk" +email_login_desc = "Använd följande länk för att logga in. Om du inte begärde inloggning, kan du ignorera det här meddelandet. Observera att länken är giltig en begränsad tid och endast i webbläsaren du angav e-postadressen med." +email_login_link = "Länk till inloggning: <%%url%%>" +email_login_link_sent = "Vi skickade en inloggningslänk till din e-postadress. Det kan ta någon tid tills meddelanden kommer fram. Om du inte får länken inom kort, kontrollera även skräppostmappen." +email_login_requested = "Inloggning har begärts med din e-postadress i %%title%%." +email_login_subject = "Logga in till %%title%%" email_maximum_recipients_note = "Som mest %%max%% mottagare tillåtna." email_multiple_recipients_note = "Du kan ange flera mottagare separerade med kommatecken." email_selected = "E-posta valda" diff --git a/module/VuFind/config/module.config.php b/module/VuFind/config/module.config.php index b413c471867c5d49913691b117fd21598cc1beb2..5de84f136d395a35d52ea01e5a8d4645b7cfbf49 100644 --- a/module/VuFind/config/module.config.php +++ b/module/VuFind/config/module.config.php @@ -326,6 +326,7 @@ $config = [ 'factories' => [ 'ProxyManager\Configuration' => 'VuFind\Service\ProxyConfigFactory', 'VuFind\AjaxHandler\PluginManager' => 'VuFind\ServiceManager\AbstractPluginManagerFactory', + 'VuFind\Auth\EmailAuthenticator' => 'VuFind\Auth\EmailAuthenticatorFactory', 'VuFind\Auth\ILSAuthenticator' => 'VuFind\Auth\ILSAuthenticatorFactory', 'VuFind\Auth\Manager' => 'VuFind\Auth\ManagerFactory', 'VuFind\Auth\PluginManager' => 'VuFind\ServiceManager\AbstractPluginManagerFactory', diff --git a/module/VuFind/src/VuFind/Auth/Email.php b/module/VuFind/src/VuFind/Auth/Email.php new file mode 100644 index 0000000000000000000000000000000000000000..3d5725feb6ec10a7f92f54a6cf0440ab6b79313d --- /dev/null +++ b/module/VuFind/src/VuFind/Auth/Email.php @@ -0,0 +1,159 @@ +<?php +/** + * Email authentication module. + * + * PHP version 7 + * + * Copyright (C) The National Library of Finland 2019. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package Authentication + * @author Ere Maijala <ere.maijala@helsinki.fi> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:authentication_handlers Wiki + */ +namespace VuFind\Auth; + +use VuFind\Exception\Auth as AuthException; + +/** + * Email authentication module. + * + * @category VuFind + * @package Authentication + * @author Ere Maijala <ere.maijala@helsinki.fi> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:authentication_handlers Wiki + */ +class Email extends AbstractBase +{ + /** + * Email Authenticator + * + * @var EmailAuthenticator + */ + protected $emailAuthenticator; + + /** + * Constructor + * + * @param EmailAuthenticator $emailAuth Email authenticator + */ + public function __construct(EmailAuthenticator $emailAuth) + { + $this->emailAuthenticator = $emailAuth; + } + + /** + * Attempt to authenticate the current user. Throws exception if login fails. + * + * @param \Zend\Http\PhpEnvironment\Request $request Request object containing + * account credentials. + * + * @throws AuthException + * @return \VuFind\Db\Row\User Object representing logged-in user. + */ + public function authenticate($request) + { + // This is a dual-mode method: + // First, try to find a user account with the provided email address and send + // a login link. + // Second, log the user in with the hash from the login link. + + $email = trim($request->getPost()->get('username')); + $hash = $request->getQuery('hash'); + if (!$email && !$hash) { + throw new AuthException('authentication_error_blank'); + } + + if (!$hash) { + // Validate the credentials: + $user = $this->getUserTable()->getByEmail($email, false); + if ($user) { + $loginData = [ + 'vufind_id' => $user['id'] + ]; + $this->emailAuthenticator + ->sendAuthenticationLink($user['email'], $loginData); + } + // Don't reveal the result + throw new \VuFind\Exception\AuthInProgress('email_login_link_sent'); + } + + $loginData = $this->emailAuthenticator->authenticate($hash); + if (isset($loginData['vufind_id'])) { + return $this->getUserTable()->getById($loginData['vufind_id']); + } else { + return $this->processUser($loginData); + } + + // If we got this far, we have a problem: + throw new AuthException('authentication_error_invalid'); + } + + /** + * Whether this authentication method needs CSRF checking for the request. + * + * @param \Zend\Http\PhpEnvironment\Request $request Request object. + * + * @return bool + */ + public function needsCsrfCheck($request) + { + // Disable CSRF if we get a hash in the request + return $request->getQuery('hash') ? false : true; + } + + /** + * Update the database using login user details, then return the User object. + * + * @param array $info User details returned by the login initiator like ILS. + * + * @throws AuthException + * @return \VuFind\Db\Row\User Processed User object. + */ + protected function processUser($info) + { + // Check to see if we already have an account for this user: + $userTable = $this->getUserTable(); + if (!empty($info['id'])) { + $user = $userTable->getByCatalogId($info['id']); + if (empty($user)) { + $user = $userTable->getByUsername($info['email']); + $user->saveCatalogId($info['id']); + } + } else { + $user = $userTable->getByUsername($info['email']); + } + + // No need to store a password in VuFind's main password field: + $user->password = ''; + + // Update user information based on received data: + $fields = ['firstname', 'lastname', 'email', 'major', 'college']; + foreach ($fields as $field) { + $user->$field = $info[$field] ?? ' '; + } + + // Update the user in the database, then return it to the caller: + $user->saveCredentials( + $info['cat_username'] ?? ' ', + $info['cat_password'] ?? ' ' + ); + + return $user; + } +} diff --git a/module/VuFind/src/VuFind/Auth/EmailAuthenticator.php b/module/VuFind/src/VuFind/Auth/EmailAuthenticator.php new file mode 100644 index 0000000000000000000000000000000000000000..5775a59d72cac37d151977c21b622e06482efb70 --- /dev/null +++ b/module/VuFind/src/VuFind/Auth/EmailAuthenticator.php @@ -0,0 +1,237 @@ +<?php +/** + * Class for managing email-based authentication. + * + * PHP version 7 + * + * Copyright (C) The National Library of Finland 2019. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package Authentication + * @author Ere Maijala <ere.maijala@helsinki.fi> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:authentication_handlers Wiki + */ +namespace VuFind\Auth; + +use VuFind\Exception\Auth as AuthException; + +/** + * Class for managing email-based authentication. + * + * This class provides functionality for authentication based on a known-valid email + * address. + * + * @category VuFind + * @package Authentication + * @author Ere Maijala <ere.maijala@helsinki.fi> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:authentication_handlers Wiki + */ +class EmailAuthenticator implements \VuFind\I18n\Translator\TranslatorAwareInterface +{ + use \VuFind\I18n\Translator\TranslatorAwareTrait; + + /** + * Session Manager + * + * @var \Zend\Session\SessionManager + */ + protected $sessionManager = null; + + /** + * CSRF Validator + * + * @var \VuFind\Validator\Csrf $csrf CSRF validator + */ + protected $csrf = null; + + /** + * Mailer + * + * @var \VuFind\Mailer\Mailer + */ + protected $mailer = null; + + /** + * View Renderer + * + * @var \Zend\View\Renderer\RendererInterface + */ + protected $viewRenderer = null; + + /** + * Request + * + * @var \Zend\Stdlib\RequestInterface + */ + protected $request; + + /** + * Configuration + * + * @var \Zend\Config\Config + */ + protected $config; + + /** + * How long a login request is considered to be valid (seconds) + * + * @var int + */ + protected $loginRequestValidTime = 600; + + /** + * Constructor + * + * @param \Zend\Session\SessionManager $session Session Manager + * @param \VuFind\Validator\Csrf $csrf CSRF Validator + * @param \VuFind\Mailer\Mailer $mailer Mailer + * @param \Zend\View\Renderer\RendererInterface $viewRenderer View Renderer + * @param \Zend\Stdlib\RequestInterface $request Request + * @param \Zend\Config\Config $config Configuration + */ + public function __construct(\Zend\Session\SessionManager $session, + \VuFind\Validator\Csrf $csrf, \VuFind\Mailer\Mailer $mailer, + \Zend\View\Renderer\RendererInterface $viewRenderer, + \Zend\Stdlib\RequestInterface $request, + \Zend\Config\Config $config + ) { + $this->sessionManager = $session; + $this->csrf = $csrf; + $this->mailer = $mailer; + $this->viewRenderer = $viewRenderer; + $this->request = $request; + $this->config = $config; + } + + /** + * Send an email authentication link to the specified email address. + * + * Stores the required information in the session. + * + * @param string $email Email address to send the link to + * @param array $data Information from the authentication request (such as + * user details) + * @param array $urlParams Default parameters for the generated URL + * @param string $linkRoute The route to use as the base url for the login link + * + * @return void + */ + public function sendAuthenticationLink($email, $data, + $urlParams, $linkRoute = 'myresearch-home' + ) { + $sessionContainer = $this->getSessionContainer(); + + // Make sure we've waited long enough + $recoveryInterval = isset($this->config->Authentication->recover_interval) + ? $this->config->Authentication->recover_interval + : 60; + if (null !== $sessionContainer->timestamp + && time() - $sessionContainer->timestamp < $recoveryInterval + ) { + throw new AuthException('authentication_error_in_progress'); + } + + $this->csrf->trimTokenList(5); + $linkData = [ + 'timestamp' => time(), + 'data' => $data, + 'email' => $email + ]; + $hash = $this->csrf->getHash(true); + + if (!isset($sessionContainer->requests)) { + $sessionContainer->requests = []; + } + $sessionContainer->requests[$hash] = $linkData; + + $serverHelper = $this->viewRenderer->plugin('serverurl'); + $urlHelper = $this->viewRenderer->plugin('url'); + $urlParams['hash'] = $hash; + $viewParams = $linkData; + $viewParams['url'] = $serverHelper( + $urlHelper($linkRoute, [], ['query' => $urlParams]) + ); + $viewParams['title'] = $this->config->Site->title; + + $message = $this->viewRenderer->render( + 'Email/login-link.phtml', + $viewParams + ); + $from = !empty($this->config->Mail->user_email_in_from) + ? $email + : ($this->config->Mail->default_from ?? $this->config->Site->email); + $subject = $this->translator->translate('email_login_subject'); + $subject = str_replace('%%title%%', $viewParams['title'], $subject); + + $this->mailer->send($email, $from, $subject, $message); + } + + /** + * Authenticate using a hash + * + * @param string $hash Hash + * + * @return array + * @throws AuthException + */ + public function authenticate($hash) + { + $sessionContainer = $this->getSessionContainer(); + + if (!isset($sessionContainer->requests[$hash])) { + throw new AuthException('authentication_error_denied'); + } + $linkData = $sessionContainer->requests[$hash]; + unset($sessionContainer->requests[$hash]); + if (time() - $linkData['timestamp'] > $this->loginRequestValidTime) { + throw new AuthException('authentication_error_denied'); + } + + return $linkData['data']; + } + + /** + * Check if the given request is a valid login request + * + * @param \Zend\Http\PhpEnvironment\Request $request Request object. + * + * @return bool + */ + public function isValidLoginRequest(\Zend\Http\PhpEnvironment\Request $request) + { + $hash = $request->getPost()->get( + 'hash', + $request->getQuery()->get('hash', '') + ); + if ($hash) { + $sessionContainer = $this->getSessionContainer(); + return isset($sessionContainer->requests[$hash]); + } + return false; + } + + /** + * Get the session container + * + * @return \Zend\Session\Container + */ + protected function getSessionContainer() + { + return new \Zend\Session\Container('EmailAuth', $this->sessionManager); + } +} diff --git a/module/VuFind/src/VuFind/Auth/EmailAuthenticatorFactory.php b/module/VuFind/src/VuFind/Auth/EmailAuthenticatorFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..150b8c7d701430e6999dcfbf5183a92ad034b847 --- /dev/null +++ b/module/VuFind/src/VuFind/Auth/EmailAuthenticatorFactory.php @@ -0,0 +1,73 @@ +<?php +/** + * Factory for email authenticator module. + * + * PHP version 7 + * + * Copyright (C) The National Library of Finland 2019. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package Authentication + * @author Ere Maijala <ere.maijala@helsinki.fi> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +namespace VuFind\Auth; + +use Interop\Container\ContainerInterface; + +/** + * Factory for email authenticator module. + * + * @category VuFind + * @package Authentication + * @author Ere Maijala <ere.maijala@helsinki.fi> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +class EmailAuthenticatorFactory + implements \Zend\ServiceManager\Factory\FactoryInterface +{ + /** + * Create an object + * + * @param ContainerInterface $container Service manager + * @param string $requestedName Service being created + * @param null|array $options Extra options (optional) + * + * @return object + * + * @throws ServiceNotFoundException if unable to resolve the service. + * @throws ServiceNotCreatedException if an exception is raised when + * creating a service. + * @throws ContainerException if any other error occurs + */ + public function __invoke(ContainerInterface $container, $requestedName, + array $options = null + ) { + if (!empty($options)) { + throw new \Exception('Unexpected options sent to factory.'); + } + return new $requestedName( + $container->get(\Zend\Session\SessionManager::class), + $container->get(\VuFind\Validator\Csrf::class), + $container->get(\VuFind\Mailer\Mailer::class), + $container->get('ViewRenderer'), + $container->get('Request'), + $container->get(\VuFind\Config\PluginManager::class)->get('config') + ); + } +} diff --git a/module/VuFind/src/VuFind/Auth/EmailFactory.php b/module/VuFind/src/VuFind/Auth/EmailFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..60dc9055212f634d000e3c17ce35f19be50975a1 --- /dev/null +++ b/module/VuFind/src/VuFind/Auth/EmailFactory.php @@ -0,0 +1,67 @@ +<?php +/** + * Factory for Email authentication module. + * + * PHP version 7 + * + * Copyright (C) The National Library of Finland 2019. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package Authentication + * @author Ere Maijala <ere.maijala@helsinki.fi> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +namespace VuFind\Auth; + +use Interop\Container\ContainerInterface; + +/** + * Factory for Email authentication module. + * + * @category VuFind + * @package Authentication + * @author Ere Maijala <ere.maijala@helsinki.fi> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +class EmailFactory implements \Zend\ServiceManager\Factory\FactoryInterface +{ + /** + * Create an object + * + * @param ContainerInterface $container Service manager + * @param string $requestedName Service being created + * @param null|array $options Extra options (optional) + * + * @return object + * + * @throws ServiceNotFoundException if unable to resolve the service. + * @throws ServiceNotCreatedException if an exception is raised when + * creating a service. + * @throws ContainerException if any other error occurs + */ + public function __invoke(ContainerInterface $container, $requestedName, + array $options = null + ) { + if (!empty($options)) { + throw new \Exception('Unexpected options sent to factory.'); + } + return new $requestedName( + $container->get(EmailAuthenticator::class) + ); + } +} diff --git a/module/VuFind/src/VuFind/Auth/ILS.php b/module/VuFind/src/VuFind/Auth/ILS.php index f7d8e691c2fb30c83b3e0775d36e6da661c950be..8923d7aa042daf07b759867bed9fa8c911df256f 100644 --- a/module/VuFind/src/VuFind/Auth/ILS.php +++ b/module/VuFind/src/VuFind/Auth/ILS.php @@ -58,17 +58,27 @@ class ILS extends AbstractBase protected $catalog = null; /** - * Set the ILS connection for this object. + * Email Authenticator + * + * @var EmailAuthenticator + */ + protected $emailAuthenticator; + + /** + * Constructor * * @param \VuFind\ILS\Connection $connection ILS connection to set * @param \VuFind\ILS\Authenticator $authenticator ILS authenticator + * @param EmailAuthenticator $emailAuth Email authenticator */ public function __construct( \VuFind\ILS\Connection $connection, - \VuFind\Auth\ILSAuthenticator $authenticator + \VuFind\Auth\ILSAuthenticator $authenticator, + EmailAuthenticator $emailAuth = null ) { $this->setCatalog($connection); $this->authenticator = $authenticator; + $this->emailAuthenticator = $emailAuth; } /** @@ -107,27 +117,9 @@ class ILS extends AbstractBase { $username = trim($request->getPost()->get('username')); $password = trim($request->getPost()->get('password')); - if ($username == '' || $password == '') { - throw new AuthException('authentication_error_blank'); - } - - // Connect to catalog: - try { - $patron = $this->getCatalog()->patronLogin($username, $password); - } catch (AuthException $e) { - // Pass Auth exceptions through - throw $e; - } catch (\Exception $e) { - throw new AuthException('authentication_error_technical'); - } - - // Did the patron successfully log in? - if ($patron) { - return $this->processILSUser($patron); - } + $loginMethod = $this->getILSLoginMethod(); - // If we got this far, we have a problem: - throw new AuthException('authentication_error_invalid'); + return $this->handleLogin($username, $password, $loginMethod); } /** @@ -208,6 +200,88 @@ class ILS extends AbstractBase return $user; } + /** + * What login method does the ILS use (password, email, vufind) + * + * @param string $target Login target (MultiILS only) + * + * @return string + */ + public function getILSLoginMethod($target = '') + { + $config = $this->getCatalog()->checkFunction( + 'patronLogin', ['patron' => ['cat_username' => "$target.login"]] + ); + return $config['loginMethod'] ?? 'password'; + } + + /** + * Returns any authentication method this request should be delegated to. + * + * @param \Zend\Http\PhpEnvironment\Request $request Request object. + * + * @return string|bool + */ + public function getDelegateAuthMethod(\Zend\Http\PhpEnvironment\Request $request) + { + return (null !== $this->emailAuthenticator + && $this->emailAuthenticator->isValidLoginRequest($request)) + ? 'Email' : false; + } + + /** + * Handle the actual login with the ILS. + * + * @param string $username User name + * @param string $password Password + * @param string $loginMethod Login method + * + * @throws AuthException + * @return \VuFind\Db\Row\User Processed User object. + */ + protected function handleLogin($username, $password, $loginMethod) + { + if ($username == '' || ('password' === $loginMethod && $password == '')) { + throw new AuthException('authentication_error_blank'); + } + + // Connect to catalog: + try { + $patron = $this->getCatalog()->patronLogin($username, $password); + } catch (AuthException $e) { + // Pass Auth exceptions through + throw $e; + } catch (\Exception $e) { + throw new AuthException('authentication_error_technical'); + } + + // Did the patron successfully log in? + if ('email' === $loginMethod) { + if (null === $this->emailAuthenticator) { + throw new \Exception('Email authenticator not set'); + } + if ($patron) { + $class = get_class($this); + if ($p = strrpos($class, '\\')) { + $class = substr($class, $p + 1); + } + $this->emailAuthenticator->sendAuthenticationLink( + $patron['email'], + $patron, + ['auth_method' => $class] + ); + } + // Don't reveal the result + throw new \VuFind\Exception\AuthInProgress('email_login_link_sent'); + } + if ($patron) { + return $this->processILSUser($patron); + } + + // If we got this far, we have a problem: + throw new AuthException('authentication_error_invalid'); + } + /** * Update the database using details from the ILS, then return the User object. * diff --git a/module/VuFind/src/VuFind/Auth/ILSAuthenticator.php b/module/VuFind/src/VuFind/Auth/ILSAuthenticator.php index 60473282fd58789a1065214ebc5cbd761f341815..0a1f0232dcf0123eed8ca930003a524be6850ca0 100644 --- a/module/VuFind/src/VuFind/Auth/ILSAuthenticator.php +++ b/module/VuFind/src/VuFind/Auth/ILSAuthenticator.php @@ -54,6 +54,13 @@ class ILSAuthenticator */ protected $catalog; + /** + * Email authenticator + * + * @var EmailAuthenticator + */ + protected $emailAuthenticator; + /** * Cache for ILS account information (keyed by username) * @@ -64,13 +71,16 @@ class ILSAuthenticator /** * Constructor * - * @param Manager $auth Auth manager - * @param ILSConnection $catalog ILS connection + * @param Manager $auth Auth manager + * @param ILSConnection $catalog ILS connection + * @param EmailAuthenticator $emailAuth Email authenticator */ - public function __construct(Manager $auth, ILSConnection $catalog) - { + public function __construct(Manager $auth, ILSConnection $catalog, + EmailAuthenticator $emailAuth = null + ) { $this->auth = $auth; $this->catalog = $catalog; + $this->emailAuthenticator = $emailAuth; } /** @@ -146,15 +156,77 @@ class ILSAuthenticator { $result = $this->catalog->patronLogin($username, $password); if ($result) { - $user = $this->auth->isLoggedIn(); - if ($user) { - $user->saveCredentials($username, $password); - $this->auth->updateSession($user); - // cache for future use - $this->ilsAccount[$username] = $result; - } + $this->updateUser($username, $password, $result); return $result; } return false; } + + /** + * Send email authentication link + * + * @param string $email Email address + * @param string $route Route for the login link + * + * @return void + */ + public function sendEmailLoginLink($email, $route) + { + if (null === $this->emailAuthenticator) { + throw new \Exception('Email authenticator not set'); + } + + $patron = $this->catalog->patronLogin($email, ''); + if ($patron) { + $this->emailAuthenticator->sendAuthenticationLink( + $patron['email'], + $patron, + ['auth_method' => 'ILS'], + $route + ); + } + } + + /** + * Process email login + * + * @param string $hash Login hash + * + * @return array|bool + * @throws ILSException + */ + public function processEmailLoginHash($hash) + { + if (null === $this->emailAuthenticator) { + throw new \Exception('Email authenticator not set'); + } + + try { + $patron = $this->emailAuthenticator->authenticate($hash); + } catch (\Vufind\Exception\Auth $e) { + return false; + } + $this->updateUser($patron['cat_username'], '', $patron); + return $patron; + } + + /** + * Update current user account with the patron information + * + * @param string $catUsername Catalog username + * @param string $catPassword Catalog password + * @param array $patron Patron + * + * @return void + */ + protected function updateUser($catUsername, $catPassword, $patron) + { + $user = $this->auth->isLoggedIn(); + if ($user) { + $user->saveCredentials($catUsername, $catPassword); + $this->auth->updateSession($user); + // cache for future use + $this->ilsAccount[$catUsername] = $patron; + } + } } diff --git a/module/VuFind/src/VuFind/Auth/ILSAuthenticatorFactory.php b/module/VuFind/src/VuFind/Auth/ILSAuthenticatorFactory.php index 7b7e64cd48ec37c5bf7fc731fa6c9a2683370eee..92cbaddd7f45e379a2cd9e8e33e99e969c9f3290 100644 --- a/module/VuFind/src/VuFind/Auth/ILSAuthenticatorFactory.php +++ b/module/VuFind/src/VuFind/Auth/ILSAuthenticatorFactory.php @@ -70,7 +70,8 @@ class ILSAuthenticatorFactory implements FactoryInterface // Generate wrapped object: $auth = $container->get(\VuFind\Auth\Manager::class); $catalog = $container->get(\VuFind\ILS\Connection::class); - $wrapped = new $requestedName($auth, $catalog); + $emailAuth = $container->get(\VuFind\Auth\EmailAuthenticator::class); + $wrapped = new $requestedName($auth, $catalog, $emailAuth); // Indicate that initialization is complete to avoid reinitialization: $proxy->setProxyInitializer(null); diff --git a/module/VuFind/src/VuFind/Auth/ILSFactory.php b/module/VuFind/src/VuFind/Auth/ILSFactory.php index 2c6972dc742decd2ada7e9997c67a01f8b488ecf..2d1d59469f576ae9ee73bcd1f8ca6b4fedaf25d0 100644 --- a/module/VuFind/src/VuFind/Auth/ILSFactory.php +++ b/module/VuFind/src/VuFind/Auth/ILSFactory.php @@ -62,7 +62,8 @@ class IlsFactory implements \Zend\ServiceManager\Factory\FactoryInterface } return new $requestedName( $container->get(\VuFind\ILS\Connection::class), - $container->get(ILSAuthenticator::class) + $container->get(ILSAuthenticator::class), + $container->get(EmailAuthenticator::class) ); } } diff --git a/module/VuFind/src/VuFind/Auth/Manager.php b/module/VuFind/src/VuFind/Auth/Manager.php index ae0119c9c4c77092cd80f7043808a3b303ff64eb..2c8a21f8c47bf1f4011707c8564749d9d811438d 100644 --- a/module/VuFind/src/VuFind/Auth/Manager.php +++ b/module/VuFind/src/VuFind/Auth/Manager.php @@ -596,6 +596,8 @@ class Manager implements \ZfcRbac\Identity\IdentityProviderInterface * account credentials. * * @throws AuthException + * @throws \VuFind\Exception\PasswordSecurity + * @throws \VuFind\Exception\AuthInProgress * @return UserRow Object representing logged-in user. */ public function login($request) @@ -702,6 +704,22 @@ class Manager implements \ZfcRbac\Identity\IdentityProviderInterface return $this->getAuth()->validateCredentials($request); } + /** + * What login method does the ILS use (password, email, vufind) + * + * @param string $target Login target (MultiILS only) + * + * @return array|false + */ + public function getILSLoginMethod($target = '') + { + $auth = $this->getAuth(); + if (is_callable([$auth, 'getILSLoginMethod'])) { + return $auth->getILSLoginMethod($target); + } + return false; + } + /** * Update common user attributes on login * diff --git a/module/VuFind/src/VuFind/Auth/MultiILS.php b/module/VuFind/src/VuFind/Auth/MultiILS.php index e4cd1822bfc4937051f5062311ccdbbec27d02ca..73bc43dfa14b05d542c7cdce8d27f431c8e9b9de 100644 --- a/module/VuFind/src/VuFind/Auth/MultiILS.php +++ b/module/VuFind/src/VuFind/Auth/MultiILS.php @@ -57,35 +57,17 @@ class MultiILS extends ILS */ public function authenticate($request) { - $target = trim($request->getPost()->get('target')); $username = trim($request->getPost()->get('username')); $password = trim($request->getPost()->get('password')); - if ($username == '' || $password == '') { - throw new AuthException('authentication_error_blank'); - } + $target = trim($request->getPost()->get('target')); + $loginMethod = $this->getILSLoginMethod($target); // We should have target either separately or already embedded into username if ($target) { $username = "$target.$username"; } - // Connect to catalog: - try { - $patron = $this->getCatalog()->patronLogin($username, $password); - } catch (AuthException $e) { - // Pass Auth exceptions through - throw $e; - } catch (\Exception $e) { - throw new AuthException('authentication_error_technical'); - } - - // Did the patron successfully log in? - if ($patron) { - return $this->processILSUser($patron); - } - - // If we got this far, we have a problem: - throw new AuthException('authentication_error_invalid'); + return $this->handleLogin($username, $password, $loginMethod); } /** diff --git a/module/VuFind/src/VuFind/Auth/PluginManager.php b/module/VuFind/src/VuFind/Auth/PluginManager.php index 7daaff81ac0a350b69b2819b742a88399d68deff..4d29dc5346ed2711f566a25e836ea8c3c807b292 100644 --- a/module/VuFind/src/VuFind/Auth/PluginManager.php +++ b/module/VuFind/src/VuFind/Auth/PluginManager.php @@ -50,6 +50,7 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager 'cas' => CAS::class, 'choiceauth' => ChoiceAuth::class, 'database' => Database::class, + 'email' => Email::class, 'facebook' => Facebook::class, 'ils' => ILS::class, 'ldap' => LDAP::class, @@ -72,6 +73,7 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager CAS::class => InvokableFactory::class, ChoiceAuth::class => ChoiceAuthFactory::class, Database::class => InvokableFactory::class, + Email::class => EmailFactory::class, Facebook::class => FacebookFactory::class, ILS::class => ILSFactory::class, LDAP::class => InvokableFactory::class, diff --git a/module/VuFind/src/VuFind/Controller/AbstractBase.php b/module/VuFind/src/VuFind/Controller/AbstractBase.php index 90e9961f4f173c2174b259b063776552f9b62379..454af22f3ede9514e85029a96f150c66e6dbce87 100644 --- a/module/VuFind/src/VuFind/Controller/AbstractBase.php +++ b/module/VuFind/src/VuFind/Controller/AbstractBase.php @@ -28,6 +28,7 @@ */ namespace VuFind\Controller; +use VuFind\Exception\Auth as AuthException; use VuFind\Exception\ILS as ILSException; use Zend\Mvc\Controller\AbstractActionController; use Zend\Mvc\MvcEvent; @@ -354,15 +355,33 @@ class AbstractBase extends AbstractActionController $username = "$target.$username"; } try { - $patron = $ilsAuth->newCatalogLogin($username, $password); - - // If login failed, store a warning message: - if (!$patron) { - $this->flashMessenger()->addErrorMessage('Invalid Patron Login'); + if ('email' === $this->getILSLoginMethod($target)) { + $routeMatch = $this->getEvent()->getRouteMatch(); + $routeName = $routeMatch ? $routeMatch->getMatchedRouteName() + : 'myresearch-profile'; + $ilsAuth->sendEmailLoginLink($username, $routeName); + $this->flashMessenger() + ->addSuccessMessage('email_login_link_sent'); + } else { + $patron = $ilsAuth->newCatalogLogin($username, $password); + + // If login failed, store a warning message: + if (!$patron) { + $this->flashMessenger() + ->addErrorMessage('Invalid Patron Login'); + } } } catch (ILSException $e) { $this->flashMessenger()->addErrorMessage('ils_connection_failed'); } + } elseif ('ILS' === $this->params()->fromQuery('auth_method', false) + && ($hash = $this->params()->fromQuery('hash', false)) + ) { + try { + $patron = $ilsAuth->processEmailLoginHash($hash); + } catch (AuthException $e) { + $this->flashMessenger()->addErrorMessage($e->getMessage()); + } } else { try { // If no credentials were provided, try the stored values: @@ -716,4 +735,44 @@ class AbstractBase extends AbstractActionController ) === 'lightbox' || 'layout/lightbox' == $this->layout()->getTemplate(); } + + /** + * What login method does the ILS use (password, email, vufind) + * + * @param string $target Login target (MultiILS only) + * + * @return string + */ + protected function getILSLoginMethod($target = '') + { + $config = $this->getILS()->checkFunction( + 'patronLogin', ['patron' => ['cat_username' => "$target.login"]] + ); + return $config['loginMethod'] ?? 'password'; + } + + /** + * Get settings required for displaying the catalog login form + * + * @return array + */ + protected function getILSLoginSettings() + { + $targets = null; + $defaultTarget = null; + $loginMethod = null; + $loginMethods = []; + // Connect to the ILS and check if multiple target support is available: + $catalog = $this->getILS(); + if ($catalog->checkCapability('getLoginDrivers')) { + $targets = $catalog->getLoginDrivers(); + $defaultTarget = $catalog->getDefaultLoginDriver(); + foreach ($targets as $t) { + $loginMethods[$t] = $this->getILSLoginMethod($t); + } + } else { + $loginMethod = $this->getILSLoginMethod(); + } + return compact('targets', 'defaultTarget', 'loginMethod', 'loginMethods'); + } } diff --git a/module/VuFind/src/VuFind/Controller/LibraryCardsController.php b/module/VuFind/src/VuFind/Controller/LibraryCardsController.php index dbc47d66067bc6bc0d92b5aa016f9a8066f914e7..4d249b208a59fb6cf830870cbda0b62b75e3bab0 100644 --- a/module/VuFind/src/VuFind/Controller/LibraryCardsController.php +++ b/module/VuFind/src/VuFind/Controller/LibraryCardsController.php @@ -5,7 +5,7 @@ * PHP version 7 * * Copyright (C) Villanova University 2010. - * Copyright (C) The National Library of Finland 2015. + * Copyright (C) The National Library of Finland 2015-2019. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -99,6 +99,13 @@ class LibraryCardsController extends AbstractBase return $this->forceLogin(); } + // Process email authentication: + if ($this->params()->fromQuery('auth_method') === 'Email' + && ($hash = $this->params()->fromQuery('hash')) + ) { + return $this->processEmailLink($user, $hash); + } + // Process form submission: if ($this->formWasSubmitted('submit')) { if ($redirect = $this->processEditLibraryCard($user)) { @@ -111,17 +118,13 @@ class LibraryCardsController extends AbstractBase $target = null; $username = $card->cat_username; - $targets = null; - $defaultTarget = null; - // Connect to the ILS and check if multiple target support is available: - $catalog = $this->getILS(); - if ($catalog->checkCapability('getLoginDrivers')) { - $targets = $catalog->getLoginDrivers(); - $defaultTarget = $catalog->getDefaultLoginDriver(); - if (strstr($username, '.')) { - list($target, $username) = explode('.', $username, 2); - } + + $loginSettings = $this->getILSLoginSettings(); + // Split target and username if multiple login targets are available: + if ($loginSettings['targets'] && strstr($username, '.')) { + list($target, $username) = explode('.', $username, 2); } + $cardName = $this->params()->fromPost('card_name', $card->card_name); $username = $this->params()->fromPost('username', $username); $target = $this->params()->fromPost('target', $target); @@ -131,10 +134,12 @@ class LibraryCardsController extends AbstractBase [ 'card' => $card, 'cardName' => $cardName, - 'target' => $target ? $target : $defaultTarget, + 'target' => $target ?: $loginSettings['defaultTarget'], 'username' => $username, - 'targets' => $targets, - 'defaultTarget' => $defaultTarget + 'targets' => $loginSettings['targets'], + 'defaultTarget' => $loginSettings['defaultTarget'], + 'loginMethod' => $loginSettings['loginMethod'], + 'loginMethods' => $loginSettings['loginMethods'], ] ); } @@ -266,13 +271,32 @@ class LibraryCardsController extends AbstractBase $card = $user->getLibraryCard($id == 'NEW' ? null : $id); if ($card->cat_username !== $username || trim($password)) { // Connect to the ILS and check that the credentials are correct: + $loginMethod = $this->getILSLoginMethod($target); $catalog = $this->getILS(); $patron = $catalog->patronLogin($username, $password); - if (!$patron) { + if ('password' === $loginMethod && !$patron) { $this->flashMessenger() ->addMessage('authentication_error_invalid', 'error'); return false; } + if ('email' === $loginMethod) { + if ($patron) { + $info = $patron; + $info['cardID'] = $id; + $info['cardName'] = $cardName; + $emailAuthenticator = $this->serviceLocator + ->get(\VuFind\Auth\EmailAuthenticator::class); + $emailAuthenticator->sendAuthenticationLink( + $info['email'], + $info, + ['auth_method' => 'Email'], + 'editLibraryCard' + ); + } + // Don't reveal the result + $this->flashMessenger()->addSuccessMessage('email_login_link_sent'); + return $this->redirect()->toRoute('librarycards-home'); + } } try { @@ -286,4 +310,33 @@ class LibraryCardsController extends AbstractBase return $this->redirect()->toRoute('librarycards-home'); } + + /** + * Process library card addition via an email link + * + * @param User $user User object + * @param string $hash Hash + * + * @return \Zend\Http\Response Response object + */ + protected function processEmailLink($user, $hash) + { + $emailAuthenticator = $this->serviceLocator + ->get(\VuFind\Auth\EmailAuthenticator::class); + try { + $info = $emailAuthenticator->authenticate($hash); + $user->saveLibraryCard( + 'NEW' === $info['cardID'] ? null : $info['cardID'], + $info['cardName'], + $info['cat_username'], + ' ' + ); + } catch (\VuFind\Exception\Auth $e) { + $this->flashMessenger()->addErrorMessage($e->getMessage()); + } catch (\VuFind\Exception\LibraryCard $e) { + $this->flashMessenger()->addErrorMessage($e->getMessage()); + } + + return $this->redirect()->toRoute('librarycards-home'); + } } diff --git a/module/VuFind/src/VuFind/Controller/MyResearchController.php b/module/VuFind/src/VuFind/Controller/MyResearchController.php index 19d2e2952f4284a1cf98c44e63cfe6c18df752e8..c00f2f9b43b99dc896bf1c1e7b58c4719427daf0 100644 --- a/module/VuFind/src/VuFind/Controller/MyResearchController.php +++ b/module/VuFind/src/VuFind/Controller/MyResearchController.php @@ -29,6 +29,7 @@ namespace VuFind\Controller; use VuFind\Exception\Auth as AuthException; use VuFind\Exception\AuthEmailNotVerified as AuthEmailNotVerifiedException; +use VuFind\Exception\AuthInProgress as AuthInProgressException; use VuFind\Exception\Forbidden as ForbiddenException; use VuFind\Exception\ILS as ILSException; use VuFind\Exception\ListPermission as ListPermissionException; @@ -92,6 +93,10 @@ class MyResearchController extends AbstractBase protected function processAuthenticationException(AuthException $e) { $msg = $e->getMessage(); + if ($e instanceof AuthInProgressException) { + $this->flashMessenger()->addSuccessMessage($msg); + return; + } if ($e instanceof AuthEmailNotVerifiedException) { $this->sendFirstVerificationEmail($e->user); if ($msg == 'authentication_error_email_not_verified_html') { @@ -548,13 +553,8 @@ class MyResearchController extends AbstractBase */ public function catalogloginAction() { - // Connect to the ILS and check if multiple target support is available: - $targets = null; - $catalog = $this->getILS(); - if ($catalog->checkCapability('getLoginDrivers')) { - $targets = $catalog->getLoginDrivers(); - } - return $this->createViewModel(['targets' => $targets]); + $loginSettings = $this->getILSLoginSettings(); + return $this->createViewModel($loginSettings); } /** diff --git a/module/VuFind/src/VuFind/Exception/AuthInProgress.php b/module/VuFind/src/VuFind/Exception/AuthInProgress.php new file mode 100644 index 0000000000000000000000000000000000000000..31d6da3e6533a9779e4804069900db56958adad1 --- /dev/null +++ b/module/VuFind/src/VuFind/Exception/AuthInProgress.php @@ -0,0 +1,41 @@ +<?php +/** + * Authentication in Progress Exception + * + * PHP version 7 + * + * Copyright (C) The National Library of Finland 2019. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package Exceptions + * @author Ere Maijala <ere.maijala@helsinki.fi> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +namespace VuFind\Exception; + +/** + * Authentication in Progress Exception + * + * @category VuFind + * @package Exceptions + * @author Ere Maijala <ere.maijala@helsinki.fi> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +class AuthInProgress extends Auth +{ +} diff --git a/module/VuFind/src/VuFind/ILS/Connection.php b/module/VuFind/src/VuFind/ILS/Connection.php index 702baa849a89999871462695effcf07d3064832e..f52db679dd4ba42e50dabb37bde4cd5b7436427f 100644 --- a/module/VuFind/src/VuFind/ILS/Connection.php +++ b/module/VuFind/src/VuFind/ILS/Connection.php @@ -668,6 +668,24 @@ class Connection implements TranslatorAwareInterface, LoggerAwareInterface return false; } + /** + * Check Patron login + * + * A support method for checkFunction(). This is responsible for checking + * the driver configuration to determine if the system supports patron login. + * It is currently assumed that all drivers do. + * + * @param array $functionConfig The patronLogin configuration values + * @param array $params An array of function-specific params (or null) + * + * @return mixed On success, an associative array with specific function keys + * and values for login; on failure, false. + */ + protected function checkMethodpatronLogin($functionConfig, $params) + { + return $functionConfig; + } + /** * Get proper help text from the function config * diff --git a/module/VuFind/src/VuFind/ILS/Driver/Alma.php b/module/VuFind/src/VuFind/ILS/Driver/Alma.php index 9d9aaa4b37bf10b04f484e52991bf3fb4357880a..d9bc3db47a23610b20520e9dfb913b8c26ae40d6 100644 --- a/module/VuFind/src/VuFind/ILS/Driver/Alma.php +++ b/module/VuFind/src/VuFind/ILS/Driver/Alma.php @@ -630,15 +630,52 @@ class Alma extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterface * * This is responsible for authenticating a patron against the catalog. * - * @param string $barcode The patrons barcode. + * @param string $username The patrons barcode or other username. * @param string $password The patrons password. * * @return string[]|NULL */ - public function patronLogin($barcode, $password) + public function patronLogin($username, $password) { $loginMethod = $this->config['Catalog']['loginMethod'] ?? 'vufind'; - if ('password' === $loginMethod) { + + $patron = []; + $patronId = $username; + if ('email' === $loginMethod) { + // Create parameters for API call + $getParams = [ + 'q' => 'email~' . $username + ]; + + // Try to find the user in Alma + $response = $this->makeRequest( + '/users/', + $getParams + ); + + foreach (($response->user ?? []) as $user) { + if ((string)$user->status !== 'ACTIVE') { + continue; + } + if ($patron) { + // More than one match, cannot log in by email + $this->debug( + "Email $username matches more than one user, cannot login" + ); + return null; + } + $patron = [ + 'id' => (string)$user->primary_id, + 'cat_username' => trim($username), + 'email' => trim($username) + ]; + } + if (!$patron) { + return null; + } + // Use primary id in further queries + $patronId = $patron['id']; + } elseif ('password' === $loginMethod) { // Create parameters for API call $getParams = [ 'user_id_type' => 'all_unique', @@ -648,7 +685,7 @@ class Alma extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterface // Try to authenticate the user with Alma list($response, $status) = $this->makeRequest( - '/users/' . urlencode($barcode), + '/users/' . urlencode($username), $getParams, [], 'POST', @@ -662,27 +699,28 @@ class Alma extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterface } } - if ('password' === $loginMethod || 'vufind' === $loginMethod) { - // Create parameters for API call - $getParams = [ - 'user_id_type' => 'all_unique', - 'view' => 'brief', - 'expand' => 'none' - ]; + // Create parameters for API call + $getParams = [ + 'user_id_type' => 'all_unique', + 'view' => 'full', + 'expand' => 'none' + ]; - // Check for patron in Alma - $response = $this->makeRequest( - '/users/' . urlencode($barcode), - $getParams - ); + // Check for patron in Alma + $response = $this->makeRequest( + '/users/' . urlencode($patronId), + $getParams + ); - if ($response !== null) { - return [ - 'id' => (string)$response->primary_id, - 'cat_username' => trim($barcode), - 'cat_password' => trim($password) - ]; - } + if ($response !== null) { + // We may already have some information, so just fill the gaps + $patron['id'] = (string)$response->primary_id; + $patron['cat_username'] = trim($username); + $patron['cat_password'] = trim($password); + $patron['firstname'] = (string)$response->first_name ?? ''; + $patron['lastname'] = (string)$response->last_name ?? ''; + $patron['email'] = $this->getPreferredEmail($response); + return $patron; } return null; @@ -746,6 +784,7 @@ class Alma extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterface ? (string)$contact->phones[0]->phone->phone_number : null; } + $profile['email'] = $this->getPreferredEmail($xml); } // Cache the user group code @@ -1256,6 +1295,11 @@ class Alma extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterface */ public function getConfig($function, $params = null) { + if ($function == 'patronLogin') { + return [ + 'loginMethod' => $this->config['Catalog']['loginMethod'] ?? 'vufind' + ]; + } if (isset($this->config[$function])) { $functionConfig = $this->config[$function]; @@ -1640,6 +1684,30 @@ class Alma extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterface return $results; } + /** + * Get the preferred email address for the user (or first one if no preferred one + * is found) + * + * @param SimpleXMLElement $user User data + * + * @return string|null + */ + protected function getPreferredEmail($user) + { + if (!empty($user->contact_info->emails->email)) { + foreach ($user->contact_info->emails->email as $email) { + if ('true' === (string)$email['preferred']) { + return isset($email->email_address) + ? (string)$email->email_address : null; + } + } + $email = $user->contact_info->emails->email[0]; + return isset($email->email_address) + ? (string)$email->email_address : null; + } + return null; + } + // @codingStandardsIgnoreStart /** diff --git a/module/VuFind/src/VuFind/ILS/Driver/Demo.php b/module/VuFind/src/VuFind/ILS/Driver/Demo.php index ebe56c7d48052fd23551071f0ab00a765cace68a..25502891d02b92f998337a61b3bd2d3e1393207d 100644 --- a/module/VuFind/src/VuFind/ILS/Driver/Demo.php +++ b/module/VuFind/src/VuFind/ILS/Driver/Demo.php @@ -567,7 +567,7 @@ class Demo extends AbstractBase // when testing multiple accounts. $selectedPatron = empty($patron) ? (current(array_keys($this->session)) ?: 'default') - : $patron; + : md5($patron); // SessionContainer not defined yet? Build it now: if (!isset($this->session[$selectedPatron])) { @@ -773,33 +773,42 @@ class Demo extends AbstractBase * * This is responsible for authenticating a patron against the catalog. * - * @param string $barcode The patron barcode + * @param string $username The patron username * @param string $password The patron password * * @throws ILSException * @return mixed Associative array of patron info on successful login, * null on unsuccessful login. */ - public function patronLogin($barcode, $password) + public function patronLogin($username, $password) { $this->checkIntermittentFailure(); + + $user = [ + 'id' => trim($username), + 'firstname' => 'Lib', + 'lastname' => 'Rarian', + 'cat_username' => trim($username), + 'cat_password' => trim($password), + 'email' => 'Lib.Rarian@library.not', + 'major' => null, + 'college' => null + ]; + + $loginMethod = $this->config['Catalog']['loginMethod'] ?? 'password'; + if ('email' === $loginMethod) { + $user['email'] = $username; + $user['cat_password'] = ''; + return $user; + } + if (isset($this->config['Users'])) { - if (!isset($this->config['Users'][$barcode]) - || $password !== $this->config['Users'][$barcode] + if (!isset($this->config['Users'][$username]) + || $password !== $this->config['Users'][$username] ) { return null; } } - $user = []; - - $user['id'] = trim($barcode); - $user['firstname'] = trim("Lib"); - $user['lastname'] = trim("Rarian"); - $user['cat_username'] = trim($barcode); - $user['cat_password'] = trim($password); - $user['email'] = trim("Lib.Rarian@library.not"); - $user['major'] = null; - $user['college'] = null; return $user; } @@ -2361,6 +2370,12 @@ class Demo extends AbstractBase 'default_sort' => 'due asc' ]; } + if ($function == 'patronLogin') { + return [ + 'loginMethod' + => $this->config['Catalog']['loginMethod'] ?? 'password' + ]; + } return []; } diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/ManagerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/ManagerTest.php index 2cfca678a4c3374f92bf60afe51307dabb7a801b..ec66ea344099f020db77edbbc0cd6f08431175cb 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/ManagerTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/ManagerTest.php @@ -584,6 +584,8 @@ class ManagerTest extends \VuFindTest\Unit\TestCase $mockDb = $this->getMockBuilder(\VuFind\Auth\Database::class) ->disableOriginalConstructor() ->getMock(); + $mockDb->expects($this->any())->method('needsCsrfCheck') + ->will($this->returnValue(true)); $mockMulti = $this->getMockBuilder(\VuFind\Auth\MultiILS::class) ->disableOriginalConstructor() ->getMock(); @@ -622,6 +624,9 @@ class ManagerTest extends \VuFindTest\Unit\TestCase $post = new \Zend\Stdlib\Parameters(); $mock->expects($this->any())->method('getPost') ->will($this->returnValue($post)); + $get = new \Zend\Stdlib\Parameters(); + $mock->expects($this->any())->method('getQuery') + ->will($this->returnValue($get)); return $mock; } } diff --git a/themes/bootstrap3/js/common.js b/themes/bootstrap3/js/common.js index 5207d304455cb4afebb9089045083444025da12b..7af5a6f659cc28ea74b2222bcc142e5fcfeba98b 100644 --- a/themes/bootstrap3/js/common.js +++ b/themes/bootstrap3/js/common.js @@ -1,5 +1,5 @@ /*global grecaptcha, isPhoneNumberValid */ -/*exported VuFind, htmlEncode, deparam, moreFacets, lessFacets, getUrlRoot, phoneNumberFormHandler, recaptchaOnLoad, resetCaptcha, bulkFormHandler */ +/*exported VuFind, htmlEncode, deparam, moreFacets, lessFacets, getUrlRoot, phoneNumberFormHandler, recaptchaOnLoad, resetCaptcha, bulkFormHandler, setupMultiILSLoginFields */ // IE 9< console polyfill window.console = window.console || { log: function polyfillLog() {} }; @@ -355,6 +355,26 @@ function setupJumpMenus(_container) { container.find('select.jumpMenu').change(function jumpMenu(){ $(this).parent('form').submit(); }); } +function setupMultiILSLoginFields(loginMethods, idPrefix) { + var searchPrefix = idPrefix ? '#' + idPrefix : '#'; + $(searchPrefix + 'target').change(function onChangeLoginTarget() { + var target = $(this).val(); + var $usernameGroup = $(searchPrefix + 'username').closest('.form-group'); + var $password = $(searchPrefix + 'password'); + if (loginMethods[target] === 'email') { + $usernameGroup.find('label.password-login').addClass('hidden'); + $usernameGroup.find('label.email-login').removeClass('hidden'); + $password.closest('.form-group').addClass('hidden'); + // Set password to a dummy value so that any checks for username+password work + $password.val('****'); + } else { + $usernameGroup.find('label.password-login').removeClass('hidden'); + $usernameGroup.find('label.email-login').addClass('hidden'); + $password.closest('.form-group').removeClass('hidden'); + } + }).change(); +} + $(document).ready(function commonDocReady() { // Start up all of our submodules VuFind.init(); diff --git a/themes/bootstrap3/templates/Auth/Email/loginfields.phtml b/themes/bootstrap3/templates/Auth/Email/loginfields.phtml new file mode 100644 index 0000000000000000000000000000000000000000..20b9986ebaa0c096bdc90dc8f239048423aa91a3 --- /dev/null +++ b/themes/bootstrap3/templates/Auth/Email/loginfields.phtml @@ -0,0 +1,4 @@ +<div class="form-group"> + <label class="control-label" for="login_<?=$this->escapeHtmlAttr($topClass)?>_username"><?=$this->transEsc('Email')?>:</label> + <input type="text" name="username" id="login_<?=$this->escapeHtmlAttr($topClass)?>_username" value="<?=$this->escapeHtmlAttr($this->request->get('username'))?>" class="form-control"/> +</div> diff --git a/themes/bootstrap3/templates/Auth/ILS/loginfields.phtml b/themes/bootstrap3/templates/Auth/ILS/loginfields.phtml new file mode 100644 index 0000000000000000000000000000000000000000..e8e0fb9c6164d3fbb0b78eb40c1bed1000e5d7d4 --- /dev/null +++ b/themes/bootstrap3/templates/Auth/ILS/loginfields.phtml @@ -0,0 +1,16 @@ +<?php $loginMethod = $this->auth()->getManager()->getILSLoginMethod(); ?> +<?php if ('email' === $loginMethod): ?> + <div class="form-group"> + <label class="control-label" for="login_<?=$this->escapeHtmlAttr($topClass)?>_username"><?=$this->transEsc('Email')?>:</label> + <input type="text" name="username" id="login_<?=$this->escapeHtmlAttr($topClass)?>_username" value="<?=$this->escapeHtmlAttr($this->request->get('username'))?>" class="form-control"/> + </div> +<?php else: ?> + <div class="form-group"> + <label class="control-label" for="login_<?=$this->escapeHtmlAttr($topClass)?>_username"><?=$this->transEsc('Username')?>:</label> + <input type="text" name="username" id="login_<?=$this->escapeHtmlAttr($topClass)?>_username" value="<?=$this->escapeHtmlAttr($this->request->get('username'))?>" class="form-control"/> + </div> + <div class="form-group"> + <label class="control-label" for="login_<?=$this->escapeHtmlAttr($topClass)?>_password"><?=$this->transEsc('Password')?>:</label> + <input type="password" name="password" id="login_<?=$this->escapeHtmlAttr($topClass)?>_password" class="form-control"/> + </div> +<?php endif; ?> \ No newline at end of file diff --git a/themes/bootstrap3/templates/Auth/MultiILS/loginfields.phtml b/themes/bootstrap3/templates/Auth/MultiILS/loginfields.phtml index e7136a43d327f4895a8fda87acd5db459f83be33..a7b253261bd7c09bedf9c2f15c40c4c0e82e0b95 100644 --- a/themes/bootstrap3/templates/Auth/MultiILS/loginfields.phtml +++ b/themes/bootstrap3/templates/Auth/MultiILS/loginfields.phtml @@ -1,17 +1,32 @@ +<?php $loginTargets = $this->auth()->getManager()->getLoginTargets(); ?> <div class="form-group"> <label class="control-label" for="login_target"><?=$this->transEsc('login_target')?>:</label> <?php $currentTarget = $this->request->get('target'); if (!$currentTarget) $currentTarget = $this->auth()->getManager()->getDefaultLoginTarget();?> - <select id="login_target" name="target" class="form-control"> - <?php foreach ($this->auth()->getManager()->getLoginTargets() as $target):?> + <select id="login_<?=$this->escapeHtmlAttr($topClass)?>_target" name="target" class="form-control"> + <?php foreach ($loginTargets as $target):?> <option value="<?=$this->escapeHtmlAttr($target)?>"<?=($target == $currentTarget ? ' selected="selected"' : '')?>><?=$this->transEsc("source_$target", null, $target)?></option> <?php endforeach ?> </select> </div> <div class="form-group"> - <label class="control-label" for="login_<?=$this->escapeHtmlAttr($topClass)?>_username"><?=$this->transEsc('Username')?>:</label> + <label class="control-label password-login" for="login_<?=$this->escapeHtmlAttr($topClass)?>_username"><?=$this->transEsc('Username')?>:</label> + <label class="control-label email-login hidden" for="login_<?=$this->escapeHtmlAttr($topClass)?>_username"><?=$this->transEsc('Email')?>:</label> <input id="login_<?=$this->escapeHtmlAttr($topClass)?>_username" type="text" name="username" value="<?=$this->escapeHtmlAttr($this->request->get('username'))?>" class="form-control"/> </div> <div class="form-group"> <label class="control-label" for="login_<?=$this->escapeHtmlAttr($topClass)?>_password"><?=$this->transEsc('Password')?>:</label> <input id="login_<?=$this->escapeHtmlAttr($topClass)?>_password" type="password" name="password" class="form-control"/> </div> + +<?php + $methods = []; + $authManager = $this->auth()->getManager(); + foreach ($loginTargets as $target) { + $methods[$target] = $authManager->getILSLoginMethod($target); + } + $methods = json_encode($methods); + $script = "setupMultiILSLoginFields($methods, 'login_{$topClass}_');"; + + // Inline the script for lightbox compatibility + echo $this->inlineScript(\Zend\View\Helper\HeadScript::SCRIPT, $script, 'SET'); +?> diff --git a/themes/bootstrap3/templates/librarycards/editcard.phtml b/themes/bootstrap3/templates/librarycards/editcard.phtml index 1e241c594c59d15b963c18971ebd60e6cd3635dc..7d8e40684fe2a84ddb7818ea7087fc6550392515 100644 --- a/themes/bootstrap3/templates/librarycards/editcard.phtml +++ b/themes/bootstrap3/templates/librarycards/editcard.phtml @@ -1,6 +1,6 @@ <?php // Set up page title: - $pageTitle = empty($this->card->id) ? 'Add a Library Card' : "Edit Library Card"; + $pageTitle = empty($this->card->id) ? 'Add a Library Card' : 'Edit Library Card'; $this->headTitle($this->translate($pageTitle)); // Set up breadcrumbs: @@ -30,14 +30,31 @@ </div> <?php endif; ?> <div class="form-group"> - <label class="control-label" for="login_username"><?=$this->transEsc('Username')?>:</label> + <?php if (null === $this->loginMethod || 'password' === $this->loginMethod): ?> + <label class="control-label password-login" for="login_username"><?=$this->transEsc('Username')?>:</label> + <?php endif; ?> + <?php if (null === $this->loginMethod || 'email' === $this->loginMethod): ?> + <label class="control-label email-login<?php if (null === $this->loginMethod): ?> hidden<?php endif; ?>" for="login_username"><?=$this->transEsc('Email')?>:</label> + <?php endif; ?> <input id="login_username" type="text" name="username" value="<?=$this->escapeHtmlAttr($this->username)?>" class="form-control"/> </div> - <div class="form-group"> - <label class="control-label" for="login_password"><?=$this->transEsc('Password')?>:</label> - <input id="login_password" type="password" name="password" value="" placeholder="<?=!empty($this->card->id) ? $this->escapeHtmlAttr($this->translate('library_card_edit_password_placeholder')) : ''?>" class="form-control"/> - </div> + <?php if (null === $this->loginMethod || 'password' === $this->loginMethod): ?> + <div class="form-group"> + <label class="control-label" for="login_password"><?=$this->transEsc('Password')?>:</label> + <input id="login_password" type="password" name="password" value="" placeholder="<?=!empty($this->card->id) ? $this->escapeHtmlAttr($this->translate('library_card_edit_password_placeholder')) : ''?>" class="form-control"/> + </div> + <?php endif; ?> <div class="form-group"> <input class="btn btn-primary" type="submit" name="submit" value="<?=$this->transEsc('Save') ?>"/> </div> </form> + +<?php + if (null !== $targets) { + $methods = json_encode($this->loginMethods); + $script = "setupMultiILSLoginFields($methods, 'login_');"; + + // Inline the script for lightbox compatibility + echo $this->inlineScript(\Zend\View\Helper\HeadScript::SCRIPT, $script, 'SET'); + } +?> diff --git a/themes/bootstrap3/templates/myresearch/cataloglogin.phtml b/themes/bootstrap3/templates/myresearch/cataloglogin.phtml index 0cb7afbe7fa2ec9d08a1853ae61ec1cd7b4ee0e5..fe0d20c5823755bbd09d98699d4c5ef39eaaa4ca 100644 --- a/themes/bootstrap3/templates/myresearch/cataloglogin.phtml +++ b/themes/bootstrap3/templates/myresearch/cataloglogin.phtml @@ -20,24 +20,43 @@ <form method="post" action="<?=$this->serverUrl(true)?>" class="form-catalog-login"> <?php if ($this->targets !== null): ?> <div class="form-group"> - <label class="control-label" for="login_target"><?=$this->transEsc('login_target')?>:</label> - <select id="login_target" name="target" class="form-control"> + <label class="control-label" for="profile_cat_login_target"><?=$this->transEsc('login_target')?>:</label> + <select id="profile_cat_target" name="target" class="form-control"> <?php foreach ($this->targets as $target): ?> - <option value="<?=$this->escapeHtmlAttr($target)?>"><?=$this->transEsc("source_$target", null, $target)?></option> + <option value="<?=$this->escapeHtmlAttr($target)?>"<?=($target === $this->defaultTarget ? ' selected="selected"' : '')?>><?=$this->transEsc("source_$target", null, $target)?></option> <?php endforeach; ?> </select> </div> <?php endif; ?> <div class="form-group"> - <label class="control-label" for="profile_cat_username"><?=$this->transEsc('Library Catalog Username')?>:</label> + <?php if (null === $this->loginMethod || 'password' === $this->loginMethod): ?> + <label class="control-label password-login" for="profile_cat_username"><?=$this->transEsc('Library Catalog Username')?>:</label> + <?php endif; ?> + <?php if (null === $this->loginMethod || 'email' === $this->loginMethod): ?> + <label class="control-label email-login<?php if (null === $this->loginMethod): ?> hidden<?php endif; ?>" for="profile_cat_username"><?=$this->transEsc('Email')?>:</label> + <?php endif; ?> <input id="profile_cat_username" type="text" name="cat_username" value="" class="form-control"/> </div> - <div class="form-group"> - <label class="control-label" for="profile_cat_password"><?=$this->transEsc('Library Catalog Password')?>:</label> - <input id="profile_cat_password" type="password" name="cat_password" value="" class="form-control"/> - </div> + <?php if (null === $this->loginMethod || 'password' === $this->loginMethod): ?> + <div class="form-group"> + <label class="control-label" for="profile_cat_password"><?=$this->transEsc('Library Catalog Password')?>:</label> + <input id="profile_cat_password" type="password" name="cat_password" value="" class="form-control"/> + </div> + <?php else: ?> + <input type="hidden" name="cat_password" value="****"> + <?php endif; ?> <div class="form-group"> <input class="btn btn-primary" type="submit" name="processLogin" value="<?=$this->transEsc('Login')?>"> </div> </form> + + <?php + if (null !== $this->targets) { + $methods = json_encode($this->loginMethods); + $script = "setupMultiILSLoginFields($methods, 'profile_cat_');"; + + // Inline the script for lightbox compatibility + echo $this->inlineScript(\Zend\View\Helper\HeadScript::SCRIPT, $script, 'SET'); + } + ?> <?php endif; ?> diff --git a/themes/root/templates/Email/login-link.phtml b/themes/root/templates/Email/login-link.phtml new file mode 100644 index 0000000000000000000000000000000000000000..83cc607865166e2c6ef0e50efc1b13439c4f9f6c --- /dev/null +++ b/themes/root/templates/Email/login-link.phtml @@ -0,0 +1,8 @@ +<?php // This is a text-only email template; do not include HTML! ?> +<?=$this->translate('email_login_requested', ['%%title%%' => $this->title])?> + + +<?=$this->translate('email_login_desc')?> + + +<?=$this->translate('email_login_link', ['%%url%%' => $this->url])?>