@@ -, +, @@ - 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. Until a satisfactory new password is entered, the old password is not reset. --- .../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 --- a/installer/data/mysql/en/mandatory/sample_notices.sql +++ a/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'); + --- a/installer/data/mysql/kohastructure.sql +++ a/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 */; --- a/installer/data/mysql/sysprefs.sql +++ a/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'), --- a/installer/data/mysql/updatedatabase.pl +++ a/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) --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref +++ a/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 --- a/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc +++ a/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 %] + --- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt +++ a/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 %]
    --- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-password-recovery.tt +++ a/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' %] +
    --- a/opac/opac-password-recovery.pl +++ a/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; +} + --