--- loncom/interface/resetpw.pm 2019/02/08 19:57:29 1.43 +++ loncom/interface/resetpw.pm 2019/04/24 01:44:30 1.44 @@ -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.44 2019/04/24 01:44:30 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,80 @@ 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 $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 +216,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"; - END + } + } + $js = &Apache::lonhtmlcommon::scripttag($js); + if (($passwdconf{'captcha'} eq 'recaptcha') && ($passwdconf{'recaptchaversion'} >=2)) { + $js.= "\n".''."\n"; } my $header = &Apache::loncommon::start_page('Reset password',$js,$args). '

'.&mt('Reset forgotten LON-CAPA password').'

'; my $output; if ($token) { $r->print($header); - &reset_passwd($r,$token,$contact_name,$contact_email); + &reset_passwd($r,$token,$contact_name,$contact_email,\%passwdconf); $r->print(&Apache::loncommon::end_page()); return OK; } elsif ($udom) { @@ -211,66 +301,121 @@ END $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 = '

'.$blocktext.'

' - .&display_actions($contact_email,$domdesc); - } elsif ($useremail !~ /^[^\@]+\@[^\@]+\.[^\@\.]+$/) { - $output = &invalid_state('baduseremail',$domdesc, + } elsif (($uname) || ($useremail)) { + my $earlyout; + unless ($passwdconf{'captcha'} eq 'unused') { + my ($captcha_chk,$captcha_error) = + &Apache::loncommon::captcha_response('passwords',$server); + 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',$uname,$udom); + if ($blocked) { + $output = '

'.$blocktext.'

' + .&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(&Apache::loncommon::end_page()); @@ -278,44 +423,58 @@ END } 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 .= '

'.&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 = '

'.&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.').'

'; + } + 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 .= '

'.&mt('Three conditions must be met:') .'

'; - 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 .= '
'. - &Apache::lonhtmlcommon::start_pick_box(). - &Apache::lonhtmlcommon::row_title($lt{'unam'}). - ''. - &Apache::lonhtmlcommon::row_closure(1). + &Apache::lonhtmlcommon::start_pick_box(). &Apache::lonhtmlcommon::row_title($lt{'udom'}). &Apache::loncommon::select_dom_form($defdom,'udom',undef,undef,$onchange). &Apache::lonhtmlcommon::row_closure(1). + &Apache::lonhtmlcommon::row_title($lt{'unam'}). + ''. + &Apache::lonhtmlcommon::row_closure(1). &Apache::lonhtmlcommon::row_title($lt{'uemail'}). - ''. - &Apache::lonhtmlcommon::end_pick_box(). + ''. + &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(). '

'; return $msg; } sub send_token { my ($uname,$udom,$email,$server,$domdesc,$contact_name, - $contact_email) = @_; + $contact_email,$timelimit,$warning) = @_; my $msg = '

' .&mt('Thank you for your request to reset the password for your LON-CAPA account.') @@ -342,7 +501,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.') .'

' - .&mt('The link included in the message will be valid for the next [_1]two[_2] hours.','',''); + .&mt('The link included in the message will be valid for the next [_1][quant,_2,hour][_3].','',$timelimit,''); } else { $msg .= '

' @@ -393,7 +552,15 @@ sub invalid_state { .'

'; $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.'); @@ -434,7 +601,8 @@ sub homeserver_redirect { } sub reset_passwd { - my ($r,$token,$contact_name,$contact_email) = @_; + my ($r,$token,$contact_name,$contact_email,$passwdconf) = @_; + return unless (ref($passwdconf) eq 'HASH'); my %data = &Apache::lonnet::tmpget($token); my $now = time; if (keys(%data) == 0) { @@ -448,23 +616,59 @@ 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'}); if ($blocked) { $r->print('

'.$blocktext.'

'); 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'}||"); + 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 { + unless ((lc($env{'form.uname'}) eq lc($data{'username'})) && (lc($env{'form.udom'}) eq lc($data{'domain'}))) { + $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"); $r->print(&generic_failure_msg($contact_name,$contact_email)); + unless ($formfields{'username'}) { + delete($env{'form.uname'}); + delete($env{'form.udom'}); + } return; } - my $change_failed = + my $change_failed = &Apache::lonpreferences::verify_and_change_password($r,'reset_by_email',$token); if (!$change_failed) { my $delete = &Apache::lonnet::tmpdel($token); @@ -519,10 +723,46 @@ sub reset_passwd { } else { $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).'

'); - $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.').'
'); - &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(' '); + } + 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:').''); + } else { + $r->print(&mt('The new password must contain at least 7 characters.').' '); + } + $r->print(&mt('Your new password will be sent to the LON-CAPA server in an encrypted form.').'
'); + &Apache::lonpreferences::passwordchanger($r,'','reset_by_email',$token,$timelimit,\%formfields); } } else { $r->print( @@ -593,9 +833,41 @@ sub display_actions { ''.$domdesc.'') .'

'; } - 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;