From 5743977bd36865059cb65232d4cc2051907808a3 Mon Sep 17 00:00:00 2001
From: Nick Clemens <nick@bywatersolutions.com>
Date: Wed, 26 Jan 2022 12:10:49 +0000
Subject: [PATCH] Bug 29925: Add a password reset page for expired passwords

This patch adds a new page opac-reset-password where a user cna enter their login
(userid or carndumber), current password, and new password.

If the user has a password expiration date and the current password is correct and
the new passwords match and meet requirements their password will be updated and the
expiration date reset

A patron whose password does not expire will be reidrected to login to change their password

To test:
 1 - Apply patch, updatedatabase, enable new syspref EnableExpiredPasswordReset
 2 - Set 'Password expiration' for a patron category
     Home->Administration->Patron categories->Edit
 3 - Create a new patron in this category with a userid/password set, and an email
 4 - Update the patron with an expiration to be expired
     UPDATE borrowers SET password_expiration='2022-01-01' WHERE borrowernumber=51;
 5 - Give the borrower catalogue permission
 6 - Attempt to log in to Straff interface
 7 - Confirm you are signed out and notified that password must be reset
 8 - Click 'Reset your password' link
 9 - You should see the reset password page with fields for: login, current password, new password, conmfirm password
10 - enter invalid/incomplete credentials
11 - Confirm you are notified of invlaid credentials
12 - Fill in all fields, but enter current password as new password
13 - Confirm you are notified of no change
14 - Set minimum password length / strong password requirement for category
15 - Confirm you receive error if new password too short or not secure
16 - Enter a valid new password and submit and confirm update is successful
17 - Confirm you have buttons to go to OPAC or Staff and that both work
18 - Confirm you cna log in (i.e. expiration has been reset)
19 - Expire the users password
20 - Remove catalogue permission
21 - Reset password again and confirm only OPAC link

Signed-off-by: Bob Bennhoff <bbennhoff@clicweb.org>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
---
 .../atomicupdate/bug_29925_password_reset.pl  |  15 ++
 .../en/modules/admin/preferences/patrons.pref |   8 +
 .../intranet-tmpl/prog/en/modules/auth.tt     |   4 +-
 .../bootstrap/en/modules/opac-auth.tt         |   6 +-
 .../en/modules/opac-reset-password.tt         | 157 ++++++++++++++++++
 opac/opac-reset-password.pl                   | 107 ++++++++++++
 6 files changed, 294 insertions(+), 3 deletions(-)
 create mode 100755 installer/data/mysql/atomicupdate/bug_29925_password_reset.pl
 create mode 100644 koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-reset-password.tt
 create mode 100755 opac/opac-reset-password.pl

diff --git a/installer/data/mysql/atomicupdate/bug_29925_password_reset.pl b/installer/data/mysql/atomicupdate/bug_29925_password_reset.pl
new file mode 100755
index 0000000000..67e2628480
--- /dev/null
+++ b/installer/data/mysql/atomicupdate/bug_29925_password_reset.pl
@@ -0,0 +1,15 @@
+use Modern::Perl;
+
+return {
+    bug_number => "29925",
+    description => "Add password reset option",
+    up => sub {
+        my ($args) = @_;
+        my ($dbh, $out) = @$args{qw(dbh out)};
+        $dbh->do(q{
+            INSERT IGNORE INTO systempreferences ( `variable`, `value`, `options`, `explanation`, `type` ) VALUES
+            ('EnableExpiredPasswordReset', '0', NULL, 'Enable ability for patrons with expired password to reset their password directly', 'YesNo')
+        });
+        say $out "Added EnableExpirePasswordReset system preference";
+    },
+};
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref
index 76ebd9fd09..871c63722f 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref
@@ -365,6 +365,14 @@ Patrons:
                0: "Don't require"
          - a strong password for staff and patrons
          - (must contain at least one digit, one lowercase and one uppercase).
+     -
+         - pref: EnableExpiredPasswordReset
+           choices:
+               1: "Enable"
+               0: "Don't enable"
+         - the ability for patrons to directly reset their password when it is expired.
+         - If not enable patrons must either use the 'Forgot your password' feature or have staff
+         - reset their password.
      -
          - Block a patron's account if it reaches
          - pref: FailedLoginAttempts
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt
index 4dd1505c04..531235034b 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt
@@ -52,7 +52,9 @@
     [% END %]
 [% ELSIF password_has_expired %]
     <div id="login_error"><strong>Error: </strong>Your password has expired!</div>
