From 36b1c74c91cbbfa5fa0fa5eb9d64be1f9c431e2d Mon Sep 17 00:00:00 2001 From: Chris Hallberg <crhallberg@gmail.com> Date: Mon, 14 Apr 2014 15:39:55 -0400 Subject: [PATCH] Password change and recovery. - Resolves VUFIND-272 and VUFIND-296. --- config/vufind/config.ini | 13 + languages/en.ini | 20 ++ module/VuFind/config/module.config.php | 13 +- module/VuFind/sql/mysql.sql | 1 + module/VuFind/sql/postgres.sql | 1 + .../VuFind/src/VuFind/Auth/AbstractBase.php | 28 ++ module/VuFind/src/VuFind/Auth/Database.php | 82 +++++- module/VuFind/src/VuFind/Auth/Manager.php | 56 +++- .../Controller/MyResearchController.php | 246 ++++++++++++++++++ module/VuFind/src/VuFind/Db/Row/User.php | 13 + module/VuFind/src/VuFind/Db/Table/User.php | 11 + .../src/VuFind/View/Helper/Root/Auth.php | 26 ++ themes/blueprint/css/styles.css | 19 ++ themes/blueprint/images/silk/cog.png | Bin 0 -> 512 bytes themes/blueprint/images/silk/key_go.png | Bin 0 -> 744 bytes themes/blueprint/images/silk/lock.png | Bin 0 -> 749 bytes .../templates/Auth/AbstractBase/login.phtml | 3 + .../templates/Auth/Database/newpassword.phtml | 12 + .../templates/Auth/Database/recovery.phtml | 7 + .../blueprint/templates/myresearch/menu.phtml | 8 + .../templates/myresearch/newpassword.phtml | 22 ++ .../templates/myresearch/recover.phtml | 9 + themes/bootprint/css/icons.css | 3 + themes/bootprint/images/icons/cog.png | Bin 0 -> 512 bytes themes/bootprint/images/icons/key_go.png | Bin 0 -> 744 bytes themes/bootprint/images/icons/lock.png | Bin 0 -> 749 bytes .../templates/Auth/AbstractBase/login.phtml | 6 +- .../templates/Auth/Database/newpassword.phtml | 28 ++ .../templates/Auth/Database/recovery.phtml | 14 + .../bootstrap/templates/myresearch/menu.phtml | 28 +- .../templates/myresearch/newpassword.phtml | 26 ++ .../templates/myresearch/profile.phtml | 2 +- .../templates/myresearch/recover.phtml | 9 + .../templates/Auth/AbstractBase/login.phtml | 3 + .../templates/Auth/Database/newpassword.phtml | 15 ++ .../templates/Auth/Database/recovery.phtml | 10 + .../templates/myresearch/footer-navbar.phtml | 3 + .../templates/myresearch/newpassword.phtml | 29 +++ .../templates/myresearch/recover.phtml | 27 ++ .../templates/Email/recover-password.phtml | 3 + 40 files changed, 763 insertions(+), 33 deletions(-) create mode 100644 themes/blueprint/images/silk/cog.png create mode 100644 themes/blueprint/images/silk/key_go.png create mode 100644 themes/blueprint/images/silk/lock.png create mode 100644 themes/blueprint/templates/Auth/Database/newpassword.phtml create mode 100644 themes/blueprint/templates/Auth/Database/recovery.phtml create mode 100644 themes/blueprint/templates/myresearch/newpassword.phtml create mode 100644 themes/blueprint/templates/myresearch/recover.phtml create mode 100644 themes/bootprint/images/icons/cog.png create mode 100644 themes/bootprint/images/icons/key_go.png create mode 100644 themes/bootprint/images/icons/lock.png create mode 100644 themes/bootstrap/templates/Auth/Database/newpassword.phtml create mode 100644 themes/bootstrap/templates/Auth/Database/recovery.phtml create mode 100644 themes/bootstrap/templates/myresearch/newpassword.phtml create mode 100644 themes/bootstrap/templates/myresearch/recover.phtml create mode 100644 themes/jquerymobile/templates/Auth/Database/newpassword.phtml create mode 100644 themes/jquerymobile/templates/Auth/Database/recovery.phtml create mode 100644 themes/jquerymobile/templates/myresearch/newpassword.phtml create mode 100644 themes/jquerymobile/templates/myresearch/recover.phtml create mode 100644 themes/root/templates/Email/recover-password.phtml diff --git a/config/vufind/config.ini b/config/vufind/config.ini index 3bdefa43909..3df7cf9d036 100644 --- a/config/vufind/config.ini +++ b/config/vufind/config.ini @@ -216,6 +216,19 @@ hideLogin = false ; (only applies when method = Database above). hash_passwords = false +; Allow users to recover passwords via email (if supported by Auth method) +; You can set the subject of recovery emails in your +; language files under the term "recovery_email_subject" +recover_password = false +; Time (seconds) before another recovery attempt can be made +recover_interval = 60 +; Length of time before a recovery hash can no longer be used (expires) +; Default: Two weeks +recover_hash_lifetime = 1209600 + +; Allow users to set change their passwords (if supported by Auth method) +change_password = true + ; Set this to false if you would like to store catalog passwords in plain text encrypt_ils_password = false diff --git a/languages/en.ini b/languages/en.ini index c81f5be82a9..7786ee79333 100644 --- a/languages/en.ini +++ b/languages/en.ini @@ -146,6 +146,7 @@ cat_establish_account = "In order to establish your account profile, please ente cat_password_abbrev = "Catalog Password" cat_username_abbrev = "Catalog Username" CD = CD +Change Password = "Change Password" Check Hold = "Check Hold" Check Recall = "Check Recall" Checked Out = "Checked Out" @@ -185,6 +186,7 @@ confirm_hold_cancel_all_text = "Do you wish to cancel all your current holds?" confirm_hold_cancel_selected_text = "Do you wish to cancel your selected holds?" confirm_ill_request_cancel_all_text = "Do you wish to cancel all your current interlibrary loan requests?" confirm_ill_request_cancel_selected_text = "Do you wish to cancel your selected interlibrary loan requests?" +confirm_new_password = "Confirm New Password" confirm_storage_retrieval_request_cancel_all_text = "Do you wish to cancel all your current storage retrieval requests?" confirm_storage_retrieval_request_cancel_selected_text = "Do you wish to cancel your selected storage retrieval requests?" Contents = Contents @@ -199,6 +201,7 @@ course_reserves_empty_list = "No matching Course Reserves found." Cover Image = "Cover Image" Create a List = "Create a List" Create New Account = "Create New Account" +Create New Password = "Create New Password" Created = Created Date = Date date_day_placeholder = "D" @@ -525,6 +528,8 @@ New Item Search Results = "New Item Search Results" New Items = "New Items" New Title = "New Title" Newspaper = Newspaper +new_password = "New Password" +new_password_success = "Your password has successfully been changed" Next = Next No citations are available for this record = "No citations are available for this record" No Cover Image = "No Cover Image" @@ -579,6 +584,7 @@ no_items_selected = "No Items were Selected" Number = Number OAI Server = "OAI Server" of = of +old_password = "Old Password" On Reserve - Ask at Circulation Desk = "On Reserve - Ask at Circulation Desk" On Reserve = "On Reserve" Online Access = "Online Access" @@ -612,6 +618,7 @@ Please contact the Library Reference Department for assistance = "Please contact Please enable JavaScript. = "Please enable JavaScript." Posted by = "Posted by" posted_on = "on" +Preferences = Preferences Preferred Library = "Preferred Library" Prev = Prev Preview = "Preview" @@ -639,6 +646,19 @@ Read the full review online... = "Read the full review online..." Recall This = "Recall This" Record Citations = "Record Citations" Record Count = "Record Count" +recovery_by_email = "Recover by email" +recovery_by_username = "Recover by username" +recovery_disabled = "Password recovery not enabled" +recovery_email_notification = "A request was just made to recover the password for your account with %%library%%." +recovery_email_sent = "Password recovery instructions have been sent to the email address registered with this account." +recovery_email_subject = "VuFind Account Recovery" +recovery_email_url_pretext = "Please navigate to this url to set a new password: %%url%%" +recovery_expired_hash = "This recovery link has expired" +recovery_invalid_hash = "Recovery link not recognized" +recovery_new_disabled = "You are not allowed to change your password at this time" +recovery_title = "Password Recovery" +recovery_too_soon = "Too many recovery requests have been made, please try again later" +recovery_user_not_found = "We could not find your account" Region = Region Related Author = "Related Author" Related Items = "Related Items" diff --git a/module/VuFind/config/module.config.php b/module/VuFind/config/module.config.php index cb6821b0acc..b60712b1ef3 100644 --- a/module/VuFind/config/module.config.php +++ b/module/VuFind/config/module.config.php @@ -580,13 +580,14 @@ $staticRoutes = array( 'Install/FixSecurity', 'Install/FixSolr', 'Install/Home', 'Install/PerformSecurityFix', 'Install/ShowSQL', 'LibGuides/Home', 'LibGuides/Results', - 'MyResearch/Account', 'MyResearch/CheckedOut', 'MyResearch/Delete', - 'MyResearch/DeleteList', 'MyResearch/Edit', 'MyResearch/Email', - 'MyResearch/Favorites', 'MyResearch/Fines', + 'MyResearch/Account', 'MyResearch/ChangePassword', 'MyResearch/CheckedOut', + 'MyResearch/Delete', 'MyResearch/DeleteList', 'MyResearch/Edit', + 'MyResearch/Email', 'MyResearch/Favorites', 'MyResearch/Fines', 'MyResearch/Holds', 'MyResearch/Home', - 'MyResearch/ILLRequests', - 'MyResearch/Logout', 'MyResearch/Profile', - 'MyResearch/SaveSearch', 'MyResearch/StorageRetrievalRequests', + 'MyResearch/ILLRequests', 'MyResearch/Logout', + 'MyResearch/NewPassword', 'MyResearch/Profile', + 'MyResearch/Recover', 'MyResearch/SaveSearch', + 'MyResearch/StorageRetrievalRequests', 'MyResearch/Verify', 'Primo/Advanced', 'Primo/Home', 'Primo/Search', 'QRCode/Show', 'QRCode/Unavailable', 'OAI/Server', 'Pazpar2/Home', 'Pazpar2/Search', 'Records/Home', diff --git a/module/VuFind/sql/mysql.sql b/module/VuFind/sql/mysql.sql index 954c5253e8d..5cfb9252dc3 100644 --- a/module/VuFind/sql/mysql.sql +++ b/module/VuFind/sql/mysql.sql @@ -178,6 +178,7 @@ CREATE TABLE `user` ( `major` varchar(100) NOT NULL DEFAULT '', `home_library` varchar(100) NOT NULL DEFAULT '', `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', + `verify_hash` varchar(42) NOT NULL DEFAULT '', PRIMARY KEY (`id`), UNIQUE KEY `username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/module/VuFind/sql/postgres.sql b/module/VuFind/sql/postgres.sql index eb4c419725a..2d8c9aff055 100644 --- a/module/VuFind/sql/postgres.sql +++ b/module/VuFind/sql/postgres.sql @@ -110,6 +110,7 @@ college varchar(100) NOT NULL DEFAULT '', major varchar(100) NOT NULL DEFAULT '', home_library varchar(100) NOT NULL DEFAULT '', created timestamp NOT NULL DEFAULT '1970-01-01 00:00:00', +verify_hash varchar(42) NOT NULL DEFAULT '', PRIMARY KEY (id), UNIQUE (username) ); diff --git a/module/VuFind/src/VuFind/Auth/AbstractBase.php b/module/VuFind/src/VuFind/Auth/AbstractBase.php index 464452378e5..54d74e5174c 100644 --- a/module/VuFind/src/VuFind/Auth/AbstractBase.php +++ b/module/VuFind/src/VuFind/Auth/AbstractBase.php @@ -144,6 +144,23 @@ abstract class AbstractBase implements \VuFind\Db\Table\DbTableAwareInterface ); } + /** + * Update a user's password from the request. + * + * @param \Zend\Http\PhpEnvironment\Request $request Request object containing + * new account details. + * + * @throws AuthException + * @return \VuFind\Db\Row\User New user row. + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function updatePassword($request) + { + throw new AuthException( + 'Account password updating not supported by ' . get_class($this) + ); + } + /** * Get the URL to establish a session (needed when the internal VuFind login * form is inadequate). Returns false when no session initiator is needed. @@ -184,6 +201,17 @@ abstract class AbstractBase implements \VuFind\Db\Table\DbTableAwareInterface return false; } + /** + * Does this authentication method support password changing + * + * @return bool + */ + public function supportsPasswordChange() + { + // By default, password changing is not supported. + return false; + } + /** * Does the class allow for authentication from more than one auth method? * If so return an array that lists the classes for the methods allowed. diff --git a/module/VuFind/src/VuFind/Auth/Database.php b/module/VuFind/src/VuFind/Auth/Database.php index 4938747b6d1..cbd231fd56b 100644 --- a/module/VuFind/src/VuFind/Auth/Database.php +++ b/module/VuFind/src/VuFind/Auth/Database.php @@ -119,18 +119,8 @@ class Database extends AbstractBase } // Validate Input - // Needs a username - if (trim($params['username']) == '') { - throw new AuthException('Username cannot be blank'); - } - // Needs a password - if (trim($params['password']) == '') { - throw new AuthException('Password cannot be blank'); - } - // Passwords don't match - if ($params['password'] != $params['password2']) { - throw new AuthException('Passwords do not match'); - } + $this->validateUsernameAndPassword($params); + // Invalid Email Check $validator = new \Zend\Validator\EmailAddress(); if (!$validator->isValid($params['email'])) { @@ -170,6 +160,64 @@ class Database extends AbstractBase return $table->getByUsername($params['username'], false); } + /** + * Update a user's password from the request. + * + * @param \Zend\Http\PhpEnvironment\Request $request Request object containing + * new account details. + * + * @throws AuthException + * @return \VuFind\Db\Row\User New user row. + */ + public function updatePassword($request) + { + // Ensure that all expected parameters are populated to avoid notices + // in the code below. + $params = array( + 'username' => '', 'password' => '', 'password2' => '' + ); + foreach ($params as $param => $default) { + $params[$param] = $request->getPost()->get($param, $default); + } + + // Validate Input + $this->validateUsernameAndPassword($params); + + // Create the row and send it back to the caller: + $table = $this->getUserTable(); + $user = $table->getByUsername($params['username'], false); + if ($this->passwordHashingEnabled()) { + $bcrypt = new Bcrypt(); + $user->pass_hash = $bcrypt->create($params['password']); + } else { + $user->password = $params['password']; + } + $user->save(); + return $user; + } + + /** + * Make sure username and password aren't blank + * Make sure passwords match + * + * @param array $params + */ + protected function validateUsernameAndPassword($params) + { + // Needs a username + if (trim($params['username']) == '') { + throw new AuthException('Username cannot be blank'); + } + // Needs a password + if (trim($params['password']) == '') { + throw new AuthException('Password cannot be blank'); + } + // Passwords don't match + if ($params['password'] != $params['password2']) { + throw new AuthException('Passwords do not match'); + } + } + /** * Check that the user's password matches the provided value. * @@ -240,4 +288,14 @@ class Database extends AbstractBase { return true; } + + /** + * Does this authentication method support password changing + * + * @return bool + */ + public function supportsPasswordChange() + { + return true; + } } \ No newline at end of file diff --git a/module/VuFind/src/VuFind/Auth/Manager.php b/module/VuFind/src/VuFind/Auth/Manager.php index b27a2968e2c..f217c64488f 100644 --- a/module/VuFind/src/VuFind/Auth/Manager.php +++ b/module/VuFind/src/VuFind/Auth/Manager.php @@ -156,7 +156,7 @@ class Manager implements ServiceLocatorAwareInterface /** * Does the current configuration support account creation? * - *@param string $authMethod optional; check this auth method rather than + * @param string $authMethod optional; check this auth method rather than * the one in config file * * @return bool @@ -169,6 +169,43 @@ class Manager implements ServiceLocatorAwareInterface return $this->getAuth()->supportsCreation(); } + /** + * Does the current configuration support password recovery? + * + * @param string $authMethod optional; check this auth method rather than + * the one in config file + * + * @return bool + */ + public function supportsRecovery($authMethod=null) + { + if ($authMethod != null) { + $this->setActiveAuthClass($authMethod); + } + if ($this->getAuth()->supportsPasswordChange()) { + return isset($this->config->Authentication->recover_password) + && $this->config->Authentication->recover_password; + } + return false; + } + + /** + * Is new passwords currently allowed? + * + * @return bool + */ + public function supportsPasswordChange($authMethod=null) + { + if ($authMethod != null) { + $this->setActiveAuthClass($authMethod); + } + if ($this->getAuth()->supportsPasswordChange()) { + return isset($this->config->Authentication->change_password) + && $this->config->Authentication->change_password; + } + return false; + } + /** * Get the URL to establish a session (needed when the internal VuFind login * form is inadequate). Returns false when no session initiator is needed. @@ -385,6 +422,22 @@ class Manager implements ServiceLocatorAwareInterface return $user; } + /** + * Update a user's password from the request. + * + * @param \Zend\Http\PhpEnvironment\Request $request Request object containing + * new account details. + * + * @throws AuthException + * @return \VuFind\Db\Row\User New user row. + */ + public function updatePassword($request) + { + $user = $this->getAuth()->updatePassword($request); + $this->updateSession($user); + return $user; + } + /** * Try to log in the user using current query parameters; return User object * on success, throws exception on failure. @@ -528,5 +581,4 @@ class Manager implements ServiceLocatorAwareInterface $this->authToProxy = $method; $this->authProxied = false; } - } diff --git a/module/VuFind/src/VuFind/Controller/MyResearchController.php b/module/VuFind/src/VuFind/Controller/MyResearchController.php index 3713c052146..2fcfe668c94 100644 --- a/module/VuFind/src/VuFind/Controller/MyResearchController.php +++ b/module/VuFind/src/VuFind/Controller/MyResearchController.php @@ -28,6 +28,7 @@ namespace VuFind\Controller; use VuFind\Exception\Auth as AuthException, + VuFind\Exception\Mail as MailException, VuFind\Exception\ListPermission as ListPermissionException, VuFind\Exception\RecordMissing as RecordMissingException, Zend\Stdlib\Parameters; @@ -1109,4 +1110,249 @@ class MyResearchController extends AbstractBase $url = $this->getServerUrl('myresearch-home'); return $this->getAuthManager()->getSessionInitiator($url); } + + /** + * Send account recovery email + * + * @return View object + */ + public function recoverAction() + { + // Make sure we're configured to do this + if (!$this->getAuthManager()->supportsRecovery()) { + $this->flashMessenger()->setNamespace('error') + ->addMessage('recovery_disabled'); + return $this->redirect()->toRoute('myresearch-home'); + } + if ($this->getUser()) { + return $this->redirect()->toRoute('myresearch-home'); + } + // Database + $table = $this->getTable('User'); + $user = false; + // Check if we have a submitted form, and use the information to get + // the user's information + if ($email = $this->params()->fromPost('email')) { + $user = $table->getByEmail($email); + } elseif ($username = $this->params()->fromPost('username')) { + $user = $table->getByUsername($username, false); + } + // If we have a submitted form + if (false != $user) { + $this->sendRecoveryEmail($user, $this->getConfig()); + } else if ($this->params()->fromPost('submit')) { + $this->flashMessenger()->setNamespace('error') + ->addMessage('recovery_user_not_found'); + } + return $this->createViewModel(); + } + + /** + * Helper function for recoverAction + * + * @param \VuFind\Db\Row\User $user User object we're recovering + * @param \VuFind\Config $config Configuration object + * + * @return void (sends email or adds error message) + */ + protected function sendRecoveryEmail($user, $config) + { + // If we can't find a user + if (null == $user) { + $this->flashMessenger()->setNamespace('error') + ->addMessage('recovery_user_not_found'); + } else { + // Make sure we've waiting long enough + $hashtime = $this->getHashAge($user->verify_hash); + $recoveryInterval = isset($config->Authentication->recover_interval) + ? $config->Authentication->recover_interval + : 60; + if (time()-$hashtime < $recoveryInterval) { + $this->flashMessenger()->setNamespace('error') + ->addMessage('recovery_too_soon'); + } else { + // Attempt to send the email + try { + // Create a fresh hash + $user->updateHash(); + $config = $this->getConfig(); + $renderer = $this->getViewRenderer(); + // Custom template for emails (text-only) + $message = $renderer->render( + 'Email/recover-password.phtml', + array( + 'library' => $config->Site->title, + 'url' => $this->getServerUrl('myresearch-verify') + . '?hash=' + . $user->verify_hash + ) + ); + $this->getServiceLocator()->get('VuFind\Mailer')->send( + $user->email, + $config->Site->email, + $this->translate('recovery_email_subject'), + $message + ); + $this->flashMessenger()->setNamespace('info') + ->addMessage('recovery_email_sent'); + } catch (MailException $e) { + $this->flashMessenger()->setNamespace('error') + ->addMessage($e->getMessage()); + } + } + } + } + + /** + * Receive a hash and display the new password form if it's valid + * + * @return view + */ + public function verifyAction() + { + // If we have a submitted form + $hash = $this->params()->fromQuery('hash'); + // Submitted form + if (null != $hash) { + $hashtime = $this->getHashAge($hash); + $config = $this->getConfig(); + // Check if hash is expired + $hashLifetime = isset($config->Authentication->recover_hash_lifetime) + ? $config->Authentication->recover_hash_lifetime + : 1209600; // Two weeks + if (time()-$hashtime > $hashLifetime) { + $this->flashMessenger()->setNamespace('error') + ->addMessage('recovery_expired_hash'); + return $this->forwardTo('MyResearch', 'Login'); + } else { + $table = $this->getTable('User'); + $user = $table->getByVerifyHash($hash); + // If the hash is valid, forward user to create new password + if (null != $user) { + $view = $this->createViewModel(); + $view->hash = $hash; + $view->username = $user->username; + $view->setTemplate('myresearch/newpassword'); + return $view; + } + } + } + $this->flashMessenger()->setNamespace('error') + ->addMessage('recovery_invalid_hash'); + return $this->forwardTo('MyResearch', 'Login'); + } + + /** + * Handling submission of a new password for a user. + * + * @return view + */ + public function newPasswordAction() + { + // Have we submitted the form? + if (!$this->params()->fromPost('submit', false)) { + return $this->redirect()->toRoute('home'); + } + // Pull in from POST + $request = $this->getRequest(); + $post = $request->getPost(); + // Verify hash + $userFromHash = isset($post->hash) + ? $this->getTable('User')->getByVerifyHash($post->hash) + : false; + // Missing or invalid hash + if (false == $userFromHash) { + $this->flashMessenger()->setNamespace('error') + ->addMessage('recovery_user_not_found'); + // Force login or restore hash + return $this->forwardTo('MyResearch', 'ChangePassword'); + } else if ($userFromHash->username !== $post->username) { + $this->flashMessenger()->setNamespace('error') + ->addMessage('authentication_error_invalid'); + $userFromHash->updateHash(); + $post->username = $userFromHash->username; + $post->hash = $userFromHash->verify_hash; + return $this->createViewModel($post); + } + // Verify old password if we're logged in + if ($this->getUser()) { + if (isset($post->oldpwd)) { + try { + // Reassign oldpwd to password in the request so login works + $temp_password = $post->password; + $post->password = $post->oldpwd; + $authClass = $this->getAuthManager()->login($request); + $post->password = $temp_password; + } catch(AuthException $e) { + $this->flashMessenger()->setNamespace('error') + ->addMessage($e->getMessage()); + return $this->createViewModel( + $this->params()->fromPost() + ); + } + } else { + $this->flashMessenger()->setNamespace('error') + ->addMessage('authentication_error_invalid'); + $post['verifyold'] = true; + return $this->createViewModel($post); + } + } + // Update password + try { + $user = $this->getAuthManager()->updatePassword($this->getRequest()); + } catch (AuthException $e) { + $this->flashMessenger()->setNamespace('error') + ->addMessage($e->getMessage()); + return $this->createViewModel( + $this->params()->fromPost() + ); + } + // Update hash to prevent reusing hash + $user->updateHash(); + // Login + $this->getAuthManager()->login($this->request); + // Go to favorites + $this->flashMessenger()->setNamespace('info') + ->addMessage('new_password_success'); + return $this->redirect()->toRoute('myresearch-home'); + } + + /** + * Handling submission of a new password for a user. + * + * @return view + */ + public function changePasswordAction() + { + if (!$this->getAuthManager()->isLoggedIn()) { + return $this->forceLogin(); + } + // If not submitted, are we logged in? + if (!$this->getAuthManager()->supportsPasswordChange()) { + $this->flashMessenger()->setNamespace('error') + ->addMessage('recovery_new_disabled'); + return $this->redirect()->toRoute('home'); + } + $view = $this->createViewModel($this->params()->fromPost()); + // Verify user password + $view->verifyold = true; + // Display username + $user = $this->getUser(); + $view->username = $user->username; + // Identification + $user->updateHash(); + $view->hash = $user->verify_hash; + $view->setTemplate('myresearch/newpassword'); + return $view; + } + + /** + * Helper function for verification hashes + * + * @return int age in seconds + */ + protected function getHashAge($hash) + { + return intval(substr($hash, -10)); + } } diff --git a/module/VuFind/src/VuFind/Db/Row/User.php b/module/VuFind/src/VuFind/Db/Row/User.php index dbc1d5bc4f5..aac4c06cc44 100644 --- a/module/VuFind/src/VuFind/Db/Row/User.php +++ b/module/VuFind/src/VuFind/Db/Row/User.php @@ -406,4 +406,17 @@ class User extends ServiceLocatorAwareGateway // Remove the user itself: return parent::delete(); } + + /** + * Update the verification hash for this user + * + * @return bool save success + */ + public function updateHash() + { + $this->verify_hash = md5( + $this->username . $this->password . $this->pass_hash . rand() + ) . (time() % pow(10,10)); + return $this->save(); + } } diff --git a/module/VuFind/src/VuFind/Db/Table/User.php b/module/VuFind/src/VuFind/Db/Table/User.php index 6174b4a2c58..0f91d6a906d 100644 --- a/module/VuFind/src/VuFind/Db/Table/User.php +++ b/module/VuFind/src/VuFind/Db/Table/User.php @@ -93,4 +93,15 @@ class User extends Gateway }; return $this->select($callback); } + + /** + * Return a row by a verification hash + * + * @return mixed + */ + public function getByVerifyHash($hash) + { + $row = $this->select(array('verify_hash' => $hash))->current(); + return $row; + } } diff --git a/module/VuFind/src/VuFind/View/Helper/Root/Auth.php b/module/VuFind/src/VuFind/View/Helper/Root/Auth.php index 9c852d6632c..f2286308dea 100644 --- a/module/VuFind/src/VuFind/View/Helper/Root/Auth.php +++ b/module/VuFind/src/VuFind/View/Helper/Root/Auth.php @@ -227,4 +227,30 @@ class Auth extends \Zend\View\Helper\AbstractHelper $classParts = explode('\\', $className); return array_pop($classParts); } + + + /** + * Render the new password form template. + * + * @param array $context Context for rendering template + * + * @return string + */ + public function getNewPasswordForm($context = array()) + { + return $this->renderTemplate('newpassword.phtml', $context); + } + + + /** + * Render the password recovery form template. + * + * @param array $context Context for rendering template + * + * @return string + */ + public function getPasswordRecoveryForm($context = array()) + { + return $this->renderTemplate('recovery.phtml', $context); + } } diff --git a/themes/blueprint/css/styles.css b/themes/blueprint/css/styles.css index 49f518ec79a..af0baeb04de 100644 --- a/themes/blueprint/css/styles.css +++ b/themes/blueprint/css/styles.css @@ -647,6 +647,18 @@ input.bookbagEmpty { background-position: left; padding:.5em .5em .5em 20px; } +.forgot_password { + background-image:url(../images/silk/key_go.png); + background-repeat:no-repeat; + background-position: left; + padding:.5em .5em .5em 20px; +} +.lock { + background-image:url(../images/silk/lock.png); + background-repeat:no-repeat; + background-position: left; + padding:.5em .5em .5em 20px; +} .cite { background-image:url(../images/silk/report.png); background-repeat:no-repeat; @@ -724,6 +736,13 @@ input.bookbagEmpty { padding:.5em .5em .5em 20px; /*margin-left:1em;*/ } +.gear { + background-image:url(../images/silk/cog.png); + background-repeat:no-repeat; + background-position: left; + padding:.6em .5em .5em 20px; + /*margin-left:1em;*/ +} h3.list { padding-bottom:0; margin-bottom:0; diff --git a/themes/blueprint/images/silk/cog.png b/themes/blueprint/images/silk/cog.png new file mode 100644 index 0000000000000000000000000000000000000000..67de2c6ccbeac17742f56cf7391e72b2bf5033ba GIT binary patch literal 512 zcmV+b0{{JqP)<h;3K|Lk000e1NJLTq000mG000mO1ONa4wfZ;e00004XF*Lt006JZ zHwB960000PbVXQnQ*UN;cVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBUzl1W5CR4C6? zQB6w%VGv&Y0p9%y{*S$M5weR^mokV7i!Mcpk=YVa(4jh%5k+ND8>CQDsH?WF>AIFt zQuJ}i;w2$ZUU#3SZ6RY0Gw;kZ&ol1~2ky^QZ(fom$=jN<T*X<otG0ao1Md*)XSSIA z*x3T8gv)x7DUK|A#S3CA>JZt!z7w_pH~wdQ;R)Gh%BbQFCx+Nm!4SuS-vkr`vhhrX zM*>w%e+v~?m@q~ImPAgtLkR_3U<2F8LP3W5=LJ*ZN|S5p#sf4YFr$p~Q~Z*0Ngxf2 zjk#J#<7EAlhzlrV53~GF&pIzcCN_lz9@05Ue<MwWI-*!M0jvB0F~pIke2>oUXiK%N z#x+4o*i_c|6_Uu1+&TIho?3@y4k-#b8Y_o94zW*B3a1ne2-Y5s0uke$$|@=}OP-i= zNYZQA=>PrZu0MfSL=b8UhD_={W4IY1{b{)U)*gc45xtL%IYLY&hF;d`@GzI&7H&D# zh;z_BX$#hqh@q?AY3sJTod2%*Yd)_>YM0#q&ixGuh+PQsneK)F0000<MNUMnLSTXw CAKTXe literal 0 HcmV?d00001 diff --git a/themes/blueprint/images/silk/key_go.png b/themes/blueprint/images/silk/key_go.png new file mode 100644 index 0000000000000000000000000000000000000000..30b0dc316e52dba388d88112d4c1cc32672fffbb GIT binary patch literal 744 zcmV<E0vG*>P)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00004XF*Lt006JZ zHwB960000PbVXQnQ*UN;cVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU!dPzh<R5;6x zlU+!YVHk$LRU{on2NEG99t4gN<(k<vgSIj~$mk$~eksv@gmxl=Ow(;Fr^B3}b5Lr_ z2G&-MqzD~`qBeg)s5vYXON+MJpPSp<&)xfdS}mK_2lvVQa$nE=JkR@40I2o!$#_If zgcYe*-~X369QeQ}9^~I<-#CKyR)jn~<T%PeX7qx)>jGlUfiHCke$)P}4*HwX@?mb` zzgiE#M5fLDxtj=lZ6q=+Lkt2$!wyVqxC~@X03De&Gn$t$kdWJig()R`(|L#ldNHNi zi?tK@-;xA1zab1r3XkO&T;m5wdrx2~XU8>fpl4v~7ZF1h+!NXGy*rQ6jw}?mrNKGI zL&zzIrIL-VTRiLE`+jy5wt-SCISiy4pO}x6>V>$`&V{7&3{GoOF)87oTao_ek0H|L zXxL5q?8f4p7$RLZL=Q4>?LH3$EqhS@^b{VAG@wL(0y*{DquI)BECvw!(t<WU7OeSj z4slT2o&n>y8jr^s8DqzY3Mx|xw6AM%RhNVG>V(j48H=@2*+n87;k63kFsH&hc^Czx zU)p@TON5%2#gM-!LRIG_NS|MUrcZ`*_YPuLB^9Iro+W2D85SRofmAHM&xik`9B1#a z@o-oLow*L$!CJHqC<x>_n){?E(&Zwhf|^V!qqZ#X+&c)jMMwsg3*W31W<{FoWOGV1 zuOTTStWS(&DYr&0v}HowTZPN*IY_RcCU%rj3Cs*;4T3Shy&sFSmGFOV!%!{P*`>;C zSiN43jAg&56(U(ojS}<bU;n~z%OUZEdjI^WYM*^X$^G8flvN$?agoUOo#Ks1ETcBX ap8o(~AJmyDx~^sb0000<MNUMnLSTY)_fHN0 literal 0 HcmV?d00001 diff --git a/themes/blueprint/images/silk/lock.png b/themes/blueprint/images/silk/lock.png new file mode 100644 index 0000000000000000000000000000000000000000..2ebc4f6f9663e32cad77d67ef93ab8843dfea3c0 GIT binary patch literal 749 zcmV<J0uud+P)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00004XF*Lt006JZ zHwB960000PbVXQnQ*UN;cVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU!e@R3^R5;6R zQbB7IK^T3Tq)8K-7_~JtN==(+K|#b@JqUt5h~h!Lc~ek?Ku+zk2XpMN@Q@-NdiDp1 z1*uxlMiZNsG$@o5lP0D~c6MfcvuoP5I`A<w-+bSj_uh<Q+cvyoX=!OhDK#ghoMD_~ zfbo;DVp-N=E|>e|tv9>?g+k#9o0pTx<YX)sgU{y!_vrO{sMqV*;vmqy`T6;^e*oA# z!o!d0bUI_2CTg`BI-QQb9f3dqiA2JwD;A3z%w1ksSm^4#Z-B()v+?oqj1U6la(T1e zZl|~o>d@;_sq{kwlU;^VvV*?BV8P@}BoaZTQUROpWV6|-M`|^n&)=+8tHo3*<<$NU zU`%V~ZF;?hBSYsjJ6%JzV}E(D{pOLqQklliUf9um_tGl-wty`y*p?eYNW56P>X@1s zZs7KrRZKtmV7Lqj^5Fgr7_`LjhdJK@ltF&O`j7?*NUM$KvmNGz)3WjM?V$vHlP<J& zUm*}0g<*`aa0m#;nO4C59%Snq%<gw6YaijsENrvy0U$*veUpji`g`g;hWN#6sJ&if z|7lEIpGEWQIsqDprcRKsge^=jfN*5kq#B>T0AFyF?kLE<#HZabCSW3-o<y$`V(q@e zY5?H;1Doz@RIRn~d5tXI@x+4aDfGLfYLi*%3!3F^SFTb{&mjZ7(WsOVKc9j>a*6;Z zrXD`Ulwd<^2glP%1Y1Kc1Ij%DU^=ME(jKf6APNlA$Uu;J4bVilQHSWX5uJ$9Zsp4M z0%!@LvyTxz=Z6stxlichODIY+yNGt%RM;m`>H4LOKLFs9Y%b5aUN|2|{0Zw|<_~i} fmXz*V19AKYa<a0`hi}0}00000NkvXXu0mjf@;P6V literal 0 HcmV?d00001 diff --git a/themes/blueprint/templates/Auth/AbstractBase/login.phtml b/themes/blueprint/templates/Auth/AbstractBase/login.phtml index 30df536ea06..ee38214e7d5 100644 --- a/themes/blueprint/templates/Auth/AbstractBase/login.phtml +++ b/themes/blueprint/templates/Auth/AbstractBase/login.phtml @@ -15,6 +15,9 @@ <? if ($account->supportsCreation()): ?> <a class="new_account" href="<?=$this->url('myresearch-account')?>?auth_method=<?=$this->auth()->getActiveAuthMethod()?>"><?=$this->transEsc('Create New Account')?></a> <? endif; ?> + <? if ($account->supportsRecovery()): ?> + <a class="forgot_password" href="<?=$this->url('myresearch-recover')?>"><?=$this->transEsc('Forgot Password')?></a> + <? endif; ?> <? else: ?> <a href="<?=$this->escapeHtml($sessionInitiator)?>"><?=$this->transEsc("Institutional Login")?></a> <? endif; ?> diff --git a/themes/blueprint/templates/Auth/Database/newpassword.phtml b/themes/blueprint/templates/Auth/Database/newpassword.phtml new file mode 100644 index 00000000000..a5c74c252e1 --- /dev/null +++ b/themes/blueprint/templates/Auth/Database/newpassword.phtml @@ -0,0 +1,12 @@ +<? if (isset($this->username)): ?> + <label class="span-4"><?=$this->transEsc('Username') ?>:</label> + <input type="text" disabled value="<?=$this->username ?>"/><br/> +<? endif; ?> +<? if (isset($this->verifyold) && $this->verifyold || isset($this->oldpwd)): ?> + <label class="span-4"><?=$this->transEsc('old_password') ?>:</label> + <input type="password" name="oldpwd"/><br/> +<? endif; ?> +<label class="span-4"><?=$this->transEsc('new_password') ?>:</label> +<input type="password" name="password"/><br/> +<label class="span-4"><?=$this->transEsc('confirm_new_password') ?>:</label> +<input type="password" name="password2"/><br/> \ No newline at end of file diff --git a/themes/blueprint/templates/Auth/Database/recovery.phtml b/themes/blueprint/templates/Auth/Database/recovery.phtml new file mode 100644 index 00000000000..359cc4f965d --- /dev/null +++ b/themes/blueprint/templates/Auth/Database/recovery.phtml @@ -0,0 +1,7 @@ +<label class="span-4"><?=$this->transEsc('recovery_by_username') ?>:</label> +<input type="text" name="username"/> +<input type="submit" name="submit"/> +<br/><br/> +<label class="span-4"><?=$this->transEsc('recovery_by_email') ?>:</label> +<input type="email" name="email"/> +<input type="submit" name="submit"/> \ No newline at end of file diff --git a/themes/blueprint/templates/myresearch/menu.phtml b/themes/blueprint/templates/myresearch/menu.phtml index 2c316aadcd8..a1b6c496e0d 100644 --- a/themes/blueprint/templates/myresearch/menu.phtml +++ b/themes/blueprint/templates/myresearch/menu.phtml @@ -18,6 +18,14 @@ <? endif; ?> <li<?=$this->active == 'history' ? ' class="active"' : ''?>><a href="<?=$this->url('search-history')?>?require_login"><?=$this->transEsc('history_saved_searches')?></a></li> </ul> + <? if ($this->auth()->isLoggedIn() && $this->auth()->getManager()->supportsPasswordChange()): ?> + <h4 class="gear"><?=$this->transEsc('Preferences')?></h4> + <ul> + <li> + <a href="<?=$this->url('myresearch-changepassword') ?>"><?=$this->transEsc('Change Password') ?></a> + </li> + </ul> + <? endif; ?> <? if ($this->userlist()->getMode() !== 'disabled' && $user = $this->auth()->isLoggedIn()): ?> <h4 class="list"><?=$this->transEsc('Your Lists')?></h4> <ul> diff --git a/themes/blueprint/templates/myresearch/newpassword.phtml b/themes/blueprint/templates/myresearch/newpassword.phtml new file mode 100644 index 00000000000..41741c051c3 --- /dev/null +++ b/themes/blueprint/templates/myresearch/newpassword.phtml @@ -0,0 +1,22 @@ +<div class="<?=$this->layoutClass('mainbody')?>"> + <h2><?=$this->transEsc('Create New Password') ?></h2> + <?=$this->flashmessages() ?> + <? if (!$this->auth()->getManager()->supportsPasswordChange()): ?> + <div class="error"><?=$this->transEsc('recovery_new_disabled') ?></div> + <? elseif (!isset($this->hash)): ?> + <div class="error"><?=$this->transEsc('recovery_user_not_found') ?></div> + <? else: ?> + <form action="<?=$this->url('myresearch-newpassword') ?>" method="post"> + <?=$this->auth()->getNewPasswordForm() ?> + <input type="hidden" value="<?=$this->hash ?>" name="hash"/> + <input type="hidden" value="<?=$this->username ?>" name="username"/> + <input name="submit" type="submit"/> + </form> + <? endif; ?> +</div> + +<? if ($this->auth()->isLoggedIn()): ?> + <div class="<?=$this->layoutClass('sidebar')?>"> + <?=$this->context($this)->renderInContext("myresearch/menu.phtml", array('active' => 'newpassword'))?> + </div> +<? endif; ?> \ No newline at end of file diff --git a/themes/blueprint/templates/myresearch/recover.phtml b/themes/blueprint/templates/myresearch/recover.phtml new file mode 100644 index 00000000000..afffcc651e8 --- /dev/null +++ b/themes/blueprint/templates/myresearch/recover.phtml @@ -0,0 +1,9 @@ +<h2><?=$this->transEsc('Password Recovery') ?></h2> +<?=$this->flashmessages()?> +<? if (!$this->auth()->getManager()->supportsRecovery()): ?> + <div class="error"><?=$this->transEsc('recovery_disabled') ?></div> +<? else: ?> + <form action="" method="post"> + <?=$this->auth()->getPasswordRecoveryForm() ?> + </form> +<? endif; ?> \ No newline at end of file diff --git a/themes/bootprint/css/icons.css b/themes/bootprint/css/icons.css index 29565471d61..d5b276b922f 100644 --- a/themes/bootprint/css/icons.css +++ b/themes/bootprint/css/icons.css @@ -23,6 +23,7 @@ i.icon-home, i.icon-inbox, i.icon-leaf, i.icon-list-alt, +i.icon-lock, i.icon-minus-sign, i.icon-ok, i.icon-phone-sign, @@ -102,6 +103,7 @@ i.icon-user { .small i.icon-inbox, .small i.icon-leaf, .small i.icon-list-alt, +.small i.icon-lock, .small i.icon-minus-sign, .small i.icon-ok, .small i.icon-phone-sign, @@ -154,6 +156,7 @@ i.icon-home {background:url('../images/icons/house.png')} i.icon-inbox {background:url('../images/icons/box.png')} i.icon-leaf,.icon-sitemap {background:url('../images/icons/treeCurrent.png')} i.icon-list-alt,i.icon-export {background:url('../images/icons/application_add.png')} +i.icon-lock {background:url('../images/icons/lock.png')} i.icon-minus-sign {background:url('../images/icons/delete.png')} i.icon-ok {background:url('../images/icons/tick.png')} i.icon-phone-sign {background:url('../images/icons/phone.png')} diff --git a/themes/bootprint/images/icons/cog.png b/themes/bootprint/images/icons/cog.png new file mode 100644 index 0000000000000000000000000000000000000000..67de2c6ccbeac17742f56cf7391e72b2bf5033ba GIT binary patch literal 512 zcmV+b0{{JqP)<h;3K|Lk000e1NJLTq000mG000mO1ONa4wfZ;e00004XF*Lt006JZ zHwB960000PbVXQnQ*UN;cVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBUzl1W5CR4C6? zQB6w%VGv&Y0p9%y{*S$M5weR^mokV7i!Mcpk=YVa(4jh%5k+ND8>CQDsH?WF>AIFt zQuJ}i;w2$ZUU#3SZ6RY0Gw;kZ&ol1~2ky^QZ(fom$=jN<T*X<otG0ao1Md*)XSSIA z*x3T8gv)x7DUK|A#S3CA>JZt!z7w_pH~wdQ;R)Gh%BbQFCx+Nm!4SuS-vkr`vhhrX zM*>w%e+v~?m@q~ImPAgtLkR_3U<2F8LP3W5=LJ*ZN|S5p#sf4YFr$p~Q~Z*0Ngxf2 zjk#J#<7EAlhzlrV53~GF&pIzcCN_lz9@05Ue<MwWI-*!M0jvB0F~pIke2>oUXiK%N z#x+4o*i_c|6_Uu1+&TIho?3@y4k-#b8Y_o94zW*B3a1ne2-Y5s0uke$$|@=}OP-i= zNYZQA=>PrZu0MfSL=b8UhD_={W4IY1{b{)U)*gc45xtL%IYLY&hF;d`@GzI&7H&D# zh;z_BX$#hqh@q?AY3sJTod2%*Yd)_>YM0#q&ixGuh+PQsneK)F0000<MNUMnLSTXw CAKTXe literal 0 HcmV?d00001 diff --git a/themes/bootprint/images/icons/key_go.png b/themes/bootprint/images/icons/key_go.png new file mode 100644 index 0000000000000000000000000000000000000000..30b0dc316e52dba388d88112d4c1cc32672fffbb GIT binary patch literal 744 zcmV<E0vG*>P)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00004XF*Lt006JZ zHwB960000PbVXQnQ*UN;cVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU!dPzh<R5;6x zlU+!YVHk$LRU{on2NEG99t4gN<(k<vgSIj~$mk$~eksv@gmxl=Ow(;Fr^B3}b5Lr_ z2G&-MqzD~`qBeg)s5vYXON+MJpPSp<&)xfdS}mK_2lvVQa$nE=JkR@40I2o!$#_If zgcYe*-~X369QeQ}9^~I<-#CKyR)jn~<T%PeX7qx)>jGlUfiHCke$)P}4*HwX@?mb` zzgiE#M5fLDxtj=lZ6q=+Lkt2$!wyVqxC~@X03De&Gn$t$kdWJig()R`(|L#ldNHNi zi?tK@-;xA1zab1r3XkO&T;m5wdrx2~XU8>fpl4v~7ZF1h+!NXGy*rQ6jw}?mrNKGI zL&zzIrIL-VTRiLE`+jy5wt-SCISiy4pO}x6>V>$`&V{7&3{GoOF)87oTao_ek0H|L zXxL5q?8f4p7$RLZL=Q4>?LH3$EqhS@^b{VAG@wL(0y*{DquI)BECvw!(t<WU7OeSj z4slT2o&n>y8jr^s8DqzY3Mx|xw6AM%RhNVG>V(j48H=@2*+n87;k63kFsH&hc^Czx zU)p@TON5%2#gM-!LRIG_NS|MUrcZ`*_YPuLB^9Iro+W2D85SRofmAHM&xik`9B1#a z@o-oLow*L$!CJHqC<x>_n){?E(&Zwhf|^V!qqZ#X+&c)jMMwsg3*W31W<{FoWOGV1 zuOTTStWS(&DYr&0v}HowTZPN*IY_RcCU%rj3Cs*;4T3Shy&sFSmGFOV!%!{P*`>;C zSiN43jAg&56(U(ojS}<bU;n~z%OUZEdjI^WYM*^X$^G8flvN$?agoUOo#Ks1ETcBX ap8o(~AJmyDx~^sb0000<MNUMnLSTY)_fHN0 literal 0 HcmV?d00001 diff --git a/themes/bootprint/images/icons/lock.png b/themes/bootprint/images/icons/lock.png new file mode 100644 index 0000000000000000000000000000000000000000..2ebc4f6f9663e32cad77d67ef93ab8843dfea3c0 GIT binary patch literal 749 zcmV<J0uud+P)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00004XF*Lt006JZ zHwB960000PbVXQnQ*UN;cVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU!e@R3^R5;6R zQbB7IK^T3Tq)8K-7_~JtN==(+K|#b@JqUt5h~h!Lc~ek?Ku+zk2XpMN@Q@-NdiDp1 z1*uxlMiZNsG$@o5lP0D~c6MfcvuoP5I`A<w-+bSj_uh<Q+cvyoX=!OhDK#ghoMD_~ zfbo;DVp-N=E|>e|tv9>?g+k#9o0pTx<YX)sgU{y!_vrO{sMqV*;vmqy`T6;^e*oA# z!o!d0bUI_2CTg`BI-QQb9f3dqiA2JwD;A3z%w1ksSm^4#Z-B()v+?oqj1U6la(T1e zZl|~o>d@;_sq{kwlU;^VvV*?BV8P@}BoaZTQUROpWV6|-M`|^n&)=+8tHo3*<<$NU zU`%V~ZF;?hBSYsjJ6%JzV}E(D{pOLqQklliUf9um_tGl-wty`y*p?eYNW56P>X@1s zZs7KrRZKtmV7Lqj^5Fgr7_`LjhdJK@ltF&O`j7?*NUM$KvmNGz)3WjM?V$vHlP<J& zUm*}0g<*`aa0m#;nO4C59%Snq%<gw6YaijsENrvy0U$*veUpji`g`g;hWN#6sJ&if z|7lEIpGEWQIsqDprcRKsge^=jfN*5kq#B>T0AFyF?kLE<#HZabCSW3-o<y$`V(q@e zY5?H;1Doz@RIRn~d5tXI@x+4aDfGLfYLi*%3!3F^SFTb{&mjZ7(WsOVKc9j>a*6;Z zrXD`Ulwd<^2glP%1Y1Kc1Ij%DU^=ME(jKf6APNlA$Uu;J4bVilQHSWX5uJ$9Zsp4M z0%!@LvyTxz=Z6stxlichODIY+yNGt%RM;m`>H4LOKLFs9Y%b5aUN|2|{0Zw|<_~i} fmXz*V19AKYa<a0`hi}0}00000NkvXXu0mjf@;P6V literal 0 HcmV?d00001 diff --git a/themes/bootstrap/templates/Auth/AbstractBase/login.phtml b/themes/bootstrap/templates/Auth/AbstractBase/login.phtml index 1bee344cd83..539c1e484b9 100644 --- a/themes/bootstrap/templates/Auth/AbstractBase/login.phtml +++ b/themes/bootstrap/templates/Auth/AbstractBase/login.phtml @@ -6,10 +6,14 @@ <input type="hidden" name="auth_method" value="<?=$this->auth()->getActiveAuthMethod()?>"> <div class="control-group"> <div class="controls"> - <input class="btn btn-primary" type="submit" name="processLogin" value="Login"> <? if ($account->supportsCreation()): ?> <a class="btn btn-link createAccountLink" href="<?=$this->url('myresearch-account') ?>"><?=$this->transEsc('Create New Account')?></a> <? endif; ?> + <input class="btn btn-primary" type="submit" name="processLogin" value="Login"> + <? if ($account->supportsRecovery()): ?> + <br/> + <a class="btn btn-link" href="<?=$this->url('myresearch-recover') ?>"><?=$this->transEsc('Forgot Password')?></a> + <? endif; ?> </div> </div> </form> diff --git a/themes/bootstrap/templates/Auth/Database/newpassword.phtml b/themes/bootstrap/templates/Auth/Database/newpassword.phtml new file mode 100644 index 00000000000..40189de011f --- /dev/null +++ b/themes/bootstrap/templates/Auth/Database/newpassword.phtml @@ -0,0 +1,28 @@ +<? if (isset($this->username)): ?> + <div class="control-group"> + <label class="control-label"><?=$this->transEsc('Username') ?>:</label> + <div class="controls"> + <span class="input-xlarge uneditable-input"><?=$this->username ?></span> + </div> + </div> +<? endif; ?> +<? if (isset($this->verifyold) && $this->verifyold || isset($this->oldpwd)): ?> + <div class="control-group"> + <label class="control-label"><?=$this->transEsc('old_password') ?>:</label> + <div class="controls"> + <input type="password" name="oldpwd"/> + </div> + </div> +<? endif; ?> +<div class="control-group"> + <label class="control-label"><?=$this->transEsc('new_password') ?>:</label> + <div class="controls"> + <input type="password" name="password"/> + </div> +</div> +<div class="control-group"> + <label class="control-label"><?=$this->transEsc('confirm_new_password') ?>:</label> + <div class="controls"> + <input type="password" name="password2"/> + </div> +</div> \ No newline at end of file diff --git a/themes/bootstrap/templates/Auth/Database/recovery.phtml b/themes/bootstrap/templates/Auth/Database/recovery.phtml new file mode 100644 index 00000000000..56355213823 --- /dev/null +++ b/themes/bootstrap/templates/Auth/Database/recovery.phtml @@ -0,0 +1,14 @@ +<div class="control-group"> + <label class="control-label"><?=$this->transEsc('recovery_by_username') ?>:</label> + <div class="controls"> + <input type="text" name="username"/> + <input class="btn" name="submit" type="submit"/> + </div> +</div> +<div class="control-group"> + <label class="control-label"><?=$this->transEsc('recovery_by_email') ?>:</label> + <div class="controls"> + <input type="email" name="email"/> + <input class="btn" name="submit" type="submit"/> + </div> +</div> \ No newline at end of file diff --git a/themes/bootstrap/templates/myresearch/menu.phtml b/themes/bootstrap/templates/myresearch/menu.phtml index 157ac8fae80..da74118efaa 100644 --- a/themes/bootstrap/templates/myresearch/menu.phtml +++ b/themes/bootstrap/templates/myresearch/menu.phtml @@ -20,14 +20,20 @@ <li><a href="<?=$this->url('myresearch-logout')?>"><?=$this->transEsc("Log Out")?> <i class="icon-signout pull-right"></i></a></li> <? endif; ?> </ul> - <? if ($this->userlist()->getMode() !== 'disabled' && $user = $this->auth()->isLoggedIn()): ?> - <h4 class="list"><?=$this->transEsc('Your Lists')?></h4> - <ul class="nav nav-list"> - <li<?=$this->active == 'favorites' ? ' class="active"' : ''?>><a href="<?=$this->url('myresearch-favorites')?>"><?=$this->transEsc('Your Favorites')?> <i class="icon-star pull-right"></i></a></li> - <? $lists = $user->getLists() ?> - <? foreach ($lists as $list): ?> - <li<?=$this->active == 'list' . $list['id'] ? ' class="active"' : ''?>> <a href="<?=$this->url('userList', array('id' => $list['id']))?>"><?=$this->escapeHtml($list['title'])?> <span class="pull-right"><?=$list->cnt?></span></a></li> - <? endforeach; ?> - <li><a href="<?=$this->url('editList', array('id'=>'NEW'))?>" title="<?=$this->transEsc('Create a List') ?>"><?=$this->transEsc('Create a List') ?> <span class="pull-right"><i class="icon-plus"></i></span></a></li> - </ul> - <? endif ?> +<? if ($this->auth()->isLoggedIn() && $this->auth()->getManager()->supportsPasswordChange()): ?> + <h4><?=$this->transEsc('Preferences')?></h4> + <ul class="nav nav-list"> + <li<?=$this->active == 'newpassword' ? ' class="active"' : ''?>><a href="<?=$this->url('myresearch-changepassword') ?>"><?=$this->transEsc('Change Password') ?> <i class="icon-lock pull-right"></i></a></li> + </ul> +<? endif; ?> +<? if ($this->userlist()->getMode() !== 'disabled' && $user = $this->auth()->isLoggedIn()): ?> + <h4><?=$this->transEsc('Your Lists')?></h4> + <ul class="nav nav-list"> + <li<?=$this->active == 'favorites' ? ' class="active"' : ''?>><a href="<?=$this->url('myresearch-favorites')?>"><?=$this->transEsc('Your Favorites')?> <i class="icon-star pull-right"></i></a></li> + <? $lists = $user->getLists() ?> + <? foreach ($lists as $list): ?> + <li<?=$this->active == 'list' . $list['id'] ? ' class="active"' : ''?>> <a href="<?=$this->url('userList', array('id' => $list['id']))?>"><?=$this->escapeHtml($list['title'])?> <span class="pull-right"><?=$list->cnt?></span></a></li> + <? endforeach; ?> + <li><a href="<?=$this->url('editList', array('id'=>'NEW'))?>" title="<?=$this->transEsc('Create a List') ?>"><?=$this->transEsc('Create a List') ?> <span class="pull-right"><i class="icon-plus"></i></span></a></li> + </ul> +<? endif ?> diff --git a/themes/bootstrap/templates/myresearch/newpassword.phtml b/themes/bootstrap/templates/myresearch/newpassword.phtml new file mode 100644 index 00000000000..74951da80b7 --- /dev/null +++ b/themes/bootstrap/templates/myresearch/newpassword.phtml @@ -0,0 +1,26 @@ +<div class="<?=$this->layoutClass('mainbody')?>"> + <h2><?=$this->transEsc('Create New Password') ?></h2> + <?=$this->flashmessages() ?> + <? if (!$this->auth()->getManager()->supportsPasswordChange()): ?> + <div class="error"><?=$this->transEsc('recovery_new_disabled') ?></div> + <? elseif (!isset($this->hash)): ?> + <div class="error"><?=$this->transEsc('recovery_user_not_found') ?></div> + <? else: ?> + <form class="form-horizontal" action="<?=$this->url('myresearch-newpassword') ?>" method="post"> + <input type="hidden" value="<?=$this->hash ?>" name="hash"/> + <input type="hidden" value="<?=$this->username ?>" name="username"/> + <?=$this->auth()->getNewPasswordForm() ?> + <div class="control-group"> + <div class="controls"> + <input class="btn" name="submit" type="submit"/> + </div> + </div> + </form> + <? endif; ?> +</div> + +<? if ($this->auth()->isLoggedIn()): ?> + <div class="<?=$this->layoutClass('sidebar')?>"> + <?=$this->context($this)->renderInContext("myresearch/menu.phtml", array('active' => 'newpassword'))?> + </div> +<? endif; ?> \ No newline at end of file diff --git a/themes/bootstrap/templates/myresearch/profile.phtml b/themes/bootstrap/templates/myresearch/profile.phtml index 989b5862ff3..96dcb5cb94f 100644 --- a/themes/bootstrap/templates/myresearch/profile.phtml +++ b/themes/bootstrap/templates/myresearch/profile.phtml @@ -33,7 +33,7 @@ ? $this->profile['home_library'] : $this->defaultPickupLocation ?> <td> - <form method="post" action="" id="profile_form"> + <form id="profile_form" class="form-inline" action="" method="post"> <select id="home_library" name="home_library"> <? foreach ($this->pickup as $lib): ?> <option value="<?=$this->escapeHtml($lib['locationID'])?>"<?=($selected == $lib['locationID'])?' selected="selected"':''?>><?=$this->escapeHtml($lib['locationDisplay'])?></option> diff --git a/themes/bootstrap/templates/myresearch/recover.phtml b/themes/bootstrap/templates/myresearch/recover.phtml new file mode 100644 index 00000000000..e2c25e006b1 --- /dev/null +++ b/themes/bootstrap/templates/myresearch/recover.phtml @@ -0,0 +1,9 @@ +<h2><?=$this->transEsc('Password Recovery') ?></h2> +<?=$this->flashmessages()?> +<? if (!$this->auth()->getManager()->supportsRecovery()): ?> + <div class="error"><?=$this->transEsc('recovery_disabled') ?></div> +<? else: ?> + <form class="form-horizontal" action="" method="post"> + <?=$this->auth()->getPasswordRecoveryForm() ?> + </form> +<? endif; ?> \ No newline at end of file diff --git a/themes/jquerymobile/templates/Auth/AbstractBase/login.phtml b/themes/jquerymobile/templates/Auth/AbstractBase/login.phtml index e41194cc528..e7f616f8c31 100644 --- a/themes/jquerymobile/templates/Auth/AbstractBase/login.phtml +++ b/themes/jquerymobile/templates/Auth/AbstractBase/login.phtml @@ -15,6 +15,9 @@ <? if ($account->supportsCreation()): ?> <a rel="external" data-role="button" class="new_account" href="<?=$this->url('myresearch-account')?>?auth_method=<?=$this->auth()->getActiveAuthMethod()?>"><?=$this->transEsc('Create New Account')?></a> <? endif; ?> + <? if ($account->supportsRecovery()): ?> + <a rel="external" data-role="button" class="recover_password" href="<?=$this->url('myresearch-recover')?>"><?=$this->transEsc('Forgot Password')?></a> + <? endif; ?> <? else: ?> <a rel="external" data-role="button" href="<?=$this->escapeHtml($sessionInitiator)?>"><?=$this->transEsc("Institutional Login")?></a> <? endif; ?> diff --git a/themes/jquerymobile/templates/Auth/Database/newpassword.phtml b/themes/jquerymobile/templates/Auth/Database/newpassword.phtml new file mode 100644 index 00000000000..4c0b2d107fd --- /dev/null +++ b/themes/jquerymobile/templates/Auth/Database/newpassword.phtml @@ -0,0 +1,15 @@ +<div data-role="fieldcontain" class="ui-field-contain ui-body ui-br"> + <? if (isset($this->username)): ?> + <input type="hidden" name="username" value="<?=$this->username ?>"/> + <label class="ui-input-text"><?=$this->transEsc('Username') ?>:</label> + <input type="text" name="username" id="username" value="<?=$this->username ?>" disabled class="ui-input-text ui-body-c ui-corner-all ui-shadow-inset" style="border:1px solid #CCC;box-shadow:rgba(0, 0, 0, 0.1) 0px 1px 4px 0px inset;color:#777"/><br/> + <? endif; ?> + <? if (isset($this->verifyold) && $this->verifyold || isset($this->oldpwd)): ?> + <label for="oldpwd" class="ui-input-text"><?=$this->transEsc('old_password') ?>:</label> + <input type="password" name="oldpwd" id="oldpwd" class="ui-input-text ui-body-c ui-corner-all ui-shadow-inset"/><br/> + <? endif; ?> + <label for="password" class="ui-input-text"><?=$this->transEsc('new_password') ?>:</label> + <input type="password" name="password" id="password" class="ui-input-text ui-body-c ui-corner-all ui-shadow-inset"/><br/> + <label for="password2" class="ui-input-text"><?=$this->transEsc('confirm_new_password') ?>:</label> + <input type="password" name="password2" id="password2" class="ui-input-text ui-body-c ui-corner-all ui-shadow-inset"/><br/> +</div> \ No newline at end of file diff --git a/themes/jquerymobile/templates/Auth/Database/recovery.phtml b/themes/jquerymobile/templates/Auth/Database/recovery.phtml new file mode 100644 index 00000000000..b1503fb4a54 --- /dev/null +++ b/themes/jquerymobile/templates/Auth/Database/recovery.phtml @@ -0,0 +1,10 @@ +<div class="ui-grid-b"> + <div class="ui-block-a"><label><?=$this->transEsc('recovery_by_username') ?>:</label></div> + <div class="ui-block-b"><input type="text" name="username" style="margin-top:.5em;height:28px"/></div> + <div class="ui-block-c"><input type="submit" name="submit" value="<?=$this->transEsc('Submit') ?>"/></div> +</div> +<div class="ui-grid-b"> + <div class="ui-block-a"><label><?=$this->transEsc('recovery_by_email') ?>:</label></div> + <div class="ui-block-b"><input type="email" name="email" style="margin-top:.5em;height:28px"/></div> + <div class="ui-block-c"><input type="submit" name="submit" value="<?=$this->transEsc('Submit') ?>"/></div> +</div> \ No newline at end of file diff --git a/themes/jquerymobile/templates/myresearch/footer-navbar.phtml b/themes/jquerymobile/templates/myresearch/footer-navbar.phtml index aec903518b6..624e3b6788a 100644 --- a/themes/jquerymobile/templates/myresearch/footer-navbar.phtml +++ b/themes/jquerymobile/templates/myresearch/footer-navbar.phtml @@ -5,6 +5,9 @@ <li><a rel="external" <?=$this->layout()->templateName=="mylist" ? ' class="ui-btn-active"' : ''?> href="<?=$this->url('myresearch-favorites')?>"><?=$this->transEsc('Favorites')?></a></li> <? endif; ?> <li><a rel="external" <?=$this->layout()->templateName=="history" ? ' class="ui-btn-active"' : ''?> href="<?=$this->url('search-history')?>?require_login"><?=$this->transEsc('History')?></a></li> + <? if ($this->auth()->getManager()->supportsPasswordChange()): ?> + <li><a rel="external" href="<?=$this->url('myresearch-changepassword')?>"><?=$this->transEsc("Change Password")?></a></li> + <? endif; ?> <li><a rel="external" href="<?=$this->url('myresearch-logout')?>"><?=$this->transEsc("Log Out")?></a></li> </ul> </div> diff --git a/themes/jquerymobile/templates/myresearch/newpassword.phtml b/themes/jquerymobile/templates/myresearch/newpassword.phtml new file mode 100644 index 00000000000..9a8e1ecbaaa --- /dev/null +++ b/themes/jquerymobile/templates/myresearch/newpassword.phtml @@ -0,0 +1,29 @@ +<? + // Set up page title: + $this->headTitle(isset($list) ? $list->title : $this->translate('Create New Password')); + + // Set up extra button for header: + $extraButton = '<a rel="external" href="' + . $this->url('myresearch-home') + . '" data-icon="back" class="ui-btn-left">' + . $this->transEsc('My Profile') + . '</a>'; +?> +<div data-role="page" id="MyResearch-newpassword" class="newpassword"> + <?=$this->mobileMenu()->header(array('extraButtons'=>array($extraButton))) ?> + <div data-role="content"> + <?=$this->flashmessages() ?> + <? if (!$this->auth()->getManager()->supportsPasswordChange()): ?> + <div class="error"><?=$this->transEsc('recovery_new_disabled') ?></div> + <? elseif (!isset($this->hash)): ?> + <div class="error"><?=$this->transEsc('recovery_user_not_found') ?></div> + <? else: ?> + <form data-ajax="false" action="<?=$this->url('myresearch-newpassword') ?>" method="post"> + <?=$this->auth()->getNewPasswordForm() ?> + <input type="hidden" value="<?=$this->hash ?>" name="hash"/> + <input type="submit" name="submit" value="<?=$this->transEsc('Submit') ?>"/> + </form> + <? endif; ?> + </div> + <?=$this->mobileMenu()->footer() ?> +</div> \ No newline at end of file diff --git a/themes/jquerymobile/templates/myresearch/recover.phtml b/themes/jquerymobile/templates/myresearch/recover.phtml new file mode 100644 index 00000000000..4e73da0009c --- /dev/null +++ b/themes/jquerymobile/templates/myresearch/recover.phtml @@ -0,0 +1,27 @@ +<? + // Set up page title: + $this->headTitle(isset($list) ? $list->title : $this->translate('recovery_title')); + + // Set up extra button for header: + $extraButton = '<a rel="external" href="' + . $this->url('myresearch-home') + . '" data-icon="back" class="ui-btn-left">'; + $extraButton .= $this->auth()->isLoggedIn() + ? $this->transEsc('My Profile') + : $this->transEsc('Login'); + $extraButton .= '</a>'; +?> +<div data-role="page" id="MyResearch-recover" class="results-page"> + <?=$this->mobileMenu()->header(array('extraButtons'=>array($extraButton))) ?> + <div data-role="content"> + <?=$this->flashmessages()?> + <? if (!$this->auth()->getManager()->supportsRecovery()): ?> + <div class="error"><?=$this->transEsc('recovery_disabled') ?></div> + <? else: ?> + <form data-ajax="false" action="" method="post"> + <?=$this->auth()->getPasswordRecoveryForm() ?> + </form> + <? endif; ?> + </div> + <?=$this->mobileMenu()->footer() ?> +</div> \ No newline at end of file diff --git a/themes/root/templates/Email/recover-password.phtml b/themes/root/templates/Email/recover-password.phtml new file mode 100644 index 00000000000..6037d640e28 --- /dev/null +++ b/themes/root/templates/Email/recover-password.phtml @@ -0,0 +1,3 @@ +<?=$this->translate('recovery_email_notification', array('%%library%%' => $this->library)) ?> + +<?=$this->translate('recovery_email_url_pretext', array('%%url%%' => $this->url)) ?> \ No newline at end of file -- GitLab