From 703caa09da94f63432c873d6babda5717288d411 Mon Sep 17 00:00:00 2001 From: Blou Date: Fri, 20 Jun 2014 11:36:43 -0400 Subject: [PATCH] Bug 8753 - Add forgot password link to OPAC This redo includes the following - usage of the template letters for the email instead of a .tt (that feature didn't exist when the functionnality was initially coded for our client) - removal of the prog version - removal of the code that was initially put into Members.pm - Added a letter into updatedatabase.pl and sample_notices.sql - Of course, rebase to latest master. The rest remain unchanged since the previous comments/approvals. As such, what worked before should still work. TEST PLAN: 1) apply the patch 2) go to system preferences OPAC>>Privacy and set 'OpacResetPassword' to ON. That will cause the link 'Forgot yo 2b) make sure that OpacPasswordChange is also ON. 3) refresh front page, click on 'Forgot your password' and enter a VALID address (one that is associated to an en 3b) Also try an INVALID address (valid yet not in your koha db). An error message will show up. 4) An email should be received at that address with a link. 5) Follow the link in the mail to fill the new password. Until a satisfactory new password is entered, the old password is not reset. 6) Go to main page try the new password. --- .../data/mysql/en/mandatory/sample_notices.sql | 3 + installer/data/mysql/kohastructure.sql | 11 + installer/data/mysql/sysprefs.sql | 1 + installer/data/mysql/updatedatabase.pl | 18 ++ .../prog/en/modules/admin/preferences/opac.pref | 8 + .../opac-tmpl/bootstrap/en/includes/masthead.inc | 4 + .../opac-tmpl/bootstrap/en/modules/opac-auth.tt | 3 + .../bootstrap/en/modules/opac-password-recovery.tt | 132 +++++++++++ opac/opac-password-recovery.pl | 231 ++++++++++++++++++++ 9 files changed, 411 insertions(+) create mode 100644 koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-password-recovery.tt create mode 100755 opac/opac-password-recovery.pl diff --git a/installer/data/mysql/en/mandatory/sample_notices.sql b/installer/data/mysql/en/mandatory/sample_notices.sql index 2281739..3fa39a5 100644 --- a/installer/data/mysql/en/mandatory/sample_notices.sql +++ b/installer/data/mysql/en/mandatory/sample_notices.sql @@ -142,3 +142,6 @@ Thank you. Your library.' ); + +INSERT INTO `letter` VALUES ('members','PASSWORD_RESET','','Online password reset',1,'Koha password recovery','\r\n

This email has been sent in response to your password recovery request for the account << borrowers.userid>>.\r\n

\r\n

\r\nYou can now create your new password using the following link:\r\n
>\"><>\r\n

\r\n

This link will be valid for 2 days from this email\'s reception, then you must reapply if you do not change your password.

\r\n

Thank you.