-    [% IF Categories.can_any_reset_password && Koha.Preference('OpacBaseURL') %]
+    [% IF Koha.Preference('EnableExpiredPasswordReset') && Koha.Preference('OpacBaseURL') %]
+        <a href="[% Koha.Preference('OpacBaseURL') | url %]/cgi-bin/koha/opac-reset-password.pl">You must reset your password</a>.
+    [% ELSIF Categories.can_any_reset_password && Koha.Preference('OpacBaseURL') %]
         <a href="[% Koha.Preference('OpacBaseURL') | url %]/cgi-bin/koha/opac-password-recovery.pl">You must reset your password</a>.
     [% ELSE %]
         <p>You must contact the library to have your password reset</p>
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 acba29d1b0..0fd0d32bba 100644
--- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt
+++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt
@@ -171,9 +171,11 @@
                             <div class="alert alert-info">
                                 <p><strong>Error: </strong>Your password has expired!</p>
                             </div>
-                            [% IF Koha.Preference('OpacPasswordChange') && Categories.can_any_reset_password %]
+                            [% IF Koha.Preference('EnableExpiredPasswordReset') %]
+                                <a href="/cgi-bin/koha/opac-reset-password.pl">Reset your password</a>.
+                            [% ELSIF Koha.Preference('OpacPasswordChange') && Categories.can_any_reset_password %]
                                 <div id="resetpassword">
-                                    <a href="/cgi-bin/koha/opac-password-recovery.pl">Reset your password?</a>
+                                    <a href="/cgi-bin/koha/opac-password-recovery.pl">Reset your password</a>
                                 </div>
                             [% ELSE %]
                                 <p>You must contact the library to reset your password</p>
diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-reset-password.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-reset-password.tt
new file mode 100644
index 0000000000..c770189a48
--- /dev/null
+++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-reset-password.tt
@@ -0,0 +1,157 @@
+[% USE raw %]
+[% USE Koha %]
+[% USE Categories %]
+[% USE AdditionalContents %]
+[% PROCESS 'html_helpers.inc' %]
+[% INCLUDE 'doc-head-open.inc' %]
+<title>
+    Reset your password
+    &rsaquo;
+ [% IF ( LibraryNameTitle ) %][% LibraryNameTitle | html %][% ELSE %]Koha online[% END %] catalog
+</title>
+[% INCLUDE 'doc-head-close.inc' %]
+[% BLOCK cssinclude %][% END %]
+</head>
+[% INCLUDE 'bodytag.inc' bodyid='opac-login-page' bodyclass='scrollto' %]
+[% INCLUDE 'masthead.inc' %]
+
+<div class="main">
+    <nav id="breadcrumbs" aria-label="Breadcrumb" class="breadcrumbs">
+        <ol class="breadcrumb">
+            <li class="breadcrumb-item">
+                <a href="/cgi-bin/koha/opac-main.pl">Home</a>
+            </li>
+            <li class="breadcrumb-item active">
+                <a href="#" aria-current="page">Reset your password</a>
+            </li>
+        </
+    </nav> <!-- /#breadcrumbs -->
+
+    <div class="container-fluid">
+        <div class="row justify-content-center">
+            <div class="col-md-10 col-lg-6">
+                <div id="opac-auth" class="maincontent">
+                    [% IF Koha.Preference( 'opacuserlogin' ) && Koha.Preference('EnableExpiredPasswordReset') %]
+                        [% IF ( error ) %]
+                            <div class="alert alert-warning">
+                                <h2>There was a problem with your submission</h2>
+                                <p>
+                                    [% SWITCH error %]
+                                        [% CASE  'no_change' %]
+                                            New password must not be the same as old password.
+                                        [% CASE  'passwords_mismatch' %]
+                                            Passwords do not match.  Please re-type your new password.
+                                        [% CASE 'password_too_short' %]
+                                            Password must be at least [% minPasswordLength | html %] characters long.
+                                        [% CASE 'password_too_weak' %]
+                                            Password must contain at least one digit, one lowercase and one uppercase.
+                                        [% CASE 'password_has_whitespaces' %]
+                                            Password must not contain leading or trailing whitespaces.
+                                        [% CASE 'invalid_credentials' %]
+                                            You entered an incorrect username or password. Please try again! But note that passwords are case sensitive[% IF Koha.Preference('FailedLoginAttempts') %] and that your account will be locked out after a fixed number of failed login attempts[% END %]. Please contact a library staff member if you continue to have problems.
+                                        [% CASE 'no_expire' %]
+                                            Please log-in to account to update your password.</br>
+                                            <a href="/cgi-bin/koha/opac-user.pl" class="nav-link login-link loginModal-trigger"><i class="fa fa-user fa-icon-black fa-fw" aria-hidden="true"></i> <span class="userlabel">Log in to your account</span></a>
+                                        [% CASE 'account_locked' %]
+                                            This account has been locked!
+                                            [% IF Categories.can_any_reset_password && Koha.Preference('OpacBaseURL') %]
+                                                <a href="[% Koha.Preference('OpacBaseURL') | url %]/cgi-bin/koha/opac-password-recovery.pl">You must reset your password via e-mail</a>.
+                                            [% ELSE %]
+                                                You must contact the library for assistance
+                                            [% END %]
+                                        [% CASE %]
+                                            An unknown error occurred. PLease try again or contact the library for assistance
+                                    [% END %]
+                                </p>
+                            </div>
+                        [% END %]
+                        [% IF ( password_updated ) %]
+                            <div class="alert dialog-alert">
+                                <h2>Your password has successfully been updated</h2>
+                                <a href="/cgi-bin/koha/opac-main.pl" class="btn btn-primary">Go to OPAC</a>
+                                [% IF ( Koha.Preference('staffClientBaseURL') && staff_access ) %]
+                                    <a href="[% Koha.Preference('staffClientBaseURL') | url %]" class="btn btn-primary">Go to Staff client</a>
+                                [% END %]
+                            </div>
+                        [% ELSE %]
+                        <form action="/cgi-bin/koha/opac-reset-password.pl" name="mainform" id="mainform" method="post" autocomplete="off">
+                            <legend class="sr-only">Reset your password</legend>
+
+                            <fieldset class="brief">
+                                <div class="form-group">
+                                    <label for="userid">Login:</label>
+                                    <input class="form-control" type="text"  size="25" id="userid"  name="userid" />
+                                </div>
+                                <div class="form-group">
+                                    <label for="currentpassword">Current password:</label>
+                                    <input class="form-control" autocomplete="off" type="password"  size="25" id="currentpassword"  name="currentpassword" />
+                                </div>
+                                <div class="form-group">
+                                    <label for="newpassword">New password:</label>
+                                    <input class="form-control" autocomplete="off" type="password"  size="25" id="newpassword"  name="newpassword" />
+                                </div>
+                                <div class="form-group">
+                                    <label for="confirmpassword">Confirm new password:</label>
+                                    <input class="form-control" autocomplete="off" type="password"  size="25" id="confirmpassword"  name="confirmpassword" />
+                                </div>
+                                <fieldset class="action">
+                                    <input type="submit" value="Update password" class="btn btn-primary" />
+                                </fieldset>
+                                <input type="hidden" name="op" value="update" />
+                            </fieldset>
+                            [% IF Koha.Preference('OpacPasswordChange') && Categories.can_any_reset_password %]
+                                <div id="resetpassword">
+                                    <a href="/cgi-bin/koha/opac-password-recovery.pl">Forgot your password?</a>
+                                </div>
+                            [% END %]
+                        </form>
+                        [% END # /IF Error_messages %]
+
+                    [% ELSE %]
+                        <h1>Resetting your password has not been enabled by the library.</h1>
+                        [% IF Koha.Preference('OpacPasswordChange') && Categories.can_any_reset_password %]
+                            <div id="resetpassword">
+                                <a href="/cgi-bin/koha/opac-password-recovery.pl">Forgot your password?</a>
+                            </div>
+                        [% ELSE %]
+                            <p>You must contact the library to reset your password</p>
+                        [% END %]
+                    [% END # / IF opacuserlogin %]
+
+                </div> <!-- /.opac-auth -->
+            </div> <!-- /.col-md-10 col-lg-6 -->
+        </div> <!-- /.row -->
+    </div> <!-- /.container-fluid -->
+</div> <!-- /.main -->
+
+[% INCLUDE 'opac-bottom.inc' %]
+[% BLOCK jsinclude %]
+    [% Asset.js("lib/jquery/plugins/jquery.validate.min.js") | $raw %]
+    <script>
+        jQuery.validator.addMethod("password_no_spaces", function(value, element){
+            return ( this.optional(element) || !value.match(/^\s/) && !value.match(/\s$/) );
+        }, _("Password contains leading and/or trailing spaces"));
+        jQuery.validator.addMethod("password_match", function(value, element){
+            var new_password_node = $("input[name='newpassword']:first");
+            return value == $(new_password_node).val();
+        }, _("Please enter the same password as above"));
+
+        $(document).ready(function() {
+            $("#mainform").validate({
+                rules: {
+                    currentpassword: {
+                        required: true,
+                    },
+                    newpassword: {
+                        required: true,
+                        password_no_spaces: true
+                    },
+                    confirmpassword: {
+                        required: true,
+                        password_match: true
+                    }
+                }
+            });
+        });
+    </script>
+[% END %]
diff --git a/opac/opac-reset-password.pl b/opac/opac-reset-password.pl
new file mode 100755
index 0000000000..2090ddae93
--- /dev/null
+++ b/opac/opac-reset-password.pl
@@ -0,0 +1,107 @@
+#!/usr/bin/perl
+
+# 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 <http://www.gnu.org/licenses>.
+
+use Modern::Perl;
+
+use CGI qw ( -utf8 );
+
+use C4::Auth qw( get_template_and_user checkpw checkpw_hash );
+use C4::Context;
+use C4::Output qw( output_html_with_http_headers );
+use Koha::Patrons;
+
+use Try::Tiny qw( catch try );
+
+my $query = CGI->new;
+
+my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
+    {
+        template_name   => "opac-reset-password.tt",
+        query           => $query,
+        type            => "opac",
+        authnotrequired => 1,
+    }
+);
+
+my $op = $query->param('op');
+
+if ( $op eq 'update' ) {
+    my $userid          = $query->param('userid');
+    my $currentpassword = $query->param('currentpassword');
+    my $newpassword     = $query->param('newpassword');
+    my $confirmpassword = $query->param('confirmpassword');
+
+    my $patron = Koha::Patrons->find( { userid => $userid } );
+    $patron = Koha::Patrons->find( { cardnumber => $userid } ) unless $patron;
+
+    if ( $patron && $patron->password_expiration_date ) {
+        if ( $patron->account_locked ) {
+            $template->param( error => 'account_locked' );
+        }
+        elsif ( $currentpassword && $newpassword && $confirmpassword ) {
+            my $error;
+            if ( C4::Auth::checkpw_hash( $currentpassword, $patron->password ) ) {
+
+                if ( $newpassword ne $confirmpassword ) {
+                    $template->param( 'error' => 'passwords_mismatch' );
+                }
+                elsif ( $currentpassword eq $newpassword ) {
+                    $template->param( 'error' => 'no_change' );
+                }
+                else {
+                    try {
+                        $patron->set_password( { password => $newpassword } );
+                        $template->param( 'password_updated' => '1' );
+                        $template->param( 'staff_access'     => 1 )
+                          if $patron->has_permission( { catalogue => 1 } );
+                    }
+                    catch {
+                        $error = 'password_too_short'
+                          if $_->isa('Koha::Exceptions::Password::TooShort');
+                        $error = 'password_too_weak'
+                          if $_->isa('Koha::Exceptions::Password::TooWeak');
+                        $error = 'password_has_whitespaces'
+                          if $_->isa(
+                            'Koha::Exceptions::Password::WhitespaceCharacters');
+                        $template->param( 'error' => $error );
+                    };
+                }
+            }
+            else {
+                $template->param( 'error' => 'invalid_credentials' );
+                $patron->update(
+                    { login_attempts => $patron->login_attempts + 1 } )
+                  if !$patron->account_locked;
+            }
+        }
+        else {
+            $template->param( 'incomplete_form' => '1' );
+        }
+    }
+    elsif ( !$patron ) {
+        template->param( 'error' => 'invalid_credentials' );
+    }
+    elsif ( !$patron->password_expiration_date ) {
+        $template->param( 'error' => 'no_expire' );
+    }
+    else {
+        $template->param( 'error' => 'unknown' );
+    }
+}
+
+output_html_with_http_headers $query, $cookie, $template->output, undef,
+  { force_no_caching => 1 };
-- 
2.36.0