--- loncom/interface/resetpw.pm	2019/02/08 19:57:29	1.43
+++ loncom/interface/resetpw.pm	2025/02/14 22:42:05	1.53
@@ -1,7 +1,7 @@
 # The LearningOnline Network
 # Allow access to password changing via a token sent to user's e-mail. 
 #
-# $Id: resetpw.pm,v 1.43 2019/02/08 19:57:29 raeburn Exp $
+# $Id: resetpw.pm,v 1.53 2025/02/14 22:42:05 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -55,6 +55,7 @@ use Apache::Constants qw(:common);
 use Apache::lonacc;
 use Apache::lonnet;
 use Apache::loncommon;
+use Apache::lonpreferences;
 use Apache::lonlocal;
 use LONCAPA;
 use HTML::Entities;
@@ -92,22 +93,81 @@ sub handler {
     $uname =~ s/^\s+|\s+$//g;
     $uname = &LONCAPA::clean_username($uname);
     my $udom = &LONCAPA::clean_domain($env{'form.udom'});
-    my ($domdesc,$otherinst);
+    my ($domdesc,$otherinst,$lookup);
     if ($udom) {
         $domdesc = &Apache::lonnet::domain($udom,'description');
         if ($domdesc) {
             $otherinst = 1;
             my @ids=&Apache::lonnet::current_machine_ids();
             my %servers = &Apache::lonnet::internet_dom_servers($udom);
-            foreach my $server (keys(%servers)) {
-                if (grep(/^\Q$server\E$/,@ids)) {
+            foreach my $hostid (keys(%servers)) {
+                if (grep(/^\Q$hostid\E$/,@ids)) {
                     $otherinst = 0;
                     last;
                 }
             }
         }
     }
+    my $dom_in_effect = $defdom;
+    if (($udom ne '') && ($domdesc ne '')) {
+        unless ($otherinst) {
+            $dom_in_effect = $udom;
+        }
+    }
+    my %passwdconf = &Apache::lonnet::get_passwdconf($dom_in_effect);
+    my $clientip = &Apache::lonnet::get_requestor_ip($r);
     my $token = $env{'form.token'};
+    my $useremail = $env{'form.useremail'};
+    if (($udom ne '') && (!$otherinst) && (!$token)) {
+        if ($uname ne '') {
+            my $uhome = &Apache::lonnet::homeserver($uname,$udom);
+            if ($uhome eq 'no_host') {
+                my %srch = (srchby     => 'uname_ci',
+                            srchdomain => $udom,
+                            srchterm   => $uname,
+                            srchtype   => 'exact');
+                my %srch_results = &Apache::lonnet::usersearch(\%srch);
+                if (keys(%srch_results) > 1) {
+                    $lookup = 'nonunique';
+                    if ($useremail =~ /^[^\@]+\@[^\@]+\.[^\@\.]+$/) {
+                        foreach my $key (keys(%srch_results)) {
+                            if (ref($srch_results{$key}) eq 'HASH') {
+                                if ($srch_results{$key}{permanentemail} =~ /^\Q$useremail\E$/i) {
+                                    ($uname) = split(/:/,$key);
+                                    undef($lookup);
+                                    last;
+                                }
+                            }
+                        }
+                    }
+                } elsif (keys(%srch_results) == 1) {
+                    my $match = (keys(%srch_results))[0];
+                    ($uname) = split(/:/,$match);
+                } else {
+                    $lookup = 'nomatch';
+                }
+            }
+        }
+        if (($lookup eq 'nomatch') || ($uname eq '')) {
+            if (($useremail =~ /^[^\@]+\@[^\@]+\.[^\@\.]+$/) &&
+                ($passwdconf{'resetprelink'} eq 'either')) {
+                my %srch = (srchby     => 'email',
+                            srchdomain => $udom,
+                            srchterm   => $useremail,
+                            srchtype   => 'exact');
+                my %srch_results = &Apache::lonnet::usersearch(\%srch);
+                if (keys(%srch_results) > 1) {
+                    $lookup = 'nonunique';
+                } elsif (keys(%srch_results) == 1) {
+                    my $match = (keys(%srch_results))[0];
+                    ($uname) = split(/:/,$match);
+                    undef($lookup);
+                } else {
+                    $lookup = 'nomatch';
+                }
+            }
+        }
+    }
     my $brcrum = [];
     if ($token) {
         push (@{$brcrum},
@@ -157,12 +217,11 @@ sub handler {
             thdo  => 'The domain you have selected is for another institution.',
             yowi  => 'You will be switched to the Forgot Password utility at that institution.',
             unam  => 'You must enter a username.',
-            mail  => 'You must enter an e-mail address.'
+            mail  => 'You must enter an e-mail address.',
+            eith  => 'Enter a username and/or an e-mail address.',
         );
         &js_escape(\%js_lt);
         $js = <<"END";
-<script type="text/javascript">
-// <![CDATA[
 function verifyDomain(caller,form) {
     var redirect = 1; 
     var instdoms = new Array($instdomstr);
@@ -174,14 +233,43 @@ function verifyDomain(caller,form) {
             }
         }
     }
+    if (redirect == 0) {
+        if (caller.options[caller.selectedIndex].value != '$dom_in_effect') {
+            document.forgotpw.uname.value = '';
+            document.forgotpw.useremail.value = '';
+            form.submit();
+        }
+    }
     if (redirect == 1) {
         if (confirm('$js_lt{thdo}\\n$js_lt{yowi}')) {
             form.submit();
+        } else {
+            for (var i=0; i<caller.options.length; i++) { 
+                if (caller.option[i].value == '$dom_in_effect') {
+                    caller.selectedIndex = i;
+                    break;
+                }
+            }
         }
     }
     return;
 }
 
+END
+        if ($passwdconf{resetprelink} eq 'either') {
+            $js .= <<"END";
+function validInfo() {
+    if ((document.forgotpw.uname.value == '') &&
+        (document.forgotpw.useremail.value == '')) {
+        alert("$js_lt{'eith'}");
+        return false;
+    }
+    return true;
+}
+END
+        } else {
+            $js .= <<"END";
+
 function validInfo() {
     if (document.forgotpw.uname.value == '') {
         alert("$js_lt{'unam'}");
@@ -193,129 +281,202 @@ function validInfo() {
     }
     return true;
 }
-// ]]>
-</script>
 END
+        }
+    }
+    $js = &Apache::lonhtmlcommon::scripttag($js);
+    if (($passwdconf{'captcha'} eq 'recaptcha') && ($passwdconf{'recaptchaversion'} >=2)) {
+        $js.= "\n".'<script src="https://www.google.com/recaptcha/api.js"></script>'."\n";
     }
     my $header = &Apache::loncommon::start_page('Reset password',$js,$args).
-                 '<h2>'.&mt('Reset forgotten LON-CAPA password').'</h2>';
+                 '<div class="LC_landmark" role="banner">'.
+                 '<h2 class="LC_heading_2">'.&mt('Reset forgotten LON-CAPA password').'</h2></div>';
     my $output;
     if ($token) {
-        $r->print($header);
-        &reset_passwd($r,$token,$contact_name,$contact_email);
-        $r->print(&Apache::loncommon::end_page());
+        $r->print($header.'<div class="LC_landmark" role="main">');
+        &reset_passwd($r,$token,$contact_name,$contact_email,$clientip,\%passwdconf);
+        $r->print('</div>'.&Apache::loncommon::end_page());
         return OK;
     } elsif ($udom) {
         if (!$domdesc) {
             $output = &invalid_state('baddomain',$domdesc,
                                      $contact_name,$contact_email); 
         } elsif ($otherinst) {
-            ($header,$output) = &homeserver_redirect($uname,$udom,$domdesc,$brcrum);
-        } elsif ($uname) {
-            my $authtype = &Apache::lonnet::queryauthenticate($uname,$udom);
-            if ($authtype =~ /^internal/) {
-                my $useremail = $env{'form.useremail'};
-                my ($blocked,$blocktext) =
-                    &Apache::loncommon::blocking_status('passwd',$uname,$udom);
-                if ($blocked) {
-                    $output = '<p class="LC_warning">'.$blocktext.'</p>'
-                              .&display_actions($contact_email,$domdesc);
-                } elsif ($useremail !~ /^[^\@]+\@[^\@]+\.[^\@\.]+$/) {
-                    $output = &invalid_state('baduseremail',$domdesc,
+            ($header,$output) = &homeserver_redirect($r,$uname,$udom,$domdesc,$brcrum);
+        } elsif (($uname) || ($useremail)) {
+            my $earlyout;
+            unless ($passwdconf{'captcha'} eq 'unused') {
+                my ($captcha_chk,$captcha_error) =
+                    &Apache::loncommon::captcha_response('passwords',$server,$dom_in_effect);
+                if ($captcha_chk != 1) {
+                    my $error = 'captcha'; 
+                    if ($passwdconf{'captcha'} eq 'recaptcha') {
+                        $error = 'recaptcha';
+                    }
+                    $output = &invalid_state($error,$domdesc,
                                              $contact_name,$contact_email);
-                } else {
-                    my %userinfo = 
-	                &Apache::lonnet::get('environment',\@emailtypes,
-					     $udom,$uname);
-                    my @allemails;
-                    foreach my $type (@emailtypes) {
-                        my $email = $userinfo{$type};
-                        my @items;
-                        if ($email =~ /,/) {
-                            @items = split(',',$userinfo{$type});
-                        } else {
-                            @items = ($email);
-                        }
-                        foreach my $item (@items) {
-                            if ($item =~ /^[^\@]+\@[^\@]+\.[^\@\.]+$/) {
-                                unless(grep(/^\Q$item\E$/,@allemails)) { 
-                                    push(@allemails,$item);
+                    $earlyout = 1;
+                }
+            }
+            unless ($earlyout) {
+                if ($lookup) {
+                    $output = &invalid_state($lookup,$domdesc,
+                                             $contact_name,$contact_email);
+                    $earlyout = 1;
+                }
+            }
+            unless ($earlyout) {
+                my $authtype = &Apache::lonnet::queryauthenticate($uname,$udom);
+                if ($authtype =~ /^internal/) {
+                    my ($blocked,$blocktext) =
+                        &Apache::loncommon::blocking_status('passwd',$clientip,$uname,$udom);
+                    if ($blocked) {
+                        $output = '<p class="LC_warning">'.$blocktext.'</p>'
+                                  .&display_actions($contact_email,$domdesc);
+                    } elsif (($passwdconf{'resetprelink'} ne 'either') && 
+                             ($useremail !~ /^[^\@]+\@[^\@]+\.[^\@\.]+$/)) {
+                        $output = &invalid_state('baduseremail',$domdesc,
+                                                 $contact_name,$contact_email);
+                    } else {
+                        my %userinfo = 
+	                    &Apache::lonnet::get('environment',\@emailtypes,
+					         $udom,$uname);
+                        my @allemails;
+                        foreach my $type (@emailtypes) {
+                            if (ref($passwdconf{resetemail}) eq 'ARRAY') {
+                                if ($type eq 'permanentemail') {
+                                    next unless (grep(/^permanent$/,@{$passwdconf{resetemail}}));
+                                } elsif ($type eq 'critnotification') {
+                                    next unless (grep(/^critical$/,@{$passwdconf{resetemail}}));
+                                } elsif ($type eq 'notification') {
+                                    next unless (grep(/^notify$/,@{$passwdconf{resetemail}}));
+                                }
+                            }
+                            my $email = $userinfo{$type};
+                            my @items;
+                            if ($email =~ /,/) {
+                                @items = split(',',$userinfo{$type});
+                            } else {
+                                @items = ($email);
+                            }
+                            foreach my $item (@items) {
+                                if ($item =~ /^[^\@]+\@[^\@]+\.[^\@\.]+$/) {
+                                    unless (grep(/^\Q$item\E$/i,@allemails)) {
+                                        push(@allemails,$item);
+                                    }
                                 }
                             }
                         }
-                    }
-                    if (@allemails > 0) {
-                        if (grep(/^\Q$useremail\E$/,@allemails)) {
-                            $output = &send_token($uname,$udom,$useremail,$server,
-                                                  $domdesc,$contact_name,
-                                                  $contact_email);
+                        if (@allemails > 0) {
+                            my ($sendto,$warning,$timelimit);
+                            my $timelimit = 2;
+                            if ($passwdconf{'resetlink'} =~ /^\d+(|\.\d*)$/) {
+                                $timelimit = $passwdconf{'resetlink'};
+                            }
+                            if ($passwdconf{'resetprelink'} eq 'either') {
+                                if ($useremail ne '') {
+                                    if (grep(/^\Q$useremail\E$/i,@allemails)) {
+                                        $sendto = $useremail;
+                                    } else {
+                                        $warning = &mt('The e-mail address you entered did not match the expected e-mail address.');
+                                    }
+                                } elsif (@allemails > 1) {
+                                    $warning = &mt('More than one e-mail address is associated with your username, and one has been selected to receive the message sent by LON-CAPA.');
+                                }
+                                unless ($sendto) {
+                                    $sendto = $allemails[0];
+                                }
+                            } else {
+                                if (grep(/^\Q$useremail\E$/i,@allemails)) {
+                                    $sendto = $useremail;
+                                } else {
+                                    $output = &invalid_state('mismatch',$domdesc,
+                                                             $contact_name,
+                                                             $contact_email);
+                                }
+                            }
+                            if ($sendto) {
+                                $output = &send_token($uname,$udom,$sendto,$server,
+                                                      $domdesc,$contact_name,
+                                                      $contact_email,$timelimit,$warning);
+                            }
                         } else {
-                            $output = &invalid_state('mismatch',$domdesc,
-                                                     $contact_name,
-                                                     $contact_email);
+                            $output = &invalid_state('missing',$domdesc,
+                                                     $contact_name,$contact_email);
                         }
-                    } else {
-                        $output = &invalid_state('missing',$domdesc,
-                                                 $contact_name,$contact_email);
                     }
-                }
-            } elsif ($authtype =~ /^(krb|unix|local)/) { 
-                $output = &invalid_state('authentication',$domdesc,
-                                         $contact_name,$contact_email);
-            } else {
-                $output = &invalid_state('invalid',$domdesc,
+                } elsif ($authtype =~ /^(krb|unix|local)/) {
+                    $output = &invalid_state('authentication',$domdesc,
                                          $contact_name,$contact_email);
+                } else {
+                    $output = &invalid_state('invalid',$domdesc,
+                                             $contact_name,$contact_email);
+                }
             }
         } else {
-            $output = &get_uname($defdom);
+            $output = &get_uname($server,$dom_in_effect,\%passwdconf);
         }
     } else {
-        $output = &get_uname($defdom);
+        $output = &get_uname($server,$defdom,\%passwdconf);
     }
-    $r->print($header.$output);
+    $r->print($header.'<div class="LC_landmark" role="main">'.$output.'</div>');
     $r->print(&Apache::loncommon::end_page());
     return OK;
 }
 
 sub get_uname {
-    my ($defdom) = @_;
+    my ($server,$defdom,$passwdconf) = @_;
+    return unless (ref($passwdconf) eq 'HASH');
     my %lt = &Apache::lonlocal::texthash(
-                                         unam => 'LON-CAPA username',
-                                         udom => 'LON-CAPA domain',
-                                         uemail => 'E-mail address in LON-CAPA',
-                                         proc => 'Proceed');
-
-    my $msg = &mt('If you use the same account for other campus services besides LON-CAPA, (e.g., e-mail, course registration, etc.), a separate centrally managed mechanism likely exists to reset a password. However, if your account is used for just LON-CAPA access you will probably be able to reset a password from this page.');
-    $msg .= '<br /><br />'.&mt('Three conditions must be met:')
+            unam => 'LON-CAPA username',
+            udom => 'LON-CAPA domain',
+            uemail => 'E-mail address in LON-CAPA',
+            vali   => 'Validation',
+            proc => 'Proceed');
+    my $msg;
+    unless ($passwdconf->{'resetremove'}) {
+        $msg = '<p>'.&mt('If you use the same account for other campus services besides LON-CAPA, (e.g., e-mail, course registration, etc.), a separate centrally managed mechanism likely exists to reset a password. However, if your account is used for just LON-CAPA access you will probably be able to reset a password from this page.').'</p>';
+    }
+    if ($passwdconf->{'resetcustom'} eq "/res/$defdom/$defdom-domainconfig/customtext/resetpw/resetpw.html") {
+        my $contents = &Apache::lonnet::getfile(&Apache::lonnet::filelocation('',$passwdconf->{'resetcustom'}));
+        unless ($contents eq '-1') {
+            $msg .= $contents;
+        }
+    }
+    $msg .= '<p>'.&mt('Three conditions must be met:').'</p>'
            .'<ul><li>'.&mt('An e-mail address must have previously been associated with your LON-CAPA username.').'</li>'
            .'<li>'.&mt('You must be able to access e-mail sent to that address.').'</li>'
            .'<li>'.&mt('Your LON-CAPA account must be of a type for which LON-CAPA can reset a password.')
            .'</ul>';
-    my $mobileargs;
-    (undef,undef,undef,undef,undef,undef,my $clientmobile) =
-        &Apache::loncommon::decode_user_agent();
-    if ($clientmobile) {
-        $mobileargs = 'autocapitalize="off" autocorrect="off" ';
-    }
     my $onchange = 'javascript:verifyDomain(this,this.form);';
     $msg .= '<form name="forgotpw" method="post" action="/adm/resetpw" onsubmit="return validInfo();">'.
-            &Apache::lonhtmlcommon::start_pick_box(). 
-            &Apache::lonhtmlcommon::row_title($lt{'unam'}).
-            '<input type="text" name="uname" size="20" '.$mobileargs.'/>'.
+            &Apache::lonhtmlcommon::start_pick_box().
+            &Apache::lonhtmlcommon::row_title('<label for="udom">'.$lt{'udom'}).'</label>').
+            &Apache::loncommon::select_dom_form($defdom,'udom',undef,undef,$onchange,'','','','udom').
             &Apache::lonhtmlcommon::row_closure(1).
-            &Apache::lonhtmlcommon::row_title($lt{'udom'}).
-            &Apache::loncommon::select_dom_form($defdom,'udom',undef,undef,$onchange).
+            &Apache::lonhtmlcommon::row_title('<label for="uname">'.$lt{'unam'}.'</label>').
+            '<input type="text" name="uname" size="20" autocapitalize="off" autocorrect="off" />'.
             &Apache::lonhtmlcommon::row_closure(1).
-            &Apache::lonhtmlcommon::row_title($lt{'uemail'}).
-            '<input type="text" name="useremail" size="30" '.$mobileargs.'/>'.
-            &Apache::lonhtmlcommon::end_pick_box().
+            &Apache::lonhtmlcommon::row_title('<label for="useremail">'.$lt{'uemail'}.'</label>').
+            '<input type="text" name="useremail" size="30" autocapitalize="off" autocorrect="off" />'.
+            &Apache::lonhtmlcommon::row_closure(1);
+    unless ($passwdconf->{'captcha'} eq 'notused') {
+        my ($captcha_form,$captcha_error,$captcha,$recaptcha_version) =
+            &Apache::loncommon::captcha_display('passwords',$server,$defdom);
+        if ($captcha_form) {
+            $msg .= &Apache::lonhtmlcommon::row_title($lt{'vali'}).
+                    $captcha_form."\n".
+                    &Apache::lonhtmlcommon::row_closure(1);
+        }
+    }
+    $msg .= &Apache::lonhtmlcommon::end_pick_box().
             '<br /><br /><input type="submit" name="resetter" value="'.$lt{'proc'}.'" /></form>';
     return $msg;
 }
 
 sub send_token {
     my ($uname,$udom,$email,$server,$domdesc,$contact_name,
-        $contact_email) = @_;
+        $contact_email,$timelimit,$warning) = @_;
     my $msg =
         '<p class="LC_info">'
        .&mt('Thank you for your request to reset the password for your LON-CAPA account.')
@@ -323,7 +484,8 @@ sub send_token {
 
     my $now = time;
     my $temppasswd = &create_passwd();
-    my %info = ('ip'         => $ENV{'REMOTE_ADDR'},
+    my $ip = &Apache::lonnet::get_requestor_ip(); 
+    my %info = ('ip'         => $ip,
 		'time'       => $now,
 		'domain'     => $udom,
 		'username'   => $uname,
@@ -342,7 +504,7 @@ sub send_token {
             $msg .=
                 &mt('An e-mail sent to the e-mail address associated with your LON-CAPA account includes the web address for the link you should use to complete the reset process.')
                .'<br /><br />'
-               .&mt('The link included in the message will be valid for the next [_1]two[_2] hours.','<b>','</b>');
+               .&mt('The link included in the message will be valid for the next [_1][quant,_2,hour][_3].','<b>',$timelimit,'</b>');
         } else {
             $msg .=
                 '<p class="LC_error">'
@@ -393,7 +555,15 @@ sub invalid_state {
               .'</p>';
         $msg .= &display_actions($contact_email,$domdesc);
     } else {
-        if ($error eq 'baduseremail') {
+        if ($error eq 'captcha') {
+            $msg = &mt('Validation of the code you entered failed');
+        } elsif ($error eq 'recaptcha') {
+            $msg = &mt('Validation of human, not robot, failed'); 
+        } elsif ($error eq 'nonunique') {
+            $msg = &mt('More than one username was identified from the information you provided; try providing both a username and e-mail address');
+        } elsif ($error eq 'nomatch') {
+            $msg = &mt('A valid user could not be identified from the username and/or e-mail address you provided');
+        } elsif ($error eq 'baduseremail') {
             $msg = &mt('The e-mail address you provided does not appear to be a valid address.');
         } elsif ($error eq 'mismatch') {
             $msg = &mt('The e-mail address you provided does not match the address recorded in the LON-CAPA system for the username and domain you provided.');  
@@ -411,7 +581,7 @@ sub invalid_state {
 }
 
 sub homeserver_redirect {
-    my ($uname,$udom,$domdesc,$brcrum) = @_;
+    my ($r,$uname,$udom,$domdesc,$brcrum) = @_;
     my $uhome;
     if (($uname ne '') && ($udom ne '')) {
         $uhome = &Apache::lonnet::homeserver($uname,$udom);
@@ -422,6 +592,8 @@ sub homeserver_redirect {
     my $hostname = &Apache::lonnet::hostname($uhome);
     my $protocol = $Apache::lonnet::protocol{$uhome};
     $protocol = 'http' if ($protocol ne 'https');
+    my $alias = &Apache::lonnet::use_proxy_alias($r,$uhome);
+    $hostname = $alias if ($alias ne '');
     my $url = $protocol.'://'.$hostname.'/adm/resetpw';
     # Breadcrumbs
     my $start_page = &Apache::loncommon::start_page('Switching Server',undef,
@@ -434,7 +606,8 @@ sub homeserver_redirect {
 }
 
 sub reset_passwd {
-    my ($r,$token,$contact_name,$contact_email) = @_;
+    my ($r,$token,$contact_name,$contact_email,$clientip,$passwdconf) = @_;
+    return unless (ref($passwdconf) eq 'HASH');
     my %data = &Apache::lonnet::tmpget($token);
     my $now = time;
     if (keys(%data) == 0) {
@@ -448,30 +621,115 @@ sub reset_passwd {
         ($data{'domain'} ne '') && 
         ($data{'email'}  =~ /^[^\@]+\@[^\@]+\.[^\@\.]+$/) && 
         ($data{'temppasswd'} =~/^\w+$/)) {
+        my $timelimit = 7200;
+        if ($passwdconf->{resetlink} =~ /^\d+(|\.\d*)$/) {
+            $timelimit = 3600 * $passwdconf->{resetlink};
+        }
         my $reqtime = &Apache::lonlocal::locallocaltime($data{'time'});
         my ($blocked,$blocktext) =
-            &Apache::loncommon::blocking_status('passwd',$data{'username'},$data{'domain'});
+            &Apache::loncommon::blocking_status('passwd',$clientip,$data{'username'},$data{'domain'});
         if ($blocked) {
             $r->print('<p class="LC_warning">'.$blocktext.'</p>');
             return;
-        } elsif ($now - $data{'time'} < 7200) {
+        } elsif ($now - $data{'time'} < $timelimit) {
+            my ($needscase,%formfields) = &reset_requires($data{'username'},$data{'domain'},
+                                                          $passwdconf);
             if ($env{'form.action'} eq 'verify_and_change_pass') {
-                $env{'form.uname'} =~ s/^\s+|\s+$//g;
-                $env{'form.udom'} =~ s/^\s+|\s+$//g;
-                $env{'form.email'} =~ s/^\s+|\s+$//g;
-                unless (($env{'form.uname'} eq $data{'username'}) && ($env{'form.udom'} eq $data{'domain'}) && ($env{'form.email'} eq $data{'email'})) {
-                    &Apache::lonnet::logthis("Forgot Password -- token data: ||$data{'username'}|| ||$data{'domain'}|| ||$data{'email'}|| differs from form: ||$env{'form.uname'}|| ||$env{'form.udom'}|| ||$env{'form.email'}||");
-                    $r->print(&generic_failure_msg($contact_name,$contact_email));
+                my $invalidinfo;
+                if ($formfields{'username'}) {
+                    $env{'form.uname'} =~ s/^\s+|\s+$//g;
+                    $env{'form.udom'} =~ s/^\s+|\s+$//g;
+                    if ($needscase) {
+                        unless (($env{'form.uname'} eq $data{'username'}) && ($env{'form.udom'} eq $data{'domain'})) {
+                            $invalidinfo = "||$env{'form.uname'}|| ||$env{'form.udom'}|| ";
+                        }
+                    } else {
+                        if ((lc($env{'form.uname'}) eq lc($data{'username'})) && (lc($env{'form.udom'}) eq lc($data{'domain'}))) {
+                            $env{'form.uname'} = $data{'username'};
+                        } else {
+                            $invalidinfo = "||$env{'form.uname'}|| ||$env{'form.udom'}|| ";
+                        }
+                    }
+                } else {
+                    $env{'form.uname'} = $data{'username'};
+                    $env{'form.udom'} = $data{'domain'};
+                }
+                if ($formfields{'email'}) {
+                    $env{'form.email'} =~ s/^\s+|\s+$//g;
+                    if ($needscase) {
+                        unless ($env{'form.email'} eq $data{'email'}) {
+                            $invalidinfo .= "||$env{'form.email'}||";
+                        }
+                    } else {
+                        unless (lc($env{'form.email'}) eq lc($data{'email'})) {
+                            $invalidinfo = "||$env{'form.email'}||";
+                        }
+                    }
+                }
+                if ($invalidinfo) {
+                    &Apache::lonnet::logthis("Forgot Password -- token data: ||$data{'username'}|| ||$data{'domain'}|| ||$data{'email'}|| differs from form: $invalidinfo");
+                    my $retry;
+                    $r->print(
+                              '<p class="LC_warning">'
+                             .&mt('A problem occurred when attempting to reset'
+                             .' the password for your account.').'</p>');
+                    if (($formfields{'username'}) && ($formfields{'email'})) {
+                        if ($needscase) {
+                            $r->print('<p>'
+                                     .&mt('Please verify you entered the correct username and e-mail address, '
+                                     .'including the correct lower and/or upper case letters')
+                                     .'</p>');
+                        } else {
+                            $r->print('<p>'
+                                     .&mt('Please verify you entered the correct username and e-mail address.')
+                                     .'</p>');
+                        }
+                        $retry = 1;
+                    } elsif ($formfields{'username'}) {
+                        if ($needscase) {
+                            $r->print('<p>'
+                                     .&mt('Please verify you entered the correct username, '
+                                     .'including the correct lower and/or upper case letters')
+                                     .'</p>');
+                        } else {
+                            $r->print('<p>'
+                                     .&mt('Please verify you entered the correct username.')
+                                     .'</p>');
+                        }
+                        $retry = 1;
+                    } elsif ($formfields{'email'}) {
+                        if ($needscase) {
+                            $r->print('<p>'
+                                     .&mt('Please verify you entered the correct e-mail address, '
+                                     .'including the correct lower and/or upper case letters')
+                                     .'</p>');
+                        } else {
+                            $r->print('<p>'
+                                     .&mt('Please verify you entered the correct e-mail address.')
+                                     .'</p>');
+                        }
+                        $retry = 1;
+                    }
+                    if ($retry) {
+                         &Apache::lonpreferences::passwordchanger($r,'','reset_by_email',$token,$timelimit,\%formfields);
+                    } else {
+                        $r->print(&generic_failure_msg($contact_name,$contact_email));
+                    }
+                    unless ($formfields{'username'}) {
+                        delete($env{'form.uname'});
+                        delete($env{'form.udom'});
+                    }
                     return;
                 }
-                my $change_failed = 
-		    &Apache::lonpreferences::verify_and_change_password($r,'reset_by_email',$token);
+                my $change_failed =
+		    &Apache::lonpreferences::verify_and_change_password($r,'reset_by_email',$token,$timelimit,\%formfields);
                 if (!$change_failed) {
                     my $delete = &Apache::lonnet::tmpdel($token);
                     my $now = &Apache::lonlocal::locallocaltime(time);
                     my $domdesc = 
 			&Apache::lonnet::domain($data{'domain'},'description');
-                    my $mailmsg = &mt('The password for your LON-CAPA account in the [_1] domain was changed [_2] from IP address: [_3].  If you did not perform this change or authorize it, please contact the [_4] ([_5]).',$domdesc,$now,$ENV{'REMOTE_ADDR'},$contact_name,$contact_email)."\n";
+                    my $ip = &Apache::lonnet::get_requestor_ip();
+                    my $mailmsg = &mt('The password for your LON-CAPA account in the [_1] domain was changed [_2] from IP address: [_3].  If you did not perform this change or authorize it, please contact the [_4] ([_5]).',$domdesc,$now,$ip,$contact_name,$contact_email)."\n";
                     my $result = &send_mail($domdesc,$data{'email'},$mailmsg,
                                             $contact_name,$contact_email);
                     my $confirm_msg;
@@ -501,6 +759,8 @@ sub reset_passwd {
                         my $hostname = &Apache::lonnet::hostname($homeserver);
                         my $protocol = $Apache::lonnet::protocol{$homeserver};
                         $protocol = 'http' if ($protocol ne 'https');
+                        my $alias = &Apache::lonnet::use_proxy_alias($r,$homeserver);
+                        $hostname = $alias if ($alias ne '');
                         my $url = $protocol.'://'.$hostname.'/adm/resetpw';
                         my ($opentag,$closetag);
                         if ($url) {
@@ -516,18 +776,93 @@ sub reset_passwd {
                            .'</p>'
                         );
                     }
-                } else {
+                } elsif (($change_failed eq 'prioruse') && ($passwdconf->{'numsaved'})) {
+                    my $domdesc =
+                        &Apache::lonnet::domain($data{'domain'},'description');
+                    $r->print(
+                          '<p class="LC_warning">'
+                         .&mt('Please enter a password that you have not used recently.')
+                         .'</p>'
+                         .&display_actions($contact_email,$domdesc,$token)
+                    );
+                } elsif (($change_failed eq 'internalerror') || ($change_failed eq 'missingtemp') ||
+                         ($change_failed eq 'error')) {
                     $r->print(&generic_failure_msg($contact_name,$contact_email));
                 }
+                unless ($formfields{'username'}) {
+                    delete($env{'form.uname'});
+                    delete($env{'form.udom'});
+                }
             } else {
                 $r->print(&mt('The token included in an e-mail sent to you [_1] has been verified, so you may now proceed to reset the password for your LON-CAPA account.',$reqtime).'<br /><br />');
-                $r->print(&mt('Please enter the username and domain of the LON-CAPA account, and the associated e-mail address, for which you are setting a password. The new password must contain at least 7 characters.').' '.&mt('Your new password will be sent to the LON-CAPA server in an encrypted form.').'<br />');
-                &Apache::lonpreferences::passwordchanger($r,'','reset_by_email',$token);
+                if (keys(%formfields)) {
+                    if (($formfields{'username'}) && ($formfields{'email'})) {
+                        $r->print(&mt('Please enter the username and domain of the LON-CAPA account, and the associated e-mail address, for which you are setting a password.'));
+                    } elsif ($formfields{'username'}) {
+                        $r->print(&mt('Please enter the username and domain of the LON-CAPA account for which you are setting a password.'));
+                    } elsif ($formfields{'email'}) {
+                        $r->print(&mt('Please enter the e-mail address associated with the LON-CAPA account for which you are setting a password.'));
+                    }
+                    if ($needscase) {
+                        $r->print(' '.&mt('User data entered must match LON-CAPA account information (including case).'));
+                    }
+                    $r->print('<br />');
+                }
+                my ($min,$max,$minrule,$maxrule);
+                if ($passwdconf->{min}) {
+                    $min = $passwdconf->{min};
+                } else {
+                    $min = $Apache::lonnet::passwdmin;
+                }
+                if ($min) {
+                    $minrule = &mt('Minimum password length: [_1]',$min);
+                }
+                if ($passwdconf->{max}) {
+                    $max = $passwdconf->{max};
+                    $maxrule = &mt('Maximum password length: [_1]',$max);
+                }
+                if (ref($passwdconf->{chars}) eq 'ARRAY') {
+                    my %rules;
+                    map { $rules{$_} = 1; } @{$passwdconf->{chars}};
+                    my %rulenames = &Apache::lonlocal::texthash(
+                                                     uc => 'At least one upper case letter',
+                                                     lc => 'At least one lower case letter',
+                                                     num => 'At least one number',
+                                                     spec => 'At least one non-alphanumeric',
+                                                   );
+                    $r->print(&mt('The new password must satisfy the following:').'<ul>');
+                    foreach my $poss ('uc','lc','num','spec') {
+                        if ($rules{$poss}) {
+                            $r->print('<li>'.$rulenames{$poss}.'</li>');
+                        }
+                    }
+                    if ($min) {
+                        $r->print('<li>'.$minrule.'</li>');
+                    }
+                    if ($max) {
+                        $r->print('<li>'.$maxrule.'</li>');
+                    }
+                    $r->print('</ul>');
+                } else {
+                    if ($min && $max) {
+                        $r->print(&mt('The new password must satisfy the following:').'<ul>'."\n".
+                                  '<li>'.$minrule.'</li>'."\n".
+                                  '<li>'.$maxrule.'</li>'."\n".
+                                  '</ul>'."\n");
+                    } elsif ($min) {
+                        $r->print($minrule.'<br />');
+                    } elsif ($max) {
+                        $r->print($maxrule.'<br />');
+                    }
+                }
+                $r->print(&mt('Your new password will be sent to the LON-CAPA server in an encrypted form.').'<br />');
+                &Apache::lonpreferences::passwordchanger($r,'','reset_by_email',$token,$timelimit,\%formfields);
             }
         } else {
             $r->print(
                 '<p class="LC_warning">'
-               .&mt('Sorry, the token generated when you requested a password reset has expired. Please submit a [_1]new request[_2], and follow the link to the web page included in the new e-mail that will be sent to you, to allow you to enter a new password.'
+               .&mt('Sorry, the token generated when you requested a password reset has expired.').'<br />'
+               .&mt('Please submit a [_1]new request[_2], and follow the link to the web page included in the new e-mail that will be sent to you, to allow you to enter a new password.'
                     ,'<a href="/adm/resetpw">','</a>')
                .'</p>'
             );
@@ -575,9 +910,13 @@ sub create_passwd {
 }
 
 sub display_actions {
-    my ($contact_email, $domdesc) = @_;
+    my ($contact_email,$domdesc,$token) = @_;
+    my $url = '/adm/resetpw';
+    if ($token) {
+        $url .= '?token='.&escape($token);
+    }
     my @msg = (&mt('[_1]Go back[_2] and try again',
-                   '<a href="javascript:history.go(-1)">','</a>'));
+                   '<a href="'.$url.'">','</a>'));
     my $msg2 = '';
     if ($contact_email ne '') {
         my $escuri = &HTML::Entities::encode('/adm/resetpw','&<>"');
@@ -593,9 +932,41 @@ sub display_actions {
                 '<i>'.$domdesc.'</i>')
            .'</p>';
     }
-
     return &Apache::lonhtmlcommon::actionbox(\@msg).$msg2;
+}
 
+sub reset_requires {
+    my ($username,$domain,$passwdconf) = @_;
+    my (%fields,$needscase);
+    return ($needscase,%fields) unless (ref($passwdconf) eq 'HASH');
+    my (%postlink,%resetcase);
+    if (ref($passwdconf->{resetpostlink}) eq 'HASH') {
+        %postlink = %{$passwdconf->{resetpostlink}};
+    }
+    if (ref($passwdconf->{resetcase}) eq 'ARRAY') {
+        map { $resetcase{$_} = 1; } (@{$passwdconf->{resetcase}});
+    } else {
+        $needscase = 1;
+    }
+    my %userenv =
+        &Apache::lonnet::get('environment',['inststatus'],$domain,$username);
+    my @inststatuses;
+    if ($userenv{'inststatus'} ne '') {
+        @inststatuses = split(/:/,$userenv{'inststatus'});
+    } else {
+        @inststatuses = ('default');
+    }
+    foreach my $status (@inststatuses) {
+        if (ref($postlink{$status}) eq 'ARRAY') {
+            map { $fields{$_} = 1; } (@{$postlink{$status}});
+        } else {
+            map { $fields{$_} = 1; } ('username','email');
+        }
+        if ($resetcase{$status}) {
+            $needscase = 1;
+        }
+    }
+    return ($needscase,%fields);
 }
 
 1;