From d5abcbc8f3afef9272a93b455e3e8516c833067a Mon Sep 17 00:00:00 2001 From: Maxime Beaulieu Date: Wed, 11 Mar 2015 13:52:59 -0400 Subject: [PATCH] Bug 8753 - Add forgot password link to OPAC I've addressed a lot of Liz Rea's points. 1. I have moved the code from updatedatabase.pl and kohastructure.sql to a file in the atomicupdates directory. 1a. The feature is now off by default when the atomicupdate is run. 2. The password reset link is now visible on the home page, in the modal box and on opac-user.pl . 3. The password recovery pages now use bootstrap markup. 4. I am unsure here. I see "New Password:" and "Confirm new password:". 5. This should still work :). 6. I could not reproduce. 7. I have added the userid field. You can now reset the password by submitting either your useid or email address. Both fields can be filled, but the email address must be one of the borrower's (email, emailpro or b_email). When entering only the email address and two borrowers use that same address, the system tells the user to try with another address or to specify his userid. 8. The text is in the atomicupdate file. Have at it, anyone. Concerning the email. It is inconvenient for the use to have to wait X minutes for the message queue the be processed. Maybe we could add a sub in Letters.pm that: Takes the same argments as EnqueueLetter Sends the letter. Saves the letter in the message queue with a 'sent' status. TEST PLAN: Setup) 1) apply the patch 2) go to system preferences OPAC>>Privacy and set 'OpacResetPassword' to ON. 2b) make sure that OpacPasswordChange is also ON. A) 1) refresh front page, click on 'Forgot your password' and enter a VALID address 1b) Also try an INVALID address (valid yet not in your koha db). An error message will show up. 2) An email should be received at that address with a link. 3) Follow the link in the mail to fill the new password. Until a satisfactory new password is entered, the old password is not reset. 4) Go to main page try the new password. B) 1) Repeat the password reset, this time use the userid (username) field. 2) Try to reset the password using a userid and an email not linked to the account. An error appears. 3) Make sure the borrower has many available email addresses. 4) For each email, reset the password using both the userid and the email. The link should be sent to the specified address C) 1) Make sure two borrowers use the same email. 2) Repeat the reset procedure in test case A). An error message appears http://bugs.koha-community.org/show_bug.cgi?id=13068 Author: Maxime Beaulieu Followed test plan. Works as described. Signed-off-by: Marc Veron New sign-off after testing all patches together Signed-off-by: Marc Veron Signed-off-by: Marcel de Rooy --- C4/Passwordrecovery.pm | 173 +++++++++++++++++ .../Schema/Result/BorrowerPasswordRecovery.pm | 66 +++++++ ..._8753-Add_forgot_password_link_to_OPAC.sql | 12 ++ .../mysql/en/mandatory/sample_notices.sql | 4 + .../en/modules/admin/preferences/opac.pref | 8 + .../bootstrap/en/includes/masthead.inc | 6 + .../bootstrap/en/modules/opac-auth.tt | 6 + .../bootstrap/en/modules/opac-main.tt | 6 + .../en/modules/opac-password-recovery.tt | 134 +++++++++++++ opac/opac-password-recovery.pl | 179 ++++++++++++++++++ 10 files changed, 594 insertions(+) create mode 100644 C4/Passwordrecovery.pm create mode 100644 Koha/Schema/Result/BorrowerPasswordRecovery.pm create mode 100644 installer/data/mysql/atomicupdate/bug_8753-Add_forgot_password_link_to_OPAC.sql 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/C4/Passwordrecovery.pm b/C4/Passwordrecovery.pm new file mode 100644 index 0000000000..67b5695841 --- /dev/null +++ b/C4/Passwordrecovery.pm @@ -0,0 +1,173 @@ +package C4::Passwordrecovery; + +# Copyright 2014 Solutions InLibro inc. +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; +use C4::Context; + +use vars qw($VERSION @ISA @EXPORT); + +BEGIN { + # set the version for version checking + $VERSION = 3.07.00.049; + require Exporter; + @ISA = qw(Exporter); + push @EXPORT, qw( + &ValidateBorrowernumber + &SendPasswordRecoveryEmail + &GetValidLinkInfo + &CompletePasswordRecovery + ); +} + +=head1 NAME + +C4::Passwordrecovery - Koha password recovery module + +=head1 SYNOPSIS + +use C4::Passwordrecovery; + +=head1 FUNCTIONS + +=head2 ValidateBorrowernumber + +$alread = ValidateBorrowernumber( $borrower_number ); + +Check if the system already start recovery + +Returns true false + +=cut + +sub ValidateBorrowernumber { + my ($borrower_number) = @_; + my $schema = Koha::Database->new->schema; + + my $rs = $schema->resultset('BorrowerPasswordRecovery')->search( + { + borrowernumber => $borrower_number, + valid_until => \'> NOW()' + }, { + columns => 'borrowernumber' + }); + + if ($rs->next){ + return 1; + } + + return 0; +} + +=head2 GetValidLinkInfo + + Check if the link is still valid and return some info. + +=cut + +sub GetValidLinkInfo { + my ($uniqueKey) = @_; + my $dbh = C4::Context->dbh; + my $query = ' + SELECT borrower_password_recovery.borrowernumber, userid + FROM borrower_password_recovery, borrowers + WHERE borrowers.borrowernumber = borrower_password_recovery.borrowernumber + AND NOW() < valid_until + AND uuid = ? + '; + my $sth = $dbh->prepare($query); + $sth->execute($uniqueKey); + return $sth->fetchrow; +} + +=head2 SendPasswordRecoveryEmail + + It creates an email using the templates and send it to the user, using the specified email + +=cut + +sub SendPasswordRecoveryEmail { + my $borrower = shift; # from GetMember + my $userEmail = shift; #to_address (the one specified in the request) + my $protocol = shift; #only required to determine if 'http' or 'https' + my $update = shift; + + my $schema = Koha::Database->new->schema; + + # 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 $rs = $schema->resultset('BorrowerPasswordRecovery')->search( + { + borrowernumber => $borrower->{'borrowernumber'}, + }); + $rs->update({uuid => $uuid_str, valid_until => $expirydate->datetime()}); + } else { + my $rs = $schema->resultset('BorrowerPasswordRecovery')->create({ + borrowernumber=>$borrower->{'borrowernumber'}, + uuid => $uuid_str, + valid_until=> $expirydate->datetime() + }); + } + + # create link + 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}, + substitute => {passwordreseturl => $uuidLink, user => $borrower->{userid} }, + ); + + # 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; +} +=head2 CompletePasswordRecovery + + $bool = CompletePasswordRevovery($uuid); + + Deletes a password recovery entry. + +=cut +sub CompletePasswordRecovery{ + my $uniqueKey = shift; + my $model = Koha::Database->new->schema->resultset('BorrowerPasswordRecovery'); + my $entry = $model->search({-or => [uuid => $uniqueKey, valid_until => \'< NOW()']}); + return $entry->delete(); +} + +END { } # module clean-up code here (global destructor) + +1; \ No newline at end of file diff --git a/Koha/Schema/Result/BorrowerPasswordRecovery.pm b/Koha/Schema/Result/BorrowerPasswordRecovery.pm new file mode 100644 index 0000000000..5b41fbf6e6 --- /dev/null +++ b/Koha/Schema/Result/BorrowerPasswordRecovery.pm @@ -0,0 +1,66 @@ +use utf8; +package Koha::Schema::Result::BorrowerPasswordRecovery; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +=head1 NAME + +Koha::Schema::Result::BorrowerPasswordRecovery + +=cut + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=head1 TABLE: C + +=cut + +__PACKAGE__->table("borrower_password_recovery"); + +=head1 ACCESSORS + +=head2 borrowernumber + + data_type: 'integer' + is_nullable: 0 + +=head2 uuid + + data_type: 'varchar' + is_nullable: 0 + size: 128 + +=head2 valid_until + + data_type: 'timestamp' + datetime_undef_if_invalid: 1 + default_value: current_timestamp + is_nullable: 0 + +=cut + +__PACKAGE__->add_columns( + "borrowernumber", + { data_type => "integer", is_nullable => 0 }, + "uuid", + { data_type => "varchar", is_nullable => 0, size => 128 }, + "valid_until", + { + data_type => "timestamp", + datetime_undef_if_invalid => 1, + default_value => \"current_timestamp", + is_nullable => 0, + }, +); + + +# Created by DBIx::Class::Schema::Loader v0.07039 @ 2014-11-03 12:08:20 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ORAWxUHIkefSfPSqrcfeXA + + +# You can replace this text with custom code or comments, and it will be preserved on regeneration +1; diff --git a/installer/data/mysql/atomicupdate/bug_8753-Add_forgot_password_link_to_OPAC.sql b/installer/data/mysql/atomicupdate/bug_8753-Add_forgot_password_link_to_OPAC.sql new file mode 100644 index 0000000000..9719bee330 --- /dev/null +++ b/installer/data/mysql/atomicupdate/bug_8753-Add_forgot_password_link_to_OPAC.sql @@ -0,0 +1,12 @@ +INSERT IGNORE INTO systempreferences (variable,value,options,explanation,type) +VALUES ('OpacResetPassword', '0','','Shows the ''Forgot your password?'' link in the OPAC','YesNo'); + +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; + +INSERT IGNORE 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 <>.\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/en/mandatory/sample_notices.sql b/installer/data/mysql/en/mandatory/sample_notices.sql index 2e25cfff5a..d8f8e6b642 100644 --- a/installer/data/mysql/en/mandatory/sample_notices.sql +++ b/installer/data/mysql/en/mandatory/sample_notices.sql @@ -162,3 +162,7 @@ VALUES ( 'circulation', 'OVERDUES_SLIP', '', 'Overdues Slip', '0', 'OVERDUES_SLI "<>" by <>, <>, Barcode: <> Fine: <> ', 'print' ); + +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 <>.\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/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref index 09b850c52a..bb9e1eb905 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 @@ -345,6 +345,14 @@ OPAC: yes: Allow 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: diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc index 1afeae67b5..636c5cbd06 100644 --- a/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc +++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc @@ -316,6 +316,12 @@
+ [% IF Koha.Preference('OpacPasswordChange') && Koha.Preference('OpacResetPassword') %] +
+
Forgot your password?
+

If you do not remember your password, click here to create a new one.

+
+ [% END %] [% IF Koha.Preference( 'NoLoginInstructions' ) %]
[% Koha.Preference( 'NoLoginInstructions' ) %] 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 3dc1c56ec9..1174f59572 100644 --- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt +++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt @@ -150,6 +150,12 @@
+ [% IF Koha.Preference('OpacPasswordChange') && Koha.Preference('OpacResetPassword') %] +
+
Forgot your password?
+

If you do not remember your password, click here to create a new one.

+
+ [% END %]
[% IF Koha.Preference('NoLoginInstructions') %] [% Koha.Preference('NoLoginInstructions') %] diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-main.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-main.tt index 7e51a94b9f..87051fb14f 100644 --- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-main.tt +++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-main.tt @@ -85,6 +85,12 @@ [% IF PatronSelfRegistration && PatronSelfRegistrationDefaultCategory %]

Don't have an account? Register here.

[% END %] + [% IF Koha.Preference('OpacPasswordChange') && Koha.Preference('OpacResetPassword') %] +
+
Forgot your password?
+

If you do not remember your password, click here to create a new one.

+
+ [% END %] [% IF Koha.Preference( 'NoLoginInstructions' ) %]
[% Koha.Preference( 'NoLoginInstructions' ) %] 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 0000000000..8eedb7d0d4 --- /dev/null +++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-password-recovery.tt @@ -0,0 +1,134 @@ +[% 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 'bodytag.inc' bodyid='opac-password-recovery' %] +[% INCLUDE 'masthead.inc' %] + +
+ + +
+
+
+ +
+
+

Password recovery

+ [% IF (hasError) %] +
+

An error occured

+

+ [% IF (sendmailError) %] + An error has occured while sending you the password recovery link. +
Please try again later. + [% ELSIF (errNoBorrowerFound) %] + No account was found with the provided information. +
Check if you typed it correctly. + [% ELSIF (errBadEmail) %] + The provided email address is not tied to this account. + [% ELSIF (errTooManyEmailFound) %] + More than one account has been found for the email address: "[% email %]" +
Try to use your username or an alternative email if you have one. + [% ELSIF (errNoBorrowerEmail) %] + This account has no email address we can send the email to. + [% ELSIF (errAlreadyStartRecovery) %] + The process of password recovery has already started for this account ("[% username %]") +
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. + [% ELSIF (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. + [% ELSIF (errLinkNotValid) %] + We could not authenticate you as the account owner. +
Be sure to use the link you received in your email. + [% END %] +

+

Please contact the staff if you need further assistance.

+
+ [% END %] +
+[% IF (!Koha.Preference('OpacResetPassword')) %] +
You can't reset your password.
+[% ELSIF (password_recovery) %] +
+ +
+

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

+ + + + +
+ +
+
+
+[% ELSIF (new_password) %] +
+ +
+
The password must contain at least [% minPassLength %] characters.
+ + + + +
+ + + +
+
+
+[% ELSIF (mail_sent) %] +
+

+ An email 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 %]".

+ Click here to login. +
+[% END %] +
+
+
+
+
+[% INCLUDE 'opac-bottom.inc' %] \ No newline at end of file diff --git a/opac/opac-password-recovery.pl b/opac/opac-password-recovery.pl new file mode 100755 index 0000000000..0013870f99 --- /dev/null +++ b/opac/opac-password-recovery.pl @@ -0,0 +1,179 @@ +#!/usr/bin/perl + +use strict; +use Modern::Perl; +use CGI; + +use C4::Auth; +use C4::Koha; +use C4::Members qw(changepassword Search); +use C4::Output; +use C4::Context; +use C4::Passwordrecovery qw(SendPasswordRecoveryEmail ValidateBorrowernumber GetValidLinkInfo CompletePasswordRecovery); +use Koha::AuthUtils qw(hash_password); +my $query = new CGI; +use HTML::Entities; + +my ( $template, $dummy, $cookie ) = get_template_and_user( + { + template_name => "opac-password-recovery.tt", + 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 $errNoBorrowerFound; +my $errNoBorrowerEmail; +my $errAlreadyStartRecovery; +my $errTooManyEmailFound; +my $errBadEmail; + +#new password form error +my $errLinkNotValid; +my $errPassNotMatch; +my $errPassTooShort; + +if ( $query->param('sendEmail') || $query->param('resendEmail') ) { + my $protocol = $query->https() ? "https://" : "http://"; + #try with the main email + $email ||= ''; # avoid undef + my $borrower; + my $search_results; + + # Find the borrower by his userid or email + if( $username ){ + $search_results = Search({ userid => $username }); + } + elsif ( $email ){ + $search_results = Search({ '' => $email }, undef, undef, undef, ['emailpro', 'email', 'B_email']); + } + + if(scalar @$search_results > 1){ # Many matching borrowers + $hasError = 1; + $errTooManyEmailFound = 1; + } + elsif( $borrower = shift @$search_results ){ # One matching borrower + $username ||= $borrower->{'userid'}; + my @emails = ( $borrower->{'email'}, $borrower->{'emailpro'}, $borrower->{'B_email'} ); + # Is the given email one of the borrower's ? + if( $email && !($email ~~ @emails) ){ + $hasError = 1; + $errBadEmail = 1; + } + # If we dont have an email yet. Get one of the borrower's email or raise an error. + # FIXME: That ugly shift-grep contraption. + # $email = shift [ grep { length() } @emails ] + # It's supposed to get a non-empty string from the @emails array. There's surely a simpler way + elsif( !$email && !($email = shift [ grep { length() } @emails ]) ){ + $hasError = 1; + $errNoBorrowerEmail = 1; + } + # Check if a password reset already issued for this borrower AND we are not asking for a new email + elsif( ValidateBorrowernumber( $borrower->{'borrowernumber'} ) && !$query->param('resendEmail') ){ + $hasError = 1; + $errAlreadyStartRecovery = 1; + } + } + else{ # 0 matching borrower + $hasError = 1; + $errNoBorrowerFound = 1; + } + if ($hasError) { + $template->param( + hasError => 1, + errNoBorrowerFound => $errNoBorrowerFound, + errTooManyEmailFound => $errTooManyEmailFound, + errAlreadyStartRecovery => $errAlreadyStartRecovery, + errBadEmail => $errBadEmail, + errNoBorrowerEmail => $errNoBorrowerEmail, + password_recovery => 1, + email => HTML::Entities::encode($email), + username => $username + ); + } + elsif ( SendPasswordRecoveryEmail( $borrower, $email, $protocol, $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') ) { + ( $borrower_number, $username ) = GetValidLinkInfo($uniqueKey); + #validate password length & match + if ( ($borrower_number) + && ( $password eq $repeatPassword ) + && ( length($password) >= $minPassLength ) ) + { #apply changes + changepassword( $username, $borrower_number, hash_password($password) ); + CompletePasswordRecovery($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 + ( $borrower_number, $username ) = GetValidLinkInfo($uniqueKey); + + if ( !$borrower_number ) { + $errLinkNotValid = 1; + } + + $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; -- 2.39.5