\r\n\r\n','email'); + diff --git a/installer/data/mysql/kohastructure.sql b/installer/data/mysql/kohastructure.sql index b46dc6a..1389c89 100644 --- a/installer/data/mysql/kohastructure.sql +++ b/installer/data/mysql/kohastructure.sql @@ -3422,6 +3422,17 @@ CREATE TABLE IF NOT EXISTS `misc_files` ( -- miscellaneous files attached to rec KEY `record_id` (`record_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +-- Table structure for table 'borrower_password_recovery' +-- this stores the unique ID sent by email to the patron, for future validation +-- + +CREATE TABLE IF NOT EXISTS borrower_password_recovery ( + borrowernumber int(11) NOT NULL, + uuid varchar(128) NOT NULL, + valid_until timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + KEY borrowernumber (borrowernumber) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; diff --git a/installer/data/mysql/sysprefs.sql b/installer/data/mysql/sysprefs.sql index 5e4ce7c..03815b1 100644 --- a/installer/data/mysql/sysprefs.sql +++ b/installer/data/mysql/sysprefs.sql @@ -270,6 +270,7 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` ('opacreadinghistory','1','','If ON, enables display of Patron Circulation History in OPAC','YesNo'), ('OpacRenewalAllowed','0',NULL,'If ON, users can renew their issues directly from their OPAC account','YesNo'), ('OpacRenewalBranch','checkoutbranch','itemhomebranch|patronhomebranch|checkoutbranch|null','Choose how the branch for an OPAC renewal is recorded in statistics','Choice'), +('OpacResetPassword','1','','Shows the \'Forgot your password?\' link in the OPAC','YesNo'), ('OPACResultsSidebar','','70|10','Define HTML to be included on the search results page, underneath the facets sidebar','Textarea'), ('OPACSearchForTitleIn','
  • Other Libraries (WorldCat)
  • \n
  • Other Databases (Google Scholar)
  • \n
  • Online Stores (Bookfinder.com)
  • \n
  • Open Library (openlibrary.org)
  • ','70|10','Enter the HTML that will appear in the \'Search for this title in\' box on the detail page in the OPAC. Enter {TITLE}, {AUTHOR}, or {ISBN} in place of their respective variables in the URL. Leave blank to disable \'More Searches\' menu.','Textarea'), ('OpacSeparateHoldings','0',NULL,'Separate current branch holdings from other holdings (OPAC)','YesNo'), diff --git a/installer/data/mysql/updatedatabase.pl b/installer/data/mysql/updatedatabase.pl index c68ba51..a70ab8e 100755 --- a/installer/data/mysql/updatedatabase.pl +++ b/installer/data/mysql/updatedatabase.pl @@ -8560,6 +8560,24 @@ if ( CheckVersion($DBversion) ) { SetVersion($DBversion); } +$DBversion = "3.17.00.XXX"; +if ( CheckVersion($DBversion) ) { + $dbh->do("INSERT INTO systempreferences (variable,value,options,explanation,type) VALUES ('OpacResetPassword', '1','','Shows the ''Forgot your password?'' link in the OPAC','YesNo')"); + $dbh->do(q{ + CREATE TABLE IF NOT EXISTS borrower_password_recovery ( + borrowernumber int(11) NOT NULL, + uuid varchar(128) NOT NULL, + valid_until timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + KEY borrowernumber (borrowernumber) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + }); + $dbh->do(q{ + INSERT INTO `letter` (module, code, branchcode, name, is_html, title, content, message_transport_type) VALUES ('members','PASSWORD_RESET','','Online password reset',1,'Koha password recovery','\r\n

    This email has been sent in response to your password recovery request for the account << borrowers.userid>>.\r\n

    \r\n

    \r\nYou can now create your new password using the following link:\r\n
    >\"><>\r\n

    \r\n

    This link will be valid for 2 days from this email\'s reception, then you must reapply if you do not change your password.

    \r\n

    Thank you.

    \r\n\r\n','email') + }); + print "Upgrade to $DBversion done (Bug 8753: Add forgot password link to OPAC)\n"; + SetVersion ($DBversion); +} + =head1 FUNCTIONS =head2 TableExists($table) diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref index 1304cef..1834260 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref @@ -338,6 +338,14 @@ OPAC: no: "Don't allow" - patrons to change their own password on the OPAC. Note that this must be off to use LDAP authentication. - + - "The user " + - pref: OpacResetPassword + default: 1 + choices: + yes: "can reset" + no: "can not reset" + - " their password on OPAC." + - - pref: OPACPatronDetails choices: yes: Allow diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc index d17f893..6e39e82 100644 --- a/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc +++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc @@ -283,6 +283,9 @@ [% IF PatronSelfRegistration && PatronSelfRegistrationDefaultCategory %]

    Don't have an account? Register here.

    [% END %] + [% IF Koha.Preference('OpacPasswordChange') && Koha.Preference('OpacResetPassword') %] +

    Forgot your password?

    + [% END %] + diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt index f2196dd..b58e8ef 100644 --- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt +++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt @@ -94,6 +94,9 @@ + [% IF Koha.Preference('OpacPasswordChange') && Koha.Preference('OpacResetPassword') %] +

    Forgot your password?

    + [% END %]
    diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-password-recovery.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-password-recovery.tt new file mode 100644 index 0000000..8dd44aa --- /dev/null +++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-password-recovery.tt @@ -0,0 +1,132 @@ +[% USE Koha %] +[% INCLUDE 'doc-head-open.inc' %] +[% IF (LibraryNameTitle) %][% LibraryNameTitle %][% ELSE %]Koha online[% END %] catalog › +[% INCLUDE 'doc-head-close.inc' %] +[% BLOCK cssinclude %][% END %] +[% BLOCK jsinclude %] + +[% END %] + + + +
    +
    +[% INCLUDE 'masthead.inc' %] +
    +
    +
    +
    +[% IF (!Koha.Preference('OpacResetPassword')) %] +
    You can't reset your password.
    +[% ELSIF (password_recovery) %] + [% IF (hasError) %] + + [% IF (sendmailError) %] + An error has occured while sending you the password recovery link. +
    Please try again later. + [% ELSIF (errNoEmailFound) %] + No account was found with the email address "[% email %]" +
    Check if you typed it correctly. + [% ELSIF (errTooManyEmailFound) %] + More than one account has been found for the email address: "[% email %]" +
    Try to use an alternative email if you have one. + [% ELSIF (errAlreadyStartRecovery) %] + The process of password recovery has already started for this account ("[% email %]") +
    Check your emails; you should receive the link to reset your password. +
    If you did not receive it, click here to get a new password recovery link + [% END %] +

    Please contact the staff if you need further assistance. +
    + [% END %] +
    +
    + +
    + Password recovery form: +

    To reset your password, enter your email address. +
    A link to reset your password will be sent at this address.

    +
      +
    1. +
    +
    + +
    +
    +
    +
    + +[% ELSIF (new_password) %] + [% IF (errLinkNotValid) %] +
    + We could not authenticate you as the account owner. +
    Be sure to use the link you received in your email. +
    + [% ELSE %] + [% IF (hasError) %] + + [% IF (errPassNotMatch) %] + The passwords entered does not match. +
    Please try again. + [% ELSIF (errPassTooShort) %] + The password is too short. +
    The password must contain at least [% minPassLength %] characters. + [% END %] +
    + [% END %] +
    +
    + +
    + Password recovery form: +

    The password must contain at least [% minPassLength %] characters.

    +
      +
    1. +
    2. +
    +
    + + + +
    +
    +
    +
    + [% END %] +[% ELSIF (mail_sent) %] +

    A mail has been sent to "[% email %]". +
    It contains a link to create a new password. +
    This link will be valid for 2 days starting now.

    +
    Click here to return to the main page. +[% ELSIF (password_reset_done) %] +

    The password has been changed for user "[% username %]". +
    You can now login using this form.

    +[% END %] +
    +
    +
    +
    +
    +
    + [% INCLUDE 'usermenu.inc' %] +
    +
    +
    +[% INCLUDE 'opac-bottom.inc' %] +
    diff --git a/opac/opac-password-recovery.pl b/opac/opac-password-recovery.pl new file mode 100755 index 0000000..a1d9cc0 --- /dev/null +++ b/opac/opac-password-recovery.pl @@ -0,0 +1,231 @@ +#!/usr/bin/perl + +use strict; +use Modern::Perl; +use CGI; + +use C4::Auth; +use C4::Koha; +use C4::Members qw(changepassword GetMember GetMemberDetails ); +use C4::Output; +use C4::Context; +use Koha::AuthUtils qw(hash_password); + +my $query = new CGI; + +my ( $template, $dummy, $cookie ) = get_template_and_user( + { + template_name => "opac-password-recovery.tmpl", + query => $query, + type => "opac", + authnotrequired => 1, + debug => 1, + } +); + +my $email = $query->param('email') // q{}; +my $password = $query->param('password'); +my $repeatPassword = $query->param('repeatPassword'); +my $minPassLength = C4::Context->preference('minPasswordLength'); +my $id = $query->param('id'); +my $uniqueKey = $query->param('uniqueKey'); +my $username = $query->param('username'); +my $borrower_number; + +#errors +my $hasError; + +#email form error +my $errNoEmailFound; +my $errAlreadyStartRecovery; + +#new password form error +my $errLinkNotValid; +my $errPassNotMatch; +my $errPassTooShort; + +my $dbh = C4::Context->dbh; + +if ( $query->param('sendEmail') || $query->param('resendEmail') ) { + #send mail + confirmation + + #try with the main email + $email ||= ''; # avoid undef + my $borrower_infos = GetMember( email => $email ); + $borrower_infos = GetMember( emailpro => $email ) unless $borrower_infos; + $borrower_infos = GetMember( B_email => $email ) unless $borrower_infos; + if($borrower_infos) { + $borrower_number = $borrower_infos->{'borrowernumber'}; + } + + if ( !$email || !$borrower_number ) { + $hasError = 1; + $errNoEmailFound = 1; + } + elsif ( !$query->param('resendEmail') ) { + my $sth = $dbh->prepare( +"SELECT borrowernumber FROM borrower_password_recovery WHERE NOW() < valid_until AND borrowernumber = ?" + ); + $sth->execute($borrower_number); + if ( my $already = $sth->fetchrow ) { + $hasError = 1; + $errAlreadyStartRecovery = 1; + } + } + + if ($hasError) { + $template->param( + hasError => 1, + errNoEmailFound => $errNoEmailFound, + errAlreadyStartRecovery => $errAlreadyStartRecovery, + password_recovery => 1, + email => HTML::Entities::encode($email), + ); + } + elsif ( SendPasswordRecoveryEmail( $borrower_infos, $email, $query, $query->param('resendEmail') ) ) {#generate uuid and send recovery email + $template->param( + mail_sent => 1, + email => $email + ); + } + else {# if it doesnt work.... + $template->param( + password_recovery => 1, + sendmailError => 1 + ); + } +} +elsif ( $query->param('passwordReset') ) { + #new password form + #check if the link is still valid + my $sth = $dbh->prepare( + "SELECT borrower_password_recovery.borrowernumber, userid + FROM borrower_password_recovery, borrowers + WHERE borrowers.borrowernumber = borrower_password_recovery.borrowernumber + AND NOW() < valid_until + AND uuid = ?" + ); + $sth->execute($uniqueKey); + ( $borrower_number, $username ) = $sth->fetchrow; + + #validate password length & match + if ( ($borrower_number) + && ( $password eq $repeatPassword ) + && ( length($password) >= $minPassLength ) ) + { #apply changes + changepassword( $username, $borrower_number, hash_password($password) ); + + #remove entry + my $sth = $dbh->prepare("DELETE FROM borrower_password_recovery WHERE uuid = ? or NOW() > valid_until"); + $sth->execute($uniqueKey); + + $template->param( + password_reset_done => 1, + username => $username + ); + } + else { #errors + if ( !$borrower_number ) { #parameters not valid + $errLinkNotValid = 1; + } + elsif ( $password ne $repeatPassword ) { #passwords does not match + $errPassNotMatch = 1; + } + elsif ( length($password) < $minPassLength ) { #password too short + $errPassTooShort = 1; + } + $template->param( + new_password => 1, + minPassLength => $minPassLength, + email => $email, + uniqueKey => $uniqueKey, + errLinkNotValid => $errLinkNotValid, + errPassNotMatch => $errPassNotMatch, + errPassTooShort => $errPassTooShort, + hasError => 1 + ); + } +} +elsif ($uniqueKey) { #reset password form + #check if the link is valid + my $sth = $dbh->prepare( + "SELECT borrower_password_recovery.borrowernumber, userid + FROM borrower_password_recovery, borrowers + WHERE borrowers.borrowernumber = borrower_password_recovery.borrowernumber + AND NOW() < valid_until + AND uuid = ?" + ); + $sth->execute($uniqueKey); + ( $borrower_number, $username ) = $sth->fetchrow; + if ( !$borrower_number ) { + $errLinkNotValid = 1; + } +warn "INLIBRO username $username"; + $template->param( + new_password => 1, + minPassLength => $minPassLength, + email => $email, + uniqueKey => $uniqueKey, + username => $username, + errLinkNotValid => $errLinkNotValid + ); +} +else { #password recovery form (to send email) + $template->param( password_recovery => 1 ); +} + +output_html_with_http_headers $query, $cookie, $template->output; + +# +# It creates an email using the templates and send it to the user, using the specified email +# +sub SendPasswordRecoveryEmail { + my $borrower = shift; # from GetMember + my $userEmail = shift; #to_address (the one specified in the request) + my $query = shift; #only required to determine if 'http' or 'https' + my $update = shift; + + my $dbh = C4::Context->dbh; + + # generate UUID + my @chars = ("A".."Z", "a".."z", "0".."9"); + my $uuid_str; + $uuid_str .= $chars[rand @chars] for 1..32; + + # insert into database + my $expirydate = DateTime->now(time_zone => C4::Context->tz())->add( days => 2 ); + if($update){ + my $sth = $dbh->prepare( 'UPDATE borrower_password_recovery set uuid=?, valid_until=? where borrowernumber=? '); + $sth->execute($uuid_str, $expirydate->datetime(), $borrower->{'borrowernumber'}); + } else { + my $sth = $dbh->prepare( 'INSERT INTO borrower_password_recovery VALUES (?, ?, ?)'); + $sth->execute($borrower->{'borrowernumber'}, $uuid_str, $expirydate->datetime()); + } + + # create link + my $protocol = $query->https() ? "https://" : "http://"; + my $uuidLink = $protocol . C4::Context->preference( 'OPACBaseURL' ) . "/cgi-bin/koha/opac-password-recovery.pl?uniqueKey=$uuid_str"; + + # prepare the email + my $letter = C4::Letters::GetPreparedLetter ( + module => 'members', + letter_code => 'PASSWORD_RESET', + branchcode => $borrower->{branchcode}, + tables => {borrowers => $borrower}, + substitute => {passwordreseturl => $uuidLink}, + ); + + # define to/from emails + my $kohaEmail = C4::Context->preference( 'KohaAdminEmailAddress' ); # from + + C4::Letters::EnqueueLetter( { + letter => $letter, + borrowernumber => $borrower->{'borrowernumber'}, + to_address => $userEmail, + from_address => $kohaEmail, + message_transport_type => 'email', + } ); + + return 1; +} + -- 1.7.9.5