diff --git a/config/vufind/config.ini b/config/vufind/config.ini index 5c7c6da80660dca820c63633d74335e9932152ae..81e3c5bb8f6e34147c91d447c437711d1133c86c 100644 --- a/config/vufind/config.ini +++ b/config/vufind/config.ini @@ -388,10 +388,19 @@ recover_interval = 60 ; Default: Two weeks recover_hash_lifetime = 1209600 +; Allow users to set change their email address (if supported by Auth method). +; When turning this on, it is also strongly recommended to turn on verify_email +; below. +change_email = false + ; Allow users to set change their passwords (if supported by Auth method) change_password = true -; Force users to verify their email address before being able to log in (only if method=Database) +; Force users to verify their email address before being able to log in +; (only if method=Database) or make changes to it (if change_email=true). +; If you wish to customize the email messages used by the system, see the +; translation strings starting with verify and change_notification, as well as +; the notify-email-change.phtml and verify-email.phtml Email templates. verify_email = false ; Set this to false if you would like to store catalog passwords in plain text @@ -1770,7 +1779,7 @@ treeSearchLimit = 100 ; Use * for all supported forms ; Note: when "feedback" is active, Captcha can be conditionally disabled on a ; form-by-form basis with the useCaptcha setting in FeedbackForms.yaml. -;forms = changePassword, email, newAccount, passwordRecovery, sms +;forms = changeEmail, changePassword, email, newAccount, passwordRecovery, sms ; This section can be used to display default text inside the search boxes, useful diff --git a/languages/en.ini b/languages/en.ini index 696d693c15bbd9a589275b19645190ca63c75824..fc56a294e6f7d4a9c10f9a81a1a7e9136d7b4a0c 100644 --- a/languages/en.ini +++ b/languages/en.ini @@ -174,7 +174,12 @@ Catalog Login = "Catalog Login" Catalog Results = "Catalog Results" catalog_login_desc = "Enter your library catalog credentials." CD = "CD" +Change Email Address = "Change Email Address" Change Password = "Change Password" +change_email_disabled = "You are not allowed to change your email address at this time" +change_email_verification_reminder = "Submitting this form will send an email to the new address; you will have to click on a link in the email before the change will take effect." +change_notification_email_message = "A request was just made to change your email address at %%library%%. If you did not initiate this request, you may wish to log in at %%url%% and confirm the integrity of your account. Please contact support at %%email%% if you have questions or concerns." +change_notification_email_subject = "Account Email Change Notification" channel_add_more = "Add more channels like this" channel_browse = "Browse more records" channel_expand = "Explore related channels" @@ -325,6 +330,7 @@ Email address is invalid = "Email address is invalid" Email Record = "Email Record" Email this = "Email this" 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_maximum_recipients_note = "At most %%max%% recipients are allowed." @@ -668,6 +674,7 @@ New Item Search = "New Item Search" New Item Search Results = "New Item Search Results" New Items = "New Items" New Title = "New Title" +new_email_success = "Your email address has been changed successfully" new_password = "New Password" new_password_success = "Your password has successfully been changed" new_user_welcome_subject = "Your new account at %%library%%" @@ -1168,6 +1175,7 @@ Username = "Username" Username cannot be blank = "Username cannot be blank" Username is already in use in another library card = "Username is already in use in another library card" verification_done = "Your email address has been verified successfully." +verification_email_change_sent = "Email address verification instructions have been sent to the new email address. You must verify the address before the change will take effect." verification_email_notification = "A request was just made to verify your email address for your account with %%library%%." verification_email_sent = "Email address verification instructions have been sent to the email address registered with this account." verification_email_subject = "VuFind Email Verification" diff --git a/module/VuFind/config/module.config.php b/module/VuFind/config/module.config.php index f568416df0653d3d772428fc341bad5fbc442fd6..b413c471867c5d49913691b117fd21598cc1beb2 100644 --- a/module/VuFind/config/module.config.php +++ b/module/VuFind/config/module.config.php @@ -627,9 +627,10 @@ $staticRoutes = [ 'LibGuides/Home', 'LibGuides/Results', 'LibraryCards/Home', 'LibraryCards/SelectCard', 'LibraryCards/DeleteCard', - 'MyResearch/Account', 'MyResearch/ChangePassword', 'MyResearch/CheckedOut', - 'MyResearch/Delete', 'MyResearch/DeleteAccount', 'MyResearch/DeleteList', - 'MyResearch/Edit', 'MyResearch/Email', 'MyResearch/EmailNotVerified', 'MyResearch/Favorites', + 'MyResearch/Account', 'MyResearch/ChangeEmail', 'MyResearch/ChangePassword', + 'MyResearch/CheckedOut', 'MyResearch/Delete', 'MyResearch/DeleteAccount', + 'MyResearch/DeleteList', 'MyResearch/Edit', 'MyResearch/Email', + 'MyResearch/EmailNotVerified', 'MyResearch/Favorites', 'MyResearch/Fines', 'MyResearch/HistoricLoans', 'MyResearch/Holds', 'MyResearch/Home', 'MyResearch/ILLRequests', 'MyResearch/Logout', 'MyResearch/NewPassword', 'MyResearch/Profile', diff --git a/module/VuFind/sql/migrations/pgsql/6.1/001-modify-user-columns.sql b/module/VuFind/sql/migrations/pgsql/6.1/001-modify-user-columns.sql new file mode 100644 index 0000000000000000000000000000000000000000..347c60e01b15f4921ab2ecc492fc71d824ea5933 --- /dev/null +++ b/module/VuFind/sql/migrations/pgsql/6.1/001-modify-user-columns.sql @@ -0,0 +1,9 @@ +-- +-- Modifications to table `user` +-- + +ALTER TABLE "user" + ADD COLUMN pending_email varchar(255) NOT NULL DEFAULT ''; + +ALTER TABLE "user" + ADD COLUMN user_provided_email boolean NOT NULL DEFAULT '0'; diff --git a/module/VuFind/sql/mysql.sql b/module/VuFind/sql/mysql.sql index f500d8e70aec0d65a4eb780b7e099b729c3ab503..3b1e0285172eb5602e85bcc2a30789cafa4b9988 100644 --- a/module/VuFind/sql/mysql.sql +++ b/module/VuFind/sql/mysql.sql @@ -205,6 +205,8 @@ CREATE TABLE `user` ( `lastname` varchar(50) NOT NULL DEFAULT '', `email` varchar(255) NOT NULL DEFAULT '', `email_verified` datetime DEFAULT NULL, + `pending_email` varchar(255) NOT NULL DEFAULT '', + `user_provided_email` tinyint(1) NOT NULL DEFAULT '0', `cat_id` varchar(255) DEFAULT NULL, `cat_username` varchar(50) DEFAULT NULL, `cat_password` varchar(70) DEFAULT NULL, diff --git a/module/VuFind/sql/pgsql.sql b/module/VuFind/sql/pgsql.sql index e31d908e4e8b568ff2824a596c876b8ef3942df8..20f0a0f53c03dca6215e24adfafdcc4774ea1208 100644 --- a/module/VuFind/sql/pgsql.sql +++ b/module/VuFind/sql/pgsql.sql @@ -131,6 +131,8 @@ firstname varchar(50) NOT NULL DEFAULT '', lastname varchar(50) NOT NULL DEFAULT '', email varchar(255) NOT NULL DEFAULT '', email_verified timestamp DEFAULT NULL, +pending_email varchar(255) NOT NULL DEFAULT '', +user_provided_email boolean NOT NULL DEFAULT '0', cat_id varchar(255) DEFAULT NULL, cat_username varchar(50) DEFAULT NULL, cat_password varchar(70) DEFAULT NULL, diff --git a/module/VuFind/src/VuFind/Auth/CAS.php b/module/VuFind/src/VuFind/Auth/CAS.php index 5d7b2d60ffc04dbc9e075c121f55a43d58603176..341ea989e42adb3c10af8e2668d90eebea9f2588 100644 --- a/module/VuFind/src/VuFind/Auth/CAS.php +++ b/module/VuFind/src/VuFind/Auth/CAS.php @@ -142,7 +142,9 @@ class CAS extends AbstractBase foreach ($attribsToCheck as $attribute) { if (isset($cas->$attribute)) { $value = $casauth->getAttribute($cas->$attribute); - if ($attribute != 'cat_password') { + if ($attribute == 'email') { + $user->updateEmail($value); + } elseif ($attribute != 'cat_password') { $user->$attribute = ($value === null) ? '' : $value; } else { $catPassword = $value; diff --git a/module/VuFind/src/VuFind/Auth/Database.php b/module/VuFind/src/VuFind/Auth/Database.php index 4c9df10bc2effe3eb83f62691ffad2bdb9cd50fc..f81a82eae6140528a2d5e1ad158262bb2394b068 100644 --- a/module/VuFind/src/VuFind/Auth/Database.php +++ b/module/VuFind/src/VuFind/Auth/Database.php @@ -29,6 +29,7 @@ */ namespace VuFind\Auth; +use VuFind\Db\Row\User; use VuFind\Db\Table\User as UserTable; use VuFind\Exception\Auth as AuthException; use VuFind\Exception\AuthEmailNotVerified as AuthEmailNotVerifiedException; @@ -68,7 +69,7 @@ class Database extends AbstractBase * @param Request $request Request object containing account credentials. * * @throws AuthException - * @return \VuFind\Db\Row\User Object representing logged-in user. + * @return User Object representing logged-in user. */ public function authenticate($request) { @@ -110,7 +111,7 @@ class Database extends AbstractBase * @param Request $request Request object containing new account details. * * @throws AuthException - * @return \VuFind\Db\Row\User New user row. + * @return User New user row. */ public function create($request) { @@ -142,7 +143,7 @@ class Database extends AbstractBase * @param Request $request Request object containing new account details. * * @throws AuthException - * @return \VuFind\Db\Row\User New user row. + * @return User New user row. */ public function updatePassword($request) { @@ -201,7 +202,7 @@ class Database extends AbstractBase * Check if the user's email address has been verified (if necessary) and * throws exception if not. * - * @param \VuFind\Db\Row\User $user User to check + * @param User $user User to check * * @return void * @throws AuthEmailNotVerifiedException @@ -387,14 +388,14 @@ class Database extends AbstractBase * @param string[] $params Parameters returned from collectParamsFromRequest() * @param UserTable $table The VuFind user table * - * @return \VuFind\Db\Row\User A user row object + * @return User A user row object */ protected function createUserFromParams($params, $table) { $user = $table->createRowForUsername($params['username']); $user->firstname = $params['firstname']; $user->lastname = $params['lastname']; - $user->email = $params['email']; + $user->updateEmail($params['email'], true); if ($this->passwordHashingEnabled()) { $bcrypt = new Bcrypt(); $user->pass_hash = $bcrypt->create($params['password']); diff --git a/module/VuFind/src/VuFind/Auth/Facebook.php b/module/VuFind/src/VuFind/Auth/Facebook.php index 2ca46ee2410629d553ef70b464900018d3596e7e..c63b2bef1cae6b959c5beeb80cf6cb27ff7ce8cf 100644 --- a/module/VuFind/src/VuFind/Auth/Facebook.php +++ b/module/VuFind/src/VuFind/Auth/Facebook.php @@ -121,7 +121,7 @@ class Facebook extends AbstractBase implements $user->lastname = $details->last_name; } if (isset($details->email)) { - $user->email = $details->email; + $user->updateEmail($details->email); } // Save and return the user object: diff --git a/module/VuFind/src/VuFind/Auth/ILS.php b/module/VuFind/src/VuFind/Auth/ILS.php index c39b3aeb6624b5a4a00ac7452e3a4fca14341a61..8eee1714527140cb0527915bab9cd8948c5745e4 100644 --- a/module/VuFind/src/VuFind/Auth/ILS.php +++ b/module/VuFind/src/VuFind/Auth/ILS.php @@ -241,10 +241,11 @@ class ILS extends AbstractBase $user->password = ''; // Update user information based on ILS data: - $fields = ['firstname', 'lastname', 'email', 'major', 'college']; + $fields = ['firstname', 'lastname', 'major', 'college']; foreach ($fields as $field) { $user->$field = $info[$field] ?? ' '; } + $user->updateEmail($info['email']); // Update the user in the database, then return it to the caller: $user->saveCredentials( diff --git a/module/VuFind/src/VuFind/Auth/Manager.php b/module/VuFind/src/VuFind/Auth/Manager.php index 2722f1dd0274dd9ed71a9fa86b7481363856ff44..611aef87ece45f185ea1f248d8c1fc0f0b13ed7c 100644 --- a/module/VuFind/src/VuFind/Auth/Manager.php +++ b/module/VuFind/src/VuFind/Auth/Manager.php @@ -209,11 +209,24 @@ class Manager implements \ZfcRbac\Identity\IdentityProviderInterface && $this->getAuth($authMethod)->supportsPasswordRecovery(); } + /** + * Is email changing currently allowed? + * + * @param string $authMethod optional; check this auth method rather than + * the one in config file + * + * @return bool + */ + public function supportsEmailChange($authMethod = null) + { + return $this->config->Authentication->change_email ?? false; + } + /** * Is new passwords currently allowed? * * @param string $authMethod optional; check this auth method rather than - * the one in config file + * the one in config file * * @return bool */ @@ -551,6 +564,30 @@ class Manager implements \ZfcRbac\Identity\IdentityProviderInterface return $user; } + /** + * Update a user's email from the request. + * + * @param UserRow $user Object representing user being updated. + * @param string $email New email address to set (must be pre-validated!). + * + * @throws AuthException + * @return void + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function updateEmail(UserRow $user, $email) + { + // Depending on verification setting, either do a direct update or else + // put the new address into a pending state. + if ($this->config->Authentication->verify_email ?? false) { + $user->pending_email = $email; + } else { + $user->updateEmail($email, true); + } + $user->save(); + $this->updateSession($user); + } + /** * Try to log in the user using current query parameters; return User object * on success, throws exception on failure. diff --git a/module/VuFind/src/VuFind/Auth/Shibboleth.php b/module/VuFind/src/VuFind/Auth/Shibboleth.php index 2e490709de48d59fe127d9b6a25e226ebed556a2..63d5f33e607b190bb0baabf0a1f20137fdab69fc 100644 --- a/module/VuFind/src/VuFind/Auth/Shibboleth.php +++ b/module/VuFind/src/VuFind/Auth/Shibboleth.php @@ -142,7 +142,9 @@ class Shibboleth extends AbstractBase foreach ($attribsToCheck as $attribute) { if (isset($shib->$attribute)) { $value = $request->getServer()->get($shib->$attribute); - if ($attribute != 'cat_password') { + if ($attribute == 'email') { + $user->updateEmail($value); + } elseif ($attribute != 'cat_password') { $user->$attribute = ($value === null) ? '' : $value; } else { $catPassword = $value; diff --git a/module/VuFind/src/VuFind/Controller/AlmaController.php b/module/VuFind/src/VuFind/Controller/AlmaController.php index 32d3864ff7c97f2002b6a8bc89ca38ea0192b201..3753d63c18f63a03d8373589ea9212e8ca7f5bbf 100644 --- a/module/VuFind/src/VuFind/Controller/AlmaController.php +++ b/module/VuFind/src/VuFind/Controller/AlmaController.php @@ -238,7 +238,7 @@ class AlmaController extends AbstractBase $user->username = $username; $user->firstname = $firstname; $user->lastname = $lastname; - $user->email = $email; + $user->updateEmail($email); $user->cat_id = $primaryId; $user->cat_username = $username; diff --git a/module/VuFind/src/VuFind/Controller/MyResearchController.php b/module/VuFind/src/VuFind/Controller/MyResearchController.php index 30d7d1e1f1823a890e0bae5cd9adeb7ccee54011..3eae2a3fe1ad6e1020af1f026a13d6a7d360e10f 100644 --- a/module/VuFind/src/VuFind/Controller/MyResearchController.php +++ b/module/VuFind/src/VuFind/Controller/MyResearchController.php @@ -982,9 +982,17 @@ class MyResearchController extends AbstractBase { if ($this->params()->fromQuery('reverify')) { $table = $this->getTable('User'); + // Case 1: new user: $user = $table ->getByUsername($this->getUserVerificationContainer()->user, false); - $this->sendVerificationEmail($user); + // Case 2: pending email change: + if (!$user) { + $user = $this->getUser(); + if (!empty($user->pending_email)) { + $change = true; + } + } + $this->sendVerificationEmail($user, $change ?? false); } else { $this->flashMessenger()->addMessage('verification_email_sent', 'info'); } @@ -1571,13 +1579,46 @@ class MyResearchController extends AbstractBase } /** - * Send a verify email message. + * When a request to change a user's email address has been received, we should + * send a notification to the old email address for the user's information. * * @param \VuFind\Db\Row\User $user User object we're recovering * * @return void (sends email or adds error message) */ - protected function sendVerificationEmail($user) + protected function sendChangeNotificationEmail($user) + { + $config = $this->getConfig(); + $renderer = $this->getViewRenderer(); + // Custom template for emails (text-only) + $message = $renderer->render( + 'Email/notify-email-change.phtml', + [ + 'library' => $config->Site->title, + 'url' => $this->getServerUrl('home'), + 'email' => $config->Site->email, + ] + ); + // If the user is setting up a new account, use the main email + // address; if they have a pending address change, use that. + $this->serviceLocator->get('VuFind\Mailer\Mailer')->send( + $user->email, + $config->Site->email, + $this->translate('change_notification_email_subject'), + $message + ); + } + + /** + * Send a verify email message. + * + * @param \VuFind\Db\Row\User $user User object we're recovering + * @param bool $change Is the user changing their email (true) + * or setting up a new account (false). + * + * @return void (sends email or adds error message) + */ + protected function sendVerificationEmail($user, $change = false) { // If we can't find a user if (null == $user) { @@ -1588,7 +1629,7 @@ class MyResearchController extends AbstractBase $hashtime = $this->getHashAge($user->verify_hash); $recoveryInterval = $this->getConfig()->Authentication->recover_interval ?? 60; - if (time() - $hashtime < $recoveryInterval) { + if (time() - $hashtime < $recoveryInterval && !$change) { $this->flashMessenger() ->addMessage('verification_too_soon', 'error'); } else { @@ -1609,14 +1650,25 @@ class MyResearchController extends AbstractBase . $user->verify_hash . '&auth_method=' . $method ] ); + // If the user is setting up a new account, use the main email + // address; if they have a pending address change, use that. + $to = empty($user->pending_email) + ? $user->email : $user->pending_email; $this->serviceLocator->get('VuFind\Mailer\Mailer')->send( - $user->email, + $to, $config->Site->email, $this->translate('verification_email_subject'), $message ); - $this->flashMessenger() - ->addMessage('verification_email_sent', 'info'); + $flashMessage = $change + ? 'verification_email_change_sent' + : 'verification_email_sent'; + $this->flashMessenger()->addMessage($flashMessage, 'info'); + // If this is an email change, send a notification to the old + // email address as well. + if ($change) { + $this->sendChangeNotificationEmail($user); + } } catch (MailException $e) { $this->flashMessenger()->addMessage($e->getMessage(), 'error'); } @@ -1691,6 +1743,11 @@ class MyResearchController extends AbstractBase $user = $table->getByVerifyHash($hash); // If the hash is valid, store validation in DB and forward to login if (null != $user) { + // Apply pending email address change, if applicable: + if (!empty($user->pending_email)) { + $user->updateEmail($user->pending_email, true); + $user->pending_email = ''; + } $user->saveEmailVerified(); $this->setUpAuthenticationFromRequest(); @@ -1796,6 +1853,79 @@ class MyResearchController extends AbstractBase return $this->redirect()->toRoute('myresearch-home'); } + /** + * Handling submission of a new email for a user. + * + * @return view + */ + public function changeEmailAction() + { + // Always check that we are logged in and function is enabled first: + if (!$this->getAuthManager()->isLoggedIn()) { + return $this->forceLogin(); + } + if (!$this->getAuthManager()->supportsEmailChange()) { + $this->flashMessenger()->addMessage('change_email_disabled', 'error'); + return $this->redirect()->toRoute('home'); + } + $view = $this->createViewModel($this->params()->fromPost()); + // Display email + $user = $this->getUser(); + $view->email = $user->email; + // Identification + $view->useRecaptcha = $this->recaptcha()->active('changeEmail'); + // Special case: form was submitted: + if ($this->formWasSubmitted('submit', $view->useRecaptcha)) { + // Do CSRF check + $csrf = $this->serviceLocator->get(\VuFind\Validator\Csrf::class); + if (!$csrf->isValid($this->getRequest()->getPost()->get('csrf'))) { + throw new \VuFind\Exception\BadRequest( + 'error_inconsistent_parameters' + ); + } + // Update email + $validator = new \Zend\Validator\EmailAddress(); + $email = $this->params()->fromPost('email', ''); + try { + if (!$validator->isValid($email)) { + throw new AuthException('Email address is invalid'); + } + $this->getAuthManager()->updateEmail($user, $email); + // If we have a pending change, we need to send a verification email: + if (!empty($user->pending_email)) { + $this->sendVerificationEmail($user, true); + } else { + $this->flashMessenger() + ->addMessage('new_email_success', 'success'); + } + } catch (AuthException $e) { + $this->flashMessenger()->addMessage($e->getMessage(), 'error'); + return $view; + } + // Return to account home + return $this->redirect()->toRoute('myresearch-home'); + } elseif ($this->getConfig()->Authentication->verify_email ?? false) { + $this->flashMessenger() + ->addMessage('change_email_verification_reminder', 'info'); + } + if (!empty($user->pending_email)) { + $url = $this->url()->fromRoute('myresearch-emailnotverified') + . '?reverify=true'; + $this->flashMessenger()->addMessage( + [ + 'html' => true, + 'msg' => 'email_change_pending_html', + 'tokens' => [ + '%%pending%%' => $user->pending_email, + '%%url%%' => $url, + ], + ], + 'info' + ); + } + return $view; + } + /** * Handling submission of a new password for a user. * diff --git a/module/VuFind/src/VuFind/Db/Row/User.php b/module/VuFind/src/VuFind/Db/Row/User.php index a759f61b005fb8868d24aaa7deb88163c6ef47da..2c007ab088d0256fcba038e2f3ee02ce2045ea0a 100644 --- a/module/VuFind/src/VuFind/Db/Row/User.php +++ b/module/VuFind/src/VuFind/Db/Row/User.php @@ -680,6 +680,28 @@ class User extends RowGateway implements \VuFind\Db\Table\DbTableAwareInterface, return $this->save(); } + /** + * Update the user's email address, if appropriate. Note that this does NOT + * automatically save the row; it assumes a subsequent call will be made to + * the save() method. + * + * @param string $email New email address + * @param bool $userProvided Was this email provided by the user (true) or + * an automated lookup (false)? + * + * @return void + */ + public function updateEmail($email, $userProvided = false) + { + // Only change the email if it is a non-empty value and was user provided + // (the user is always right) or the previous email was NOT user provided + // (a value may have changed in an upstream system). + if (!empty($email) && ($userProvided || !$this->user_provided_email)) { + $this->email = $email; + $this->user_provided_email = $userProvided ? 1 : 0; + } + } + /** * Get the list of roles of this identity * diff --git a/module/VuFind/src/VuFind/Db/Table/User.php b/module/VuFind/src/VuFind/Db/Table/User.php index 17fb1a3c6114d7fd1d0ce5841c56c7ff47e06f97..b23ddcef64adc2753d1c155c65926d61022cda8f 100644 --- a/module/VuFind/src/VuFind/Db/Table/User.php +++ b/module/VuFind/src/VuFind/Db/Table/User.php @@ -90,6 +90,9 @@ class User extends Gateway $row = $this->createRow(); $row->username = $username; $row->created = date('Y-m-d H:i:s'); + // Failing to initialize this here can cause Zend\Db errors in + // the VuFind\Auth\Shibboleth and VuFind\Auth\ILS integration tests. + $row->user_provided_email = 0; return $row; } diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/AccountActionsTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/AccountActionsTest.php index 338b5ae1315099f5b6d6462d6ad6a7ecc722e43b..01fefbb74d81f29822a65e64e9b2851eb903a320 100644 --- a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/AccountActionsTest.php +++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/AccountActionsTest.php @@ -143,6 +143,79 @@ class AccountActionsTest extends \VuFindTest\Unit\MinkTestCase $this->snooze(); } + /** + * Test that changing email is disabled by default. + * + * @return void + */ + public function testChangeEmailDisabledByDefault() + { + // Go to profile page: + $session = $this->getMinkSession(); + $session->visit($this->getVuFindUrl('/MyResearch/Profile')); + $page = $session->getPage(); + + // Log in + $this->clickCss($page, '#loginOptions a'); + $this->fillInLoginForm($page, 'username1', 'good'); + $this->clickCss($page, '.modal-body .btn.btn-primary'); + $this->snooze(); + + // Now confirm that email button is absent: + $link = $page->findLink('Change Email Address'); + $this->assertFalse(is_object($link)); + } + + /** + * Test changing an email. + * + * @return void + */ + public function testChangeEmail() + { + // Turn on email change option: + $this->changeConfigs( + [ + 'config' => [ + 'Authentication' => [ + 'change_email' => true, + ] + ] + ] + ); + + // Go to profile page: + $session = $this->getMinkSession(); + $session->visit($this->getVuFindUrl('/MyResearch/Profile')); + $page = $session->getPage(); + + // Log in + $this->clickCss($page, '#loginOptions a'); + $this->fillInLoginForm($page, 'username1', 'good'); + $this->clickCss($page, '.modal-body .btn.btn-primary'); + $this->snooze(); + + // Now click change email button: + $this->findAndAssertLink($page, 'Change Email Address')->click(); + $this->snooze(); + + // Change the email: + $this->findCssAndSetValue($page, '[name="email"]', 'new@email.com'); + $this->clickCss($page, '[name="submit"]'); + $this->snooze(); + $this->assertEquals( + 'Your email address has been changed successfully', + $this->findCss($page, '.alert-success')->getText() + ); + + // Now go to profile page and confirm that email has changed: + $session->visit($this->getVuFindUrl('/MyResearch/Profile')); + $this->assertEquals( + 'First Name: Tester Last Name: McTestenson Email: new@email.com', + $this->findCss($page, '.table-striped')->getText() + ); + } + /** * Standard teardown method. * 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 167a364a76b01a964a1ac5339571ef324cae812c..2cfca678a4c3374f92bf60afe51307dabb7a801b 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/ManagerTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/ManagerTest.php @@ -249,6 +249,24 @@ class ManagerTest extends \VuFindTest\Unit\TestCase $this->assertTrue($this->getManager($config, null, null, $pm)->supportsRecovery()); } + /** + * Test supportsEmailChange + * + * @return void + */ + public function testSupportsEmailChange() + { + // Most common case -- no: + $this->assertFalse($this->getManager()->supportsEmailChange()); + + // Less common case -- yes: + $pm = $this->getMockPluginManager(); + $config = ['Authentication' => ['change_email' => true]]; + $this->assertTrue($this->getManager($config, null, null, $pm)->supportsEmailChange()); + $config = ['Authentication' => ['change_email' => false]]; + $this->assertFalse($this->getManager($config, null, null, $pm)->supportsEmailChange()); + } + /** * Test supportsPasswordChange * @@ -262,9 +280,11 @@ class ManagerTest extends \VuFindTest\Unit\TestCase // Less common case -- yes: $pm = $this->getMockPluginManager(); $db = $pm->get('Database'); - $db->expects($this->once())->method('supportsPasswordChange')->will($this->returnValue(true)); + $db->expects($this->any())->method('supportsPasswordChange')->will($this->returnValue(true)); $config = ['Authentication' => ['change_password' => true]]; $this->assertTrue($this->getManager($config, null, null, $pm)->supportsPasswordChange()); + $config = ['Authentication' => ['change_password' => false]]; + $this->assertFalse($this->getManager($config, null, null, $pm)->supportsPasswordChange()); } /** diff --git a/themes/bootstrap3/templates/myresearch/changeemail.phtml b/themes/bootstrap3/templates/myresearch/changeemail.phtml new file mode 100644 index 0000000000000000000000000000000000000000..356699c90a298e0479f0002d7009d822ffa1074c --- /dev/null +++ b/themes/bootstrap3/templates/myresearch/changeemail.phtml @@ -0,0 +1,32 @@ +<?php + // Set up page title: + $this->headTitle($this->translate('Change Email Address')); + + // Set up breadcrumbs: + $this->layout()->breadcrumbs = '<li><a href="' . $this->url('myresearch-home') . '">' . $this->transEsc('Your Account') . '</a></li>' + . '<li class="active">' . $this->transEsc('Change Email Address') . '</li>'; +?> +<div class="<?=$this->layoutClass('mainbody')?>"> + + <h2><?=$this->transEsc('Change Email Address') ?></h2> + <?=$this->flashmessages() ?> + + <?php if (!$this->auth()->getManager()->supportsEmailChange($this->auth_method)): ?> + <div class="error"><?=$this->transEsc('change_email_disabled') ?></div> + <?php else: ?> + <form id="newemail" class="form-new-email" action="<?=$this->url('myresearch-changeemail') ?>" method="post" data-toggle="validator" role="form"> + <input type="hidden" value="<?=$this->escapeHtmlAttr($this->auth()->getManager()->getCsrfHash())?>" name="csrf"/> + <div class="form-group"> + <label class="control-label"><?=$this->transEsc('Email Address') ?>:</label> + <input type="text" name="email" class="form-control" value="<?=$this->escapeHtmlAttr($this->email)?>" /> + </div> + <?=$this->recaptcha()->html($this->useRecaptcha) ?> + <div class="form-group"> + <input class="btn btn-primary" name="submit" type="submit" value="<?=$this->transEsc('Submit')?>" /> + </div> + </form> + <?php endif; ?> +</div> +<div class="<?=$this->layoutClass('sidebar')?>"> + <?=$this->context($this)->renderInContext("myresearch/menu.phtml", ['active' => 'newpassword'])?> +</div> diff --git a/themes/bootstrap3/templates/myresearch/profile.phtml b/themes/bootstrap3/templates/myresearch/profile.phtml index e38e97a926b1cb30bb63079772c3fa15995643b4..aed26de8389541fe7e40ea86121ce20ae06a9dbb 100644 --- a/themes/bootstrap3/templates/myresearch/profile.phtml +++ b/themes/bootstrap3/templates/myresearch/profile.phtml @@ -53,6 +53,12 @@ </table> <div id="account-actions"> + <?php if ($this->auth()->getManager()->supportsEmailChange()): ?> + <a class="btn btn-default" href="<?=$this->url('myresearch-changeemail') ?>"> + <i class="fa fa-fw fa-envelope" aria-hidden="true"></i> <?=$this->transEsc('Change Email Address') ?> + </a> + <?php endif; ?> + <?php if ($this->auth()->getManager()->supportsPasswordChange()): ?> <a class="btn btn-default" href="<?=$this->url('myresearch-changepassword') ?>"> <i class="fa fa-fw fa-lock" aria-hidden="true"></i> <?=$this->transEsc('Change Password') ?> diff --git a/themes/root/templates/Email/notify-email-change.phtml b/themes/root/templates/Email/notify-email-change.phtml new file mode 100644 index 0000000000000000000000000000000000000000..6ed60aaf2b49fe210e8793176a968d45a74c7290 --- /dev/null +++ b/themes/root/templates/Email/notify-email-change.phtml @@ -0,0 +1,4 @@ +<?=$this->translate( + 'change_notification_email_message', + ['%%library%%' => $this->library, '%%url%%' => $this->url, '%%email%%' => $this->email]) +?>