[LON-CAPA-cvs] cvs: loncom /interface createaccount.pm domainprefs.pm loncommon.pm lonconfigsettings.pm lonsupportreq.pm resetpw.pm
raeburn
raeburn at source.lon-capa.org
Mon Dec 8 21:29:03 EST 2025
raeburn Tue Dec 9 02:29:03 2025 EDT
Modified files:
/loncom/interface domainprefs.pm createaccount.pm loncommon.pm
lonconfigsettings.pm lonsupportreq.pm resetpw.pm
Log:
- Supported third party Captcha services now recaptcha v2 and v3, hcaptcha,
and turnstile.
-------------- next part --------------
Index: loncom/interface/domainprefs.pm
diff -u loncom/interface/domainprefs.pm:1.453 loncom/interface/domainprefs.pm:1.454
--- loncom/interface/domainprefs.pm:1.453 Wed Aug 13 19:13:09 2025
+++ loncom/interface/domainprefs.pm Tue Dec 9 02:29:03 2025
@@ -1,7 +1,7 @@
# The LearningOnline Network with CAPA
# Handler to set domain-wide configuration settings
#
-# $Id: domainprefs.pm,v 1.453 2025/08/13 19:13:09 raeburn Exp $
+# $Id: domainprefs.pm,v 1.454 2025/12/09 02:29:03 raeburn Exp $
#
# Copyright Michigan State University Board of Trustees
#
@@ -10478,10 +10478,14 @@
sub captcha_choice {
my ($context,$settings,$itemcount,$customcss,$rowstyle) = @_;
- my ($keyentry,$currpub,$currpriv,%checked,$rowname,$pubtext,$privtext,
- $vertext,$currver);
+ my ($currpub,$currpriv,$currtype,$currver,%checked,$rowname,
+ $dispprov,$dispver,$dispkeys,$threshold,$threshtext,$threshentry);
my %lt = &captcha_phrases();
- $keyentry = 'hidden';
+ $threshtext = '';
+ $threshentry = 'hidden';
+ $dispkeys = 'none';
+ $dispprov = 'none';
+ $dispver = 'none';
my $colspan=2;
if ($context eq 'cancreate') {
$rowname = &mt('CAPTCHA validation');
@@ -10497,14 +10501,24 @@
} else {
$checked{'original'} = ' checked="checked"';
}
- if ($settings->{'captcha'} eq 'recaptcha') {
- $pubtext = $lt{'pub'};
- $privtext = $lt{'priv'};
- $keyentry = 'text';
- $vertext = $lt{'ver'};
- $currver = $settings->{'recaptchaversion'};
- if ($currver ne '2') {
- $currver = 1;
+ if (($settings->{'captcha'} eq 'recaptcha') ||
+ ($settings->{'captcha'} eq 'hcaptcha') ||
+ ($settings->{'captcha'} eq 'turnstile')) {
+ $checked{'successor'} = ' checked="checked"';
+ $currtype = $settings->{'captcha'};
+ $dispkeys = 'block';
+ $dispprov = 'block';
+ if ($settings->{'captcha'} eq 'recaptcha') {
+ $dispver = 'block';
+ $currver = $settings->{'recaptchaversion'};
+ if ($currver eq '1') {
+ $currver = 2;
+ }
+ if ($currver eq '3') {
+ $threshold = $settings->{'captchathreshold'};
+ $threshtext = $lt{'threshold'};
+ $threshentry = 'text';
+ }
}
}
if (ref($settings->{'recaptchakeys'}) eq 'HASH') {
@@ -10530,32 +10544,61 @@
}
my $output = '<tr'.$css_class.'>'.
'<td class="LC_left_item">'.$rowname.'</td><td class="LC_left_item" colspan="'.$colspan.'">'."\n".
- '<table><tr><td>'."\n";
- foreach my $option ('original','recaptcha','notused') {
+ '<table><tr><td><fieldset class="LC_captcha"><legend>'.$lt{'captcha'}.'</legend>'."\n";
+ foreach my $option ('original','successor','notused') {
$output .= '<span class="LC_nobreak"><label><input type="radio" name="'.$context.'_captcha" value="'.
- $option.'" '.$checked{$option}.' onchange="javascript:updateCaptcha('."this,'$context'".');" />'.
+ $option.'" '.$checked{$option}.' onchange="javascript:updateCaptcha('."'captcha','$context',this.form".');" />'.
$lt{$option}.'</label></span>';
unless ($option eq 'notused') {
$output .= (' 'x2)."\n";
}
}
#
-# Note: If reCAPTCHA is to be used for LON-CAPA servers in a domain, a domain coordinator should visit:
-# https://www.google.com/recaptcha and generate a Public and Private key. For domains with multiple
-# servers a single key pair will be used for all servers, so the internet domain (e.g., yourcollege.edu)
-# specified for use with the key should be broad enough to accommodate all servers in the LON-CAPA domain.
+# Note: If reCaptcha is to be used for LON-CAPA servers in a domain, a domain coordinator should visit:
+# https://cloud.google.com/security/products/recaptcha and generate a site key and secret key. For domains
+# with multiple servers a single key pair will be used for all servers, so the internet domain
+# (e.g., yourcollege.edu) specified for use with the key should be broad enough to accommodate all servers
+# in the LON-CAPA domain. If a change is made from using reCaptcha v2 to using v3, then a new pair
+# needs to be generated via the Google Cloud dashboard.
+#
+# Similarly, if hCaptcha is to be used, a site key and secret key will need to be generated by adding a
+# new site at https://dashboard.hcaptcha.com/, and if Turnstile is to be used a site key and secret key
+# will need to be created by visiting https://dash.cloudflare.com and adding a new Turnstile widget.
#
- $output .= '</td></tr>'."\n".
+ $output .= '</fieldset></td></tr>'."\n".
'<tr><td class="LC_zero_height">'."\n".
- '<span class="LC_nobreak"><span id="'.$context.'_recaptchapubtxt">'.$pubtext.'</span> '."\n".
- '<input type="'.$keyentry.'" id="'.$context.'_recaptchapub" name="'.$context.'_recaptchapub" value="'.
- $currpub.'" size="40" /></span><br />'."\n".
- '<span class="LC_nobreak"><span id="'.$context.'_recaptchaprivtxt">'.$privtext.'</span> '."\n".
- '<input type="'.$keyentry.'" id="'.$context.'_recaptchapriv" name="'.$context.'_recaptchapriv" value="'.
- $currpriv.'" size="40" /></span><br />'.
- '<span class="LC_nobreak"><span id="'.$context.'_recaptchavertxt">'.$vertext.'</span> '."\n".
- '<input type="'.$keyentry.'" id="'.$context.'_recaptchaversion" name="'.$context.'_recaptchaversion" value="'.
- $currver.'" size="3" /></span><br />'.
+ '<fieldset class="LC_captcha" id="'.$context.'_recaptchatype" style="display:'.$dispprov.'">'.
+ '<legend>'.$lt{'type'}.'</legend>'."\n";
+ foreach my $type ('recaptcha','hcaptcha','turnstile') {
+ my $provider = $lt{$type};
+ my $checked;
+ if ($currtype eq $type) {
+ $checked = ' checked="checked"';
+ }
+ $output .= '<label><input type="radio" name="'.$context.'_captchaprovider" value="'.$type.'"'.$checked.' onchange="javascript:updateCaptchaProv('."'captchaprovider','$context',this.form".');" />'.$provider.'</label> ';
+ }
+ $output .= '</fieldset><br />'.
+ '<fieldset class="LC_captcha" id="'.$context.'_recaptchaver" style="display:'.$dispver.'"><legend>'.$lt{'ver'}.'</legend>'."\n".
+ '<span class="LC_nobreak">';
+ foreach my $poss ('2','3') {
+ my $checked;
+ if ($currver eq $poss) {
+ $checked = ' checked="checked"'
+ }
+ $output .= '<label><input type="radio" name="'.$context.'_recaptchaversion" value="'.$poss.'"'.$checked.' onchange="javascript:updateRecapVer('."'recaptchaversion','$context',this.form".');" />'.$poss.'</label> ';
+ }
+ $output .= '<span class="LC_nobreak">'.
+ '<span id="'.$context.'_recaptchathrtxt">'.$threshtext.'</span>'."\n".
+ ' <input id="'.$context.'_recaptchathresh" type="'.$threshentry.'" size="3"'.
+ ' name="'.$context.'_captchathreshold" value="'.$threshold.'" /></span></span>'."\n".
+ '</fieldset><br />'.
+ '<fieldset class="LC_captcha" id="'.$context.'_recaptchakeys" style="display:'.$dispkeys.'"><legend>'.$lt{'keys'}.'</legend>'."\n".
+ '<span class="LC_nobreak">'.$lt{'pub'}.': '.
+ '<input type="text" name="'.$context.'_recaptchapub" value="'.$currpub.'" size="40" />'.
+ '</span><br />'."\n".
+ '<span class="LC_nobreak">'.$lt{'priv'}.': '.
+ '<input type="text" name="'.$context.'_recaptchapriv" value="'.$currpriv.'" size="40" /></span>'."\n".
+ '</fieldset><br />'."\n".
'</td></tr></table>'."\n".
'</td></tr>';
return $output;
@@ -13036,16 +13079,16 @@
$pubkey = $loginhash{'login'}{$item}{'public'};
$privkey = $loginhash{'login'}{$item}{'private'};
}
- my $chgtxt .= &mt('ReCAPTCHA keys changes').'<ul>';
+ my $chgtxt .= &mt('CAPTCHA keys changes').'<ul>';
if (!$pubkey) {
- $chgtxt .= '<li>'.&mt('Public key deleted').'</li>';
+ $chgtxt .= '<li>'.&mt('Site key deleted').'</li>';
} else {
- $chgtxt .= '<li>'.&mt('Public key set to [_1]',$pubkey).'</li>';
+ $chgtxt .= '<li>'.&mt('Site key set to [_1]',$pubkey).'</li>';
}
if (!$privkey) {
- $chgtxt .= '<li>'.&mt('Private key deleted').'</li>';
+ $chgtxt .= '<li>'.&mt('Secret key deleted').'</li>';
} else {
- $chgtxt .= '<li>'.&mt('Private key set to [_1]',$privkey).'</li>';
+ $chgtxt .= '<li>'.&mt('Secret key set to [_1]',$privkey).'</li>';
}
$chgtxt .= '</ul>';
$resulttext .= '<li>'.$chgtxt.'</li>';
@@ -13053,7 +13096,15 @@
} elsif ($item eq 'recaptchaversion') {
if (ref($loginhash{'login'}) eq 'HASH') {
if ($loginhash{'login'}{'captcha'} eq 'recaptcha') {
- $resulttext .= '<li>'.&mt('ReCAPTCHA for helpdesk form set to version [_1]',$loginhash{'login'}{'recaptchaversion'}).
+ $resulttext .= '<li>'.&mt('reCaptcha for helpdesk form set to version [_1]',$loginhash{'login'}{'recaptchaversion'}).
+ '</li>';
+ }
+ }
+ } elsif ($item eq 'captchathreshold') {
+ if (ref($loginhash{'login'}) eq 'HASH') {
+ if (($loginhash{'login'}{'captcha'} eq 'recaptcha') &&
+ ($loginhash{'login'}{'recaptchaversion'} eq '3')) {
+ $resulttext .= '<li>'.&mt('reCaptcha for helpdesk form rejection threshold set to [_1]',$loginhash{'login'}{'captchathreshold'}).
'</li>';
}
}
@@ -18423,11 +18474,30 @@
if ($confighash{'passwords'}{'captcha'} eq 'original') {
$resulttext .= '<li>'.&mt('CAPTCHA validation set to use: original CAPTCHA').'</li>';
} elsif ($confighash{'passwords'}{'captcha'} eq 'recaptcha') {
- $resulttext .= '<li>'.&mt('CAPTCHA validation set to use: reCAPTCHA').' '.
- &mt('version: [_1]',$confighash{'passwords'}{'recaptchaversion'}).'<br />';
+ $resulttext .= '<li>'.&mt('CAPTCHA validation set to use: reCaptcha').' '.
+ &mt('version: [_1]',$confighash{'passwords'}{'recaptchaversion'});
+ if ($confighash{'passwords'}{'recaptchaversion'} eq '3') {
+ $resulttext .= ' '.&mt('rejection threshold set to [_1]',
+ $confighash{'passwords'}{'captchathreshold'});
+ }
+ $resulttext .= '<br />';
if (ref($confighash{'passwords'}{'recaptchakeys'}) eq 'HASH') {
- $resulttext .= &mt('Public key: [_1]',$confighash{'passwords'}{'recaptchakeys'}{'public'}).'</br>'.
- &mt('Private key: [_1]',$confighash{'passwords'}{'recaptchakeys'}{'private'}).'</li>';
+ $resulttext .= &mt('Site key: [_1]',$confighash{'passwords'}{'recaptchakeys'}{'public'}).'</br>'.
+ &mt('Secret key: [_1]',$confighash{'passwords'}{'recaptchakeys'}{'private'}).'</li>';
+ }
+ } elsif (($confighash{'passwords'}{'captcha'} eq 'hcaptcha') ||
+ ($confighash{'passwords'}{'captcha'} eq 'turnstile')) {
+ my %captchas = &captcha_phrases();
+ if ($captchas{$confighash{'passwords'}{'captcha'}}) {
+ $resulttext .= '<li>'.&mt('CAPTCHA validation set to use: [_1]',
+ $captchas{$confighash{'passwords'}{'captcha'}}).
+ '<br />';
+ if (ref($confighash{'passwords'}{'recaptchakeys'}) eq 'HASH') {
+ $resulttext .= &mt('Site key: [_1]',$confighash{'passwords'}{'recaptchakeys'}{'public'}).'</br>'.
+ &mt('Secret key: [_1]',$confighash{'passwords'}{'recaptchakeys'}{'private'}).'</li>';
+ }
+ } else {
+ $resulttext .= &mt('CAPTCHA validation set to use unknown type.');
}
} else {
$resulttext .= '<li>'.&mt('No CAPTCHA validation').'</li>';
@@ -19348,7 +19418,12 @@
#
$save_usercreate{'cancreate'}{'captcha'} = $savecaptcha{'captcha'};
$save_usercreate{'cancreate'}{'recaptchakeys'} = $savecaptcha{'recaptchakeys'};
- $save_usercreate{'cancreate'}{'recaptchaversion'} = $savecaptcha{'recaptchaversion'};
+ if ($savecaptcha{'captcha'} eq 'recaptcha') {
+ $save_usercreate{'cancreate'}{'recaptchaversion'} = $savecaptcha{'recaptchaversion'};
+ if ($savecaptcha{'recaptchaversion'} eq '3') {
+ $save_usercreate{'cancreate'}{'captchathreshold'} = $savecaptcha{'captchathreshold'};
+ }
+ }
$save_usercreate{'cancreate'}{'selfcreate'} = $cancreate{'selfcreate'};
if (ref($cancreate{'notify'}) eq 'HASH') {
$save_usercreate{'cancreate'}{'notify'} = $cancreate{'notify'};
@@ -19619,21 +19694,25 @@
$pubkey = $savecaptcha{$type}{'public'};
$privkey = $savecaptcha{$type}{'private'};
}
- $chgtext .= &mt('ReCAPTCHA keys changes').'<ul>';
+ $chgtext .= &mt('CAPTCHA keys changes').'<ul>';
if (!$pubkey) {
- $chgtext .= '<li>'.&mt('Public key deleted').'</li>';
+ $chgtext .= '<li>'.&mt('Site key deleted').'</li>';
} else {
- $chgtext .= '<li>'.&mt('Public key set to [_1]',$pubkey).'</li>';
+ $chgtext .= '<li>'.&mt('Site key set to [_1]',$pubkey).'</li>';
}
if (!$privkey) {
- $chgtext .= '<li>'.&mt('Private key deleted').'</li>';
+ $chgtext .= '<li>'.&mt('Secret key deleted').'</li>';
} else {
- $chgtext .= '<li>'.&mt('Private key set to [_1]',$pubkey).'</li>';
+ $chgtext .= '<li>'.&mt('Secret key set to [_1]',$pubkey).'</li>';
}
$chgtext .= '</ul>';
} elsif ($type eq 'recaptchaversion') {
if ($savecaptcha{'captcha'} eq 'recaptcha') {
- $chgtext .= &mt('ReCAPTCHA set to version [_1]',$savecaptcha{$type});
+ $chgtext .= &mt('reCaptcha set to version [_1]',$savecaptcha{$type});
+ }
+ } elsif ($type eq 'captchathreshold') {
+ if (($savecaptcha{'captcha'} eq 'recaptcha') && ($savecaptcha{'recaptchaversion'} eq '3')) {
+ $chgtext .= &mt('reCaptcha v3 rejection threshold set to [_1]',$savecaptcha{$type});
}
} elsif ($type eq 'emailusername') {
if (ref($cancreate{'emailusername'}) eq 'HASH') {
@@ -19785,9 +19864,18 @@
my ($container,$changes,$newsettings,$currsettings) = @_;
return unless ((ref($changes) eq 'HASH') && (ref($newsettings) eq 'HASH'));
$newsettings->{'captcha'} = $env{'form.'.$container.'_captcha'};
- unless ($newsettings->{'captcha'} eq 'recaptcha' || $newsettings->{'captcha'} eq 'notused') {
+ unless (($newsettings->{'captcha'} eq 'successor') ||
+ ($newsettings->{'captcha'} eq 'notused')) {
$newsettings->{'captcha'} = 'original';
}
+ if ($newsettings->{'captcha'} eq 'successor') {
+ $newsettings->{'captcha'} = $env{'form.'.$container.'_captchaprovider'};
+ unless (($newsettings->{'captcha'} eq 'recaptcha') ||
+ ($newsettings->{'captcha'} eq 'hcaptcha') ||
+ ($newsettings->{'captcha'} eq 'turnstile')) {
+ $newsettings->{'captcha'} = 'original';
+ }
+ }
my %current;
if (ref($currsettings) eq 'HASH') {
%current = %{$currsettings};
@@ -19805,8 +19893,11 @@
$changes->{'captcha'} = 1;
}
}
- my ($newpub,$newpriv,$currpub,$currpriv,$newversion,$currversion);
- if ($newsettings->{'captcha'} eq 'recaptcha') {
+ my ($newpub,$newpriv,$currpub,$currpriv,$newversion,$currversion,
+ $newthreshold,$currthreshold);
+ if (($newsettings->{'captcha'} eq 'recaptcha') ||
+ ($newsettings->{'captcha'} eq 'hcaptcha') ||
+ ($newsettings->{'captcha'} eq 'turnstile')) {
$newpub = $env{'form.'.$container.'_recaptchapub'};
$newpriv = $env{'form.'.$container.'_recaptchapriv'};
$newpub =~ s/[^\w\-]//g;
@@ -19815,17 +19906,30 @@
public => $newpub,
private => $newpriv,
};
- $newversion = $env{'form.'.$container.'_recaptchaversion'};
- $newversion =~ s/\D//g;
- if ($newversion ne '2') {
- $newversion = 1;
+ if ($newsettings->{'captcha'} eq 'recaptcha') {
+ $newversion = $env{'form.'.$container.'_recaptchaversion'};
+ $newversion =~ s/\D//g;
+ if ($newversion eq '3') {
+ $newthreshold = $env{'form.'.$container.'_captchathreshold'};
+ $newthreshold =~ s/^\s+|\s+$//g;
+ unless (($newthreshold =~ /^\d(|\.\d*)$/) ||
+ ($newthreshold =~ /^\.\d+$/) &&
+ ($newthreshold >= 0.0) && ($newthreshold <= 1.0)) {
+ $newthreshold = 0.5;
+ }
+ $newsettings->{'captchathreshold'} = $newthreshold;
+ } else {
+ $newversion = 2;
+ }
+ $newsettings->{'recaptchaversion'} = $newversion;
}
- $newsettings->{'recaptchaversion'} = $newversion;
}
if (ref($current{'recaptchakeys'}) eq 'HASH') {
$currpub = $current{'recaptchakeys'}{'public'};
$currpriv = $current{'recaptchakeys'}{'private'};
- unless ($newsettings->{'captcha'} eq 'recaptcha') {
+ unless (($newsettings->{'captcha'} eq 'recaptcha') ||
+ ($newsettings->{'captcha'} eq 'hcaptcha') ||
+ ($newsettings->{'captcha'} eq 'turnstile')) {
$newsettings->{'recaptchakeys'} = {
public => '',
private => '',
@@ -19834,9 +19938,12 @@
}
if ($current{'captcha'} eq 'recaptcha') {
$currversion = $current{'recaptchaversion'};
- if ($currversion ne '2') {
+ if (($currversion ne '2') && ($currversion ne '3')) {
$currversion = 1;
}
+ if ($currversion eq '3') {
+ $currthreshold = $current{'captchathreshold'};
+ }
}
if ($currversion ne $newversion) {
if ($container eq 'cancreate') {
@@ -19864,6 +19971,22 @@
$changes->{'recaptchakeys'} = 1;
}
}
+ if (($newsettings->{'captcha'} eq 'recaptcha') && ($current{'captcha'} eq 'recaptcha') &&
+ ($newsettings->{'recaptchaversion'} eq '3') && ($currversion eq '3')) {
+ if ($newsettings->{'captchathreshold'} ne $currthreshold) {
+ if ($container eq 'cancreate') {
+ if (ref($changes->{'cancreate'}) eq 'ARRAY') {
+ push(@{$changes->{'cancreate'}},'captchathreshold');
+ } elsif (!defined($changes->{'cancreate'})) {
+ $changes->{'cancreate'} = ['captchathreshold'];
+ }
+ } elsif ($container eq 'passwords') {
+ $changes->{'reset'} = 1;
+ } else {
+ $changes->{'captchathreshold'} = 1;
+ }
+ }
+ }
return;
}
@@ -24010,64 +24133,92 @@
<script type="text/javascript">
// <![CDATA[
-function updateCaptcha(caller,context) {
- var privitem;
- var pubitem;
- var privtext;
- var pubtext;
- var versionitem;
- var versiontext;
- if (document.getElementById(context+'_recaptchapub')) {
- pubitem = document.getElementById(context+'_recaptchapub');
+function updateCaptcha(radio,context,form) {
+ var keysitem;
+ var typeitem;
+ var veritem;
+ if (document.getElementById(context+'_recaptchakeys')) {
+ keysitem = document.getElementById(context+'_recaptchakeys');
} else {
return;
}
- if (document.getElementById(context+'_recaptchapriv')) {
- privitem = document.getElementById(context+'_recaptchapriv');
+ if (document.getElementById(context+'_recaptchatype')) {
+ typeitem = document.getElementById(context+'_recaptchatype');
} else {
return;
}
- if (document.getElementById(context+'_recaptchapubtxt')) {
- pubtext = document.getElementById(context+'_recaptchapubtxt');
+ if (document.getElementById(context+'_recaptchaver')) {
+ veritem = document.getElementById(context+'_recaptchaver');
} else {
return;
}
- if (document.getElementById(context+'_recaptchaprivtxt')) {
- privtext = document.getElementById(context+'_recaptchaprivtxt');
+ if (form.elements[context+'_'+radio].length) {
+ for (var i=0; i<form.elements[context+'_'+radio].length; i++) {
+ if (form.elements[context+'_'+radio][i].checked) {
+ if (form.elements[context+'_'+radio][i].value == 'successor') {
+ keysitem.style.display='block';
+ typeitem.style.display='block';
+ updateCaptchaProv('captchaprovider',context,form);
+ } else {
+ keysitem.style.display='none';
+ typeitem.style.display='none';
+ veritem.style.display='none';
+ }
+ break;
+ }
+ }
+ }
+ return;
+}
+
+function updateCaptchaProv(radio,context,form) {
+ var veritem;
+ if (document.getElementById(context+'_recaptchaver')) {
+ veritem = document.getElementById(context+'_recaptchaver');
} else {
return;
}
- if (document.getElementById(context+'_recaptchaversion')) {
- versionitem = document.getElementById(context+'_recaptchaversion');
+ veritem.style.display = 'none';
+ if (form.elements[context+'_'+radio].length) {
+ for (var i=0; i<form.elements[context+'_'+radio].length; i++) {
+ if (form.elements[context+'_'+radio][i].checked) {
+ if (form.elements[context+'_'+radio][i].value == 'recaptcha') {
+ veritem.style.display = 'block';
+ updateRecapVer('recaptchaversion',context,form);
+ }
+ }
+ break;
+ }
+ }
+}
+
+function updateRecapVer(radio,context,form) {
+ var threshtxtitem;
+ if (document.getElementById(context+'_recaptchathrtxt')) {
+ threshtxtitem = document.getElementById(context+'_recaptchathrtxt');
} else {
return;
}
- if (document.getElementById(context+'_recaptchavertxt')) {
- versiontext = document.getElementById(context+'_recaptchavertxt');
+ var thresholditem;
+ if (document.getElementById(context+'_recaptchathresh')) {
+ thresholditem = document.getElementById(context+'_recaptchathresh');
} else {
return;
}
- if (caller.checked) {
- if (caller.value == 'recaptcha') {
- pubitem.type = 'text';
- privitem.type = 'text';
- pubitem.size = '40';
- privitem.size = '40';
- pubtext.innerHTML = "$lt{'pub'}";
- privtext.innerHTML = "$lt{'priv'}";
- versionitem.type = 'text';
- versionitem.size = '3';
- versiontext.innerHTML = "$lt{'ver'}";
- } else {
- pubitem.type = 'hidden';
- privitem.type = 'hidden';
- versionitem.type = 'hidden';
- pubtext.innerHTML = '';
- privtext.innerHTML = '';
- versiontext.innerHTML = '';
+ thresholditem.type = 'hidden';
+ threshtxtitem.innerHTML = "";
+ if (form.elements[context+'_'+radio].length) {
+ for (var i=0; i<form.elements[context+'_'+radio].length; i++) {
+ if (form.elements[context+'_'+radio][i].checked) {
+ if (form.elements[context+'_'+radio][i].value == '3') {
+ thresholditem.type = 'text';
+ thresholditem.size = '3';
+ threshtxtitem.innerHTML = "$lt{'threshold'}";
+ }
+ break;
+ }
}
}
- return;
}
// ]]>
@@ -24131,12 +24282,19 @@
sub captcha_phrases {
return &Apache::lonlocal::texthash (
- priv => 'Private key',
- pub => 'Public key',
- original => 'original (CAPTCHA)',
- recaptcha => 'successor (ReCAPTCHA)',
+ captcha => 'Captcha validation',
+ keys => 'Keys',
+ priv => 'Secret',
+ pub => 'Site',
+ original => 'original (perl module)',
+ successor => 'third party',
notused => 'unused',
- ver => 'ReCAPTCHA version (1 or 2)',
+ type => 'Provider',
+ recaptcha => 'reCaptcha',
+ hcaptcha => 'hCaptcha',
+ turnstile => 'Turnstile',
+ ver => 'reCaptcha version (2 or 3)',
+ threshold => '-- reject if score below:',
);
}
Index: loncom/interface/createaccount.pm
diff -u loncom/interface/createaccount.pm:1.90 loncom/interface/createaccount.pm:1.91
--- loncom/interface/createaccount.pm:1.90 Tue Feb 18 17:45:11 2025
+++ loncom/interface/createaccount.pm Tue Dec 9 02:29:03 2025
@@ -4,7 +4,7 @@
# kerberos, or SSO) or an e-mail address. Requests to use an e-mail address as
# username may be processed automatically, or may be queued for approval.
#
-# $Id: createaccount.pm,v 1.90 2025/02/18 17:45:11 raeburn Exp $
+# $Id: createaccount.pm,v 1.91 2025/12/09 02:29:03 raeburn Exp $
#
# Copyright Michigan State University Board of Trustees
#
@@ -335,6 +335,7 @@
} elsif (!$token) {
&print_header($r,$start_page,$courseid,$pagetitle);
my $now=time;
+ my $formname = 'createaccount';
if ((grep(/^login$/,@{$cancreate})) &&
((!grep(/^email$/,@{$cancreate})) || ($need_affiliation))) {
if (open(my $jsh,"<","$include/londes.js")) {
@@ -342,7 +343,8 @@
$r->print($line);
}
close($jsh);
- $r->print(&javascript_setforms($now));
+ my $submitform = " document.$formname.submit();\n";
+ $r->print(&javascript_setforms($now,$submitform));
}
}
if (grep(/^email$/,@{$cancreate})) {
@@ -350,7 +352,7 @@
}
my $usertype = &get_usertype($domain);
$output = &print_username_form($r,$domain,$domdesc,$cancreate,$now,$lonhost,
- $include,$courseid,$emailusername,
+ $formname,$include,$courseid,$emailusername,
$statusforemail,$usernameset,$condition,
$excluded,$usertype,$types,$usertypes,$othertitle);
}
@@ -391,6 +393,7 @@
'token','serverid','uname','upass','phase','create_with_email',
'code','crypt','cfirstname','clastname','g-recaptcha-response',
'recaptcha_challenge_field','recaptcha_response_field',
+ 'h-captcha-response','cf-turnstile-response',
'cmiddlename','cgeneration','cpermanentemail','cid']).
'</form>');
}
@@ -436,7 +439,8 @@
}
sub javascript_setforms {
- my ($now,$emailusername,$captcha,$usertype,$recaptchaversion,$usernameset,$condition,$excluded) = @_;
+ my ($now,$submitform,$emailusername,$captcha,$usertype,
+ $recaptcha_version,$pubkey,$usernameset,$condition,$excluded) = @_;
my ($setuserinfo, at required,$requiredchk);
if (ref($emailusername) eq 'HASH') {
if (ref($emailusername->{$usertype}) eq 'HASH') {
@@ -454,7 +458,7 @@
$setuserinfo .= ' server.elements.code.value=client.elements.code.value;'."\n".
' server.elements.crypt.value=client.elements.crypt.value;'."\n";
} elsif ($captcha eq 'recaptcha') {
- if ($recaptchaversion ne '2') {
+ if ($recaptcha_version < 2) {
$setuserinfo .=
' server.elements.recaptcha_challenge_field.value=client.elements.recaptcha_challenge_field.value;'."\n".
' server.elements.recaptcha_response_field.value=client.elements.recaptcha_response_field.value;'."\n";
@@ -515,7 +519,7 @@
$setuserinfo
client.elements.upasscheck$now.value='';
}
- server.submit();
+$submitform
}
}
return false;
@@ -524,8 +528,8 @@
// ]]>
</script>
ENDSCRIPT
- if (($captcha eq 'recaptcha') && ($recaptchaversion eq '2')) {
- $js .= "\n".'<script src="https://www.google.com/recaptcha/api.js"></script>'."\n";
+ if (($captcha eq 'recaptcha') || ($captcha eq 'hcaptcha') || ($captcha eq 'turnstile')) {
+ $js .= "\n".&Apache::loncommon::captcha_js($captcha,$recaptcha_version,$pubkey)."\n";
}
return $js;
}
@@ -606,9 +610,9 @@
}
sub print_username_form {
- my ($r,$domain,$domdesc,$cancreate,$now,$lonhost,$include,$courseid,$emailusername,
- $statusforemail,$usernameset,$condition,$excluded,$usertype,$types,$usertypes,
- $othertitle) = @_;
+ my ($r,$domain,$domdesc,$cancreate,$now,$lonhost,$formname,$include,
+ $courseid,$emailusername,$statusforemail,$usernameset,$condition,
+ $excluded,$usertype,$types,$usertypes,$othertitle) = @_;
my %lt = &Apache::lonlocal::texthash (
crac => 'Create account with a username provided by this institution',
clca => 'Create LON-CAPA account',
@@ -677,8 +681,9 @@
$output .= "\n".'<p><input type="submit" name="reportedtype" value="'.&mt('Submit').'" />'.
'</p></fieldset></form>'."\n";
} else {
- my ($captchaform,$error,$captcha,$recaptchaversion) =
- &Apache::loncommon::captcha_display('usercreation',$lonhost);
+ my ($captchaform,$error,$captcha,$recaptcha_version,$pubkey,
+ $hdn_captcha,$recaptcha_submit,$recaptcha_badge) =
+ &Apache::loncommon::captcha_display('usercreation',$lonhost,$formname);
if ($error) {
my $helpdesk = '/adm/helpdesk?origurl=%2fadm%2fcreateaccount';
if ($courseid ne '') {
@@ -722,8 +727,9 @@
}
}
$output .= &print_dataentry_form($r,$domain,$lonhost,$include,$now,$captchaform,
- $courseid,$emailusername,$captcha,$usertype,
- $recaptchaversion,$usernameset,$condition,$excluded);
+ $courseid,$emailusername,$captcha,$formname,$pubkey,
+ $hdn_captcha,$recaptcha_submit,$recaptcha_badge,$usertype,
+ $recaptcha_version,$usernameset,$condition,$excluded);
}
}
$output .= '</div>';
@@ -851,10 +857,17 @@
$contact_name,$contact_email);
return $output;
} else {
- my ($captcha_chk,$captcha_error) = &Apache::loncommon::captcha_response('usercreation',$server);
+ my ($captcha_chk,$captcha_error,$captcha) = &Apache::loncommon::captcha_response('usercreation',$server);
if ($captcha_chk != 1) {
- $output = '<span class="LC_warning">'.
- &mt('Validation of the code you entered failed.').'</span>'.
+ my $errormsg;
+ if ($captcha eq 'original') {
+ $errormsg = &mt('Validation of the code you entered failed');
+ } elsif (($captcha eq 'recaptcha') ||
+ ($captcha eq 'hcaptcha') ||
+ ($captcha eq 'turnstile')) {
+ $errormsg = &mt('Validation of human, not robot, failed');
+ }
+ $output = '<span class="LC_warning">'.$errormsg.'</span>'."\n".
'<br />'.$captcha_error."\n".'<br /><p>'.
&mt('[_1]Return[_2] to the previous page to try again.',
'<a href="javascript:document.retryemail.submit();">','</a>')."\n".
@@ -1179,15 +1192,20 @@
#
sub print_dataentry_form {
my ($r,$domain,$lonhost,$include,$now,$captchaform,$courseid,$emailusername,$captcha,
- $usertype,$recaptchaversion,$usernameset,$condition,$excluded) = @_;
+ $formname,$pubkey,$hdn_captcha,$recaptcha_submit,$recaptcha_badge,$usertype,
+ $recaptcha_version,$usernameset,$condition,$excluded) = @_;
my ($error,$output);
if (open(my $jsh,"<","$include/londes.js")) {
while(my $line = <$jsh>) {
$r->print($line);
}
close($jsh);
- $output = &javascript_setforms($now,$emailusername,$captcha,$usertype,$recaptchaversion,
- $usernameset,$condition,$excluded).
+ my $submitform = " document.$formname.submit();\n";
+ if (($captcha eq 'recaptcha') && ($recaptcha_version eq '3') && ($recaptcha_submit ne '')) {
+ $submitform = $recaptcha_submit;
+ }
+ $output = &javascript_setforms($now,$submitform,$emailusername,$captcha,$usertype,
+ $recaptcha_version,$pubkey,$usernameset,$condition,$excluded).
"\n".&javascript_checkpass($now,'email',$domain);
my ($lkey,$ukey) = &Apache::loncommon::des_keys();
my ($lextkey,$uextkey) = &getkeys($lkey,$ukey);
@@ -1195,7 +1213,7 @@
$lonhost);
my $showsubmit = 1;
my $serverform =
- '<form name="createaccount" method="post" target="_top" action="/adm/createaccount">';
+ '<form name="'.$formname.'" method="post" target="_top" action="/adm/createaccount">';
if ($courseid ne '') {
$serverform .= '<input type="hidden" name="courseid" value="'.$courseid.'"/>'."\n";
}
@@ -1211,27 +1229,28 @@
<input type="hidden" name="crypt" value="" />
<input type="hidden" name="code" value="" />
';
- } elsif ($captcha eq 'recaptcha') {
- if ($recaptchaversion eq '2') {
- $serverform .= &Apache::lonhtmlcommon::start_pick_box().
- &Apache::lonhtmlcommon::row_title(&mt('Validation').'<b>*</b>',
- 'LC_pick_box_title',
- 'LC_oddrow_value')."\n".
- $captchaform.
- &Apache::lonhtmlcommon::row_closure(1)."\n".
- &Apache::lonhtmlcommon::row_title()."\n".
- '<br /><input type="button" name="createaccount" value="'.
- &mt('Create account').'" onclick="checkpass('."'createaccount','newemail'".')" />'.
- &Apache::lonhtmlcommon::row_closure(1)."\n".
- &Apache::lonhtmlcommon::end_pick_box();
- undef($captchaform);
- undef($showsubmit);
- } else {
- $serverform .= '
+ } elsif ((($captcha eq 'recaptcha') && ($recaptcha_version eq '2')) ||
+ ($captcha eq 'hcaptcha') || ($captcha eq 'turnstile')) {
+ $serverform .= &Apache::lonhtmlcommon::start_pick_box().
+ &Apache::lonhtmlcommon::row_title(&mt('Validation').'<b>*</b>',
+ 'LC_pick_box_title',
+ 'LC_oddrow_value')."\n".
+ $captchaform.
+ &Apache::lonhtmlcommon::row_closure(1)."\n".
+ &Apache::lonhtmlcommon::row_title()."\n".
+ '<br /><input type="button" name="createaccount" value="'.
+ &mt('Create account').'" onclick="checkpass('."'createaccount','newemail'".')" />'.
+ &Apache::lonhtmlcommon::row_closure(1)."\n".
+ &Apache::lonhtmlcommon::end_pick_box();
+ undef($captchaform);
+ undef($showsubmit);
+ } elsif (($captcha eq 'recaptcha') && ($recaptcha_version eq '3')) {
+ $serverform .= "\n$hdn_captcha\n";
+ } elsif (($captcha eq 'recaptcha') && ($recaptcha_version < 2)) {
+ $serverform .= '
<input type="hidden" name="recaptcha_challenge_field" value="" />
<input type="hidden" name="recaptcha_response_field" value="" />
';
- }
}
if ($usertype ne '') {
$serverform .= '<input type="hidden" name="type" value="'.
@@ -1279,6 +1298,9 @@
'<p class="LC_info">'.
&mt('Fields marked [_1]*[_2] are required.','<b>','</b>').
'</p>';
+ if ($recaptcha_badge) {
+ $output .= '<p class="LC_info">'.$recaptcha_badge.'</p>';
+ }
} else {
$output = &mt('Could not load javascript file [_1]','<tt>londes.js</tt>');
}
Index: loncom/interface/loncommon.pm
diff -u loncom/interface/loncommon.pm:1.1486 loncom/interface/loncommon.pm:1.1487
--- loncom/interface/loncommon.pm:1.1486 Thu Nov 27 21:19:07 2025
+++ loncom/interface/loncommon.pm Tue Dec 9 02:29:03 2025
@@ -1,7 +1,7 @@
# The LearningOnline Network with CAPA
# a pile of common routines
#
-# $Id: loncommon.pm,v 1.1486 2025/11/27 21:19:07 raeburn Exp $
+# $Id: loncommon.pm,v 1.1487 2025/12/09 02:29:03 raeburn Exp $
#
# Copyright Michigan State University Board of Trustees
#
@@ -8944,6 +8944,11 @@
font-weight: normal;
}
+fieldset.LC_captcha > legend {
+ font-weight: normal;
+ font-style: italic;
+}
+
p.LC_medium_line {
line-height: 0.85em;
}
@@ -9814,6 +9819,7 @@
padding-bottom:5px;
}
+.grecaptcha-badge { visibility: hidden; }
END
}
@@ -19893,8 +19899,8 @@
}
sub captcha_display {
- my ($context,$lonhost,$defdom) = @_;
- my ($output,$error);
+ my ($context,$lonhost,$formname,$defdom) = @_;
+ my ($output,$error,$hdn_captcha,$recaptcha_submit,$recaptcha_badge);
my ($captcha,$pubkey,$privkey,$version) =
&get_captcha_config($context,$lonhost,$defdom);
if ($captcha eq 'original') {
@@ -19902,32 +19908,58 @@
unless ($output) {
$error = 'captcha';
}
- } elsif ($captcha eq 'recaptcha') {
- $output = &create_recaptcha($pubkey,$version);
- unless ($output) {
- $error = 'recaptcha';
+ } else {
+ if ($captcha eq 'recaptcha') {
+ my $recaptcha = &create_recaptcha($pubkey,$version);
+ if ($version eq '3') {
+ $hdn_captcha = $recaptcha;
+ $recaptcha_submit = <<ENDJS;
+ grecaptcha.ready(function() {
+ grecaptcha.execute('$pubkey', {action: 'submit'}).then(function(token) {
+ document.getElementById('g-recaptcha-response').value = token;
+ document.$formname.submit();
+ });
+ });
+ENDJS
+ $recaptcha_badge =
+ &mt('This site is protected by reCAPTCHA and the Google [_1]Privacy Policy[_2] and [_3]Terms of Service[_4] apply.',
+ '<a href="https://policies.google.com/privacy">',
+ '</a>',
+ '<a href="https://policies.google.com/terms">',
+ '</a>');
+ } else {
+ $output = $recaptcha;
+ }
+ } elsif ($captcha eq 'hcaptcha') {
+ $output = &create_hcaptcha($pubkey);
+ } elsif ($captcha eq 'turnstile') {
+ $output = &create_turnstile($pubkey);
+ }
+ unless ($output || $hdn_captcha) {
+ $error = $captcha;
}
}
- return ($output,$error,$captcha,$version);
+ return ($output,$error,$captcha,$version,$pubkey,$hdn_captcha,$recaptcha_submit,$recaptcha_badge);
}
sub captcha_response {
my ($context,$lonhost,$defdom) = @_;
my ($captcha_chk,$captcha_error);
- my ($captcha,$pubkey,$privkey,$version) = &get_captcha_config($context,$lonhost,$defdom);
+ my ($captcha,$pubkey,$privkey,$version,$threshold) =
+ &get_captcha_config($context,$lonhost,$defdom);
if ($captcha eq 'original') {
($captcha_chk,$captcha_error) = &check_captcha();
- } elsif ($captcha eq 'recaptcha') {
- $captcha_chk = &check_recaptcha($privkey,$version);
+ } elsif (($captcha eq 'recaptcha') || ($captcha eq 'hcaptcha') || ($captcha eq 'turnstile')) {
+ $captcha_chk = &check_recaptcha($captcha,$privkey,$pubkey,$version,$threshold);
} else {
$captcha_chk = 1;
}
- return ($captcha_chk,$captcha_error);
+ return ($captcha_chk,$captcha_error,$captcha);
}
sub get_captcha_config {
my ($context,$lonhost,$dom_in_effect) = @_;
- my ($captcha,$pubkey,$privkey,$version,$hashtocheck);
+ my ($captcha,$pubkey,$privkey,$version,$threshold,$hashtocheck);
my $hostname = &Apache::lonnet::hostname($lonhost);
my $serverhomeID = &Apache::lonnet::get_server_homeID($hostname);
my $serverhomedom = &Apache::lonnet::host_domain($serverhomeID);
@@ -19936,16 +19968,18 @@
if (ref($domconfig{$context}) eq 'HASH') {
$hashtocheck = $domconfig{$context}{'cancreate'};
if (ref($hashtocheck) eq 'HASH') {
- if ($hashtocheck->{'captcha'} eq 'recaptcha') {
+ $captcha = $hashtocheck->{'captcha'};
+ if (($captcha eq 'recaptcha') || ($captcha eq 'hcaptcha') || ($captcha eq 'turnstile')) {
if (ref($hashtocheck->{'recaptchakeys'}) eq 'HASH') {
$pubkey = $hashtocheck->{'recaptchakeys'}{'public'};
$privkey = $hashtocheck->{'recaptchakeys'}{'private'};
}
if ($privkey && $pubkey) {
- $captcha = 'recaptcha';
- $version = $hashtocheck->{'recaptchaversion'};
- if ($version ne '2') {
- $version = 1;
+ if ($captcha eq 'recaptcha') {
+ $version = $hashtocheck->{'recaptchaversion'};
+ if ($version eq '3') {
+ $threshold = $hashtocheck->{'captchathreshold'};
+ }
}
} else {
$captcha = 'original';
@@ -19959,14 +19993,18 @@
}
} elsif ($context eq 'login') {
my %domconfhash = &Apache::loncommon::get_domainconf($serverhomedom);
- if ($domconfhash{$serverhomedom.'.login.captcha'} eq 'recaptcha') {
+ $captcha = $domconfhash{$serverhomedom.'.login.captcha'};
+ if (($captcha eq 'recaptcha') || ($captcha eq 'hcaptcha') || ($captcha eq 'turnstile')) {
$pubkey = $domconfhash{$serverhomedom.'.login.recaptchakeys_public'};
$privkey = $domconfhash{$serverhomedom.'.login.recaptchakeys_private'};
if ($privkey && $pubkey) {
- $captcha = 'recaptcha';
- $version = $domconfhash{$serverhomedom.'.login.recaptchaversion'};
- if ($version ne '2') {
- $version = 1;
+ if ($captcha eq 'recaptcha') {
+ $version = $domconfhash{$serverhomedom.'.login.recaptchaversion'};
+ if ($version eq '3') {
+ $threshold = $domconfhash{$serverhomedom.'.login.captchathreshold'};
+ } elsif ($version ne '2') {
+ $version = 1;
+ }
}
} else {
$captcha = 'original';
@@ -19977,16 +20015,20 @@
} elsif ($context eq 'passwords') {
if ($dom_in_effect) {
my %passwdconf = &Apache::lonnet::get_passwdconf($dom_in_effect);
- if ($passwdconf{'captcha'} eq 'recaptcha') {
+ $captcha = $passwdconf{'captcha'};
+ if (($captcha eq 'recaptcha') || ($captcha eq 'hcaptcha') || ($captcha eq 'turnstile')) {
if (ref($passwdconf{'recaptchakeys'}) eq 'HASH') {
$pubkey = $passwdconf{'recaptchakeys'}{'public'};
$privkey = $passwdconf{'recaptchakeys'}{'private'};
}
if ($privkey && $pubkey) {
- $captcha = 'recaptcha';
- $version = $passwdconf{'recaptchaversion'};
- if ($version ne '2') {
- $version = 1;
+ if ($captcha eq 'recaptcha') {
+ $version = $passwdconf{'recaptchaversion'};
+ if ($version eq '3') {
+ $threshold = $passwdconf{'captchathreshold'};
+ } elsif ($version ne '2') {
+ $version = 1;
+ }
}
} else {
$captcha = 'original';
@@ -19995,8 +20037,8 @@
$captcha = 'original';
}
}
- }
- return ($captcha,$pubkey,$privkey,$version);
+ }
+ return ($captcha,$pubkey,$privkey,$version,$threshold);
}
sub create_captcha {
@@ -20058,9 +20100,27 @@
return ($captcha_chk,$captcha_error);
}
+sub captcha_js {
+ my ($captcha,$version,$pubkey) = @_;
+ if ($captcha eq 'recaptcha') {
+ if ($version eq '2') {
+ return '<script async type="text/javascript" src="https://www.recaptcha.net/recaptcha/api.js"></script>';
+ } elsif ($version eq '3') {
+ return '<script async type="text/javascript" src="https://www.recaptcha.net/recaptcha/api.js?render='.$pubkey.'"></script>';
+ }
+ } elsif ($captcha eq 'hcaptcha') {
+ return '<script async type="text/javascript" src="https://js.hcaptcha.com/1/api.js"></script>';
+ } elsif ($captcha eq 'turnstile') {
+ return '<script async type="text/javascript" src="https://challenges.cloudflare.com/turnstile/v0/api.js"></script>'."\n";
+ }
+ return;
+}
+
sub create_recaptcha {
my ($pubkey,$version) = @_;
- if ($version >= 2) {
+ if ($version eq '3') {
+ return '<input type="hidden" name="g-recaptcha-response" id="g-recaptcha-response" />';
+ } elsif ($version eq '2') {
return '<div class="g-recaptcha" data-sitekey="'.$pubkey.'"></div>'.
'<div style="padding:0;clear:both;margin:0;border:0"></div>';
} else {
@@ -20077,33 +20137,63 @@
}
}
+sub create_hcaptcha {
+ my ($pubkey) = @_;
+ return '<div class="h-captcha" data-sitekey="'.$pubkey.'"></div>'.
+ '<div style="padding:0;clear:both;margin:0;border:0"></div>';
+}
+
+sub create_turnstile {
+ my ($pubkey) = @_;
+ return '<div class="cf-turnstile" data-sitekey="'.$pubkey.'"></div>'.
+ '<div style="padding:0;clear:both;margin:0;border:0"></div>';
+}
+
sub check_recaptcha {
- my ($privkey,$version) = @_;
+ my ($captcha,$privkey,$pubkey,$version,$threshold) = @_;
my $captcha_chk;
my $ip = &Apache::lonnet::get_requestor_ip();
- if ($version >= 2) {
+ if (($captcha eq 'hcaptcha') || ($captcha eq 'turnstile') ||
+ (($captcha eq 'recaptcha') && ($version >= 2))) {
+ my $url;
my %info = (
- secret => $privkey,
- response => $env{'form.g-recaptcha-response'},
+ secret => $privkey,
remoteip => $ip,
);
- my $request=new HTTP::Request('POST','https://www.google.com/recaptcha/api/siteverify');
- $request->content(join('&',map {
- my $name = escape($_);
- "$name=" . ( ref($info{$_}) eq 'ARRAY'
- ? join("&$name=", map {escape($_) } @{$info{$_}})
- : &escape($info{$_}) );
- } keys(%info)));
+ if ($captcha eq 'recaptcha') {
+ $info{'response'} = $env{'form.g-recaptcha-response'};
+ $url = 'https://www.recaptcha.net/recaptcha/api/siteverify';
+ } elsif ($captcha eq 'hcaptcha') {
+ $info{'response'} = $env{'form.h-captcha-response'};
+ $info{'sitekey'} = $pubkey;
+ $url = 'https://api.hcaptcha.com/siteverify';
+ } elsif ($captcha eq 'turnstile') {
+ $info{'response'} = $env{'form.cf-turnstile-response'};
+ $url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
+ }
+ my $data = join('&',map {
+ my $name = &escape($_);
+ "$name=" . &escape($info{$_});
+ } keys(%info));
+ my $header = ['Content-Type' => 'application/x-www-form-urlencoded'];
+ my $request = new HTTP::Request('POST',$url,$header,$data);
my $response = &LONCAPA::LWPReq::makerequest('',$request,'','',10,1);
if ($response->is_success) {
my $data = JSON::DWIW->from_json($response->decoded_content);
if (ref($data) eq 'HASH') {
if ($data->{'success'}) {
- $captcha_chk = 1;
+ if (($captcha eq 'recaptcha') && ($version eq '3')) {
+ my $score = $data->{'score'};
+ if ($score >= $threshold) {
+ $captcha_chk = 1;
+ }
+ } else {
+ $captcha_chk = 1;
+ }
}
}
}
- } else {
+ } elsif ($captcha eq 'recaptcha') {
my $captcha = Captcha::reCAPTCHA->new;
my $captcha_result =
$captcha->check_answer(
Index: loncom/interface/lonconfigsettings.pm
diff -u loncom/interface/lonconfigsettings.pm:1.75 loncom/interface/lonconfigsettings.pm:1.76
--- loncom/interface/lonconfigsettings.pm:1.75 Sat Jun 14 02:50:25 2025
+++ loncom/interface/lonconfigsettings.pm Tue Dec 9 02:29:03 2025
@@ -1,7 +1,7 @@
# The LearningOnline Network with CAPA
# Handler to set domain-wide configuration settings
#
-# $Id: lonconfigsettings.pm,v 1.75 2025/06/14 02:50:25 raeburn Exp $
+# $Id: lonconfigsettings.pm,v 1.76 2025/12/09 02:29:03 raeburn Exp $
#
# Copyright Michigan State University Board of Trustees
#
@@ -311,6 +311,13 @@
foreach my $server (sort(keys(%domservers))) {
$onload .= "toggleSamlOptions(document.display,'$server');";
}
+ $onload .= "updateCaptcha('captcha','login',document.display);";
+ }
+ if (grep(/^passwords$/, at actions)) {
+ $onload .= "updateCaptcha('captcha','passwords',document.display);";
+ }
+ if (grep(/^cancreate$/, at actions)) {
+ $onload .= "updateCaptcha('captcha','cancreate',document.display);";
}
if ($onload) {
my %loaditems = (
Index: loncom/interface/lonsupportreq.pm
diff -u loncom/interface/lonsupportreq.pm:1.108 loncom/interface/lonsupportreq.pm:1.109
--- loncom/interface/lonsupportreq.pm:1.108 Tue Feb 25 05:35:26 2025
+++ loncom/interface/lonsupportreq.pm Tue Dec 9 02:29:03 2025
@@ -1,7 +1,7 @@
# The LearningOnline Network with CAPA
# Helpdesk request form
#
-# $Id: lonsupportreq.pm,v 1.108 2025/02/25 05:35:26 raeburn Exp $
+# $Id: lonsupportreq.pm,v 1.109 2025/12/09 02:29:03 raeburn Exp $
#
# Copyright Michigan State University Board of Trustees
#
@@ -87,12 +87,15 @@
my ($os,$browser,$bversion,$uname,$udom,$uhome,$urole,$usec,$email,$cid,
$cdom,$cnum,$ctitle,$ccode,$sectionlist,$lastname,$firstname,$server,
$formname,$public,$homeserver,$knownuser,$captcha_form,$captcha_error,
- $captcha,$recaptcha_version,$extra_validations,%groupid);
+ $captcha,$recaptcha_version,$pubkey,$hdn_captcha,$recaptcha_badge,
+ $recaptcha_submit,$extra_validations,$submitform,%groupid);
$function = &Apache::loncommon::get_users_function() if (!$function);
$ccode = '';
$os = $env{'browser.os'};
$browser = $env{'browser.type'};
$bversion = $env{'browser.version'};
+ $submitform = 'document.logproblem.submit();';
+ $formname = 'logproblem';
if (($env{'user.name'} eq 'public') && ($env{'user.domain'} eq 'public')) {
$public = 1;
} else {
@@ -112,8 +115,13 @@
$knownuser = 1;
} else {
my $lonhost = $r->dir_config('lonHostID');
- ($captcha_form,$captcha_error,$captcha,$recaptcha_version) =
- &Apache::loncommon::captcha_display('login',$lonhost);
+ ($captcha_form,$captcha_error,$captcha,$recaptcha_version,
+ $pubkey,$hdn_captcha,$recaptcha_submit,$recaptcha_badge) =
+ &Apache::loncommon::captcha_display('login',$lonhost,$formname);
+ if (($captcha eq 'recaptcha') && ($recaptcha_version eq '3') &&
+ ($recaptcha_submit ne '')) {
+ $submitform = $recaptcha_submit;
+ }
}
&Apache::loncommon::get_unprocessed_cgi($ENV{'QUERY_STRING'},['codedom',
'useremail','useraccount']);
@@ -140,7 +148,6 @@
}
}
- $formname = 'logproblem';
my $codedom = &get_domain();
my %helpform;
my %domconfig =
@@ -273,7 +280,7 @@
return;
}
$extra_validations
- document.logproblem.submit();
+ $submitform
}
END
@@ -322,8 +329,10 @@
ENDJS
if ($knownuser) {
$js .="\n".'<script type="text/javascript" src="/res/adm/includes/file_upload.js"></script>';
- } elsif ($recaptcha_version >=2) {
- $js.= "\n".'<script src="https://www.google.com/recaptcha/api.js"></script>'."\n";
+ } else {
+ if (($captcha eq 'recaptcha') || ($captcha eq 'hcaptcha') || ($captcha eq 'turnstile')) {
+ $js .= "\n".&Apache::loncommon::captcha_js($captcha,$recaptcha_version,$pubkey)."\n";
+ }
}
my %add_entries = (
style => "margin-top:0px;margin-bottom:0px;",
@@ -646,6 +655,7 @@
</div>
<div class="LC_floatleft" style="padding-top:0, padding-left:8px; padding-right:8px; padding-bottom:0; margin:0">
<input type="reset" value="$html_lt{'clfm'}" />
+ $hdn_captcha
</div>
</div>
<div style="padding:0;clear:both;margin:0;border:0"></div>
@@ -655,7 +665,7 @@
$r->print(<<END);
$output
</form>
-<br />
+$recaptcha_badge<br />
</div>
END
$r->print(&Apache::loncommon::end_page());
@@ -671,7 +681,7 @@
}
my $lonhost = $r->dir_config('lonHostID');
unless (($env{'user.name'} =~ /^$match_username$/) && (!$public)) {
- my ($captcha_chk,$captcha_error) =
+ my ($captcha_chk,$captcha_error,$captcha) =
&Apache::loncommon::captcha_response('login',$lonhost);
if ($captcha_chk != 1) {
$args = {
@@ -690,11 +700,18 @@
if ($r->uri eq '/adm/helpdesk') {
&print_header($r,$url,'process');
}
+ my $errormsg;
+ if ($captcha eq 'original') {
+ $errormsg = &mt('Validation of the code you entered failed');
+ } elsif (($captcha eq 'recaptcha') ||
+ ($captcha eq 'hcaptcha') ||
+ ($captcha eq 'turnstile')) {
+ $errormsg = &mt('Validation of human, not robot, failed');
+ }
$r->print(
'<div class="LC_landmark" role="main">'.
'<h2 class="LC_heading_2">'.&mt('Support request failed').'</h2>'.
- &Apache::lonhtmlcommon::confirm_success(
- &mt('Validation of the code you entered failed.'),1).
+ &Apache::lonhtmlcommon::confirm_success($errormsg,1).
'<br /><br />'.
&Apache::lonhtmlcommon::actionbox([
&mt('[_1]Go back[_2] and try again',
@@ -1239,7 +1256,7 @@
back => 'Back to last location',
headline => 'help/support',
stud => 'Students',
- ifyo => 'If your problem is still unresolved, the form below can be used to send a question to the LON-CAPA helpdesk.',
+ ifyo => 'If your problem is still unresolved, use this form to send a question to the LON-CAPA helpdesk.',
cont => 'Contact your instructor instead.',
);
my ($getstartlink,$reviewtext);
Index: loncom/interface/resetpw.pm
diff -u loncom/interface/resetpw.pm:1.54 loncom/interface/resetpw.pm:1.55
--- loncom/interface/resetpw.pm:1.54 Sat Feb 15 03:43:36 2025
+++ loncom/interface/resetpw.pm Tue Dec 9 02:29:03 2025
@@ -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.54 2025/02/15 03:43:36 raeburn Exp $
+# $Id: resetpw.pm,v 1.55 2025/12/09 02:29:03 raeburn Exp $
#
# Copyright Michigan State University Board of Trustees
#
@@ -115,8 +115,16 @@
}
}
my %passwdconf = &Apache::lonnet::get_passwdconf($dom_in_effect);
- my $clientip = &Apache::lonnet::get_requestor_ip($r);
+ my $formname = 'forgotpw';
my $token = $env{'form.token'};
+ my ($captcha_form,$captcha_error,$captcha,$recaptcha_version,
+ $pubkey,$hdn_captcha,$recaptcha_submit,$recaptcha_badge);
+ unless (($passwdconf{'captcha'} eq 'notused') || ($token)) {
+ ($captcha_form,$captcha_error,$captcha,$recaptcha_version,
+ $pubkey,$hdn_captcha,$recaptcha_submit,$recaptcha_badge) =
+ &Apache::loncommon::captcha_display('passwords',$server,$formname,$dom_in_effect);
+ }
+ my $clientip = &Apache::lonnet::get_requestor_ip($r);
my $useremail = $env{'form.useremail'};
if (($udom ne '') && (!$otherinst) && (!$token)) {
if ($uname ne '') {
@@ -256,6 +264,10 @@
}
END
+ my $submitform = " document.$formname.submit();";
+ if ($recaptcha_submit ne '') {
+ $submitform = $recaptcha_submit;
+ }
if ($passwdconf{resetprelink} eq 'either') {
$js .= <<"END";
function validInfo() {
@@ -264,7 +276,8 @@
alert("$js_lt{'eith'}");
return false;
}
- return true;
+$submitform
+ return false;
}
END
} else {
@@ -279,14 +292,20 @@
alert("$js_lt{'mail'}");
return false;
}
- return true;
+$submitform
+ return false;
}
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";
+ if (($passwdconf{'captcha'} eq 'recaptcha') || ($passwdconf{'captcha'} eq 'hcaptcha') ||
+ ($passwdconf{'captcha'} eq 'turnstile')) {
+ my $pubkey;
+ if (ref($passwdconf{'recaptchakeys'}) eq 'HASH') {
+ $pubkey = $passwdconf{'recaptchakeys'}{'public'};
+ }
+ $js .= "\n".&Apache::loncommon::captcha_js($passwdconf{'captcha'},$passwdconf{'recaptchaversion'},$pubkey)."\n";
}
my $header = &Apache::loncommon::start_page('Reset password',$js,$args).
'<div class="LC_landmark" role="banner">'.
@@ -306,11 +325,13 @@
} elsif (($uname) || ($useremail)) {
my $earlyout;
unless ($passwdconf{'captcha'} eq 'unused') {
- my ($captcha_chk,$captcha_error) =
+ my ($captcha_chk,$captcha_error,$captcha) =
&Apache::loncommon::captcha_response('passwords',$server,$dom_in_effect);
if ($captcha_chk != 1) {
- my $error = 'captcha';
- if ($passwdconf{'captcha'} eq 'recaptcha') {
+ my $error = 'captcha';
+ if (($captcha eq 'recaptcha') ||
+ ($captcha eq 'hcaptcha') ||
+ ($captcha eq 'turnstile')) {
$error = 'recaptcha';
}
$output = &invalid_state($error,$domdesc,
@@ -414,10 +435,12 @@
}
}
} else {
- $output = &get_uname($server,$dom_in_effect,\%passwdconf);
+ $output = &get_uname($formname,$server,$dom_in_effect,\%passwdconf,
+ $captcha_form,$hdn_captcha,$recaptcha_badge);
}
} else {
- $output = &get_uname($server,$defdom,\%passwdconf);
+ $output = &get_uname($formname,$server,$defdom,\%passwdconf,
+ $captcha_form,$hdn_captcha,$recaptcha_badge);
}
$r->print($header.'<div class="LC_landmark" role="main">'.$output.'</div>');
$r->print(&Apache::loncommon::end_page());
@@ -425,7 +448,7 @@
}
sub get_uname {
- my ($server,$defdom,$passwdconf) = @_;
+ my ($formname,$server,$defdom,$passwdconf,$captcha_form,$hdn_captcha,$recaptcha_badge) = @_;
return unless (ref($passwdconf) eq 'HASH');
my %lt = &Apache::lonlocal::texthash(
unam => 'LON-CAPA username',
@@ -449,7 +472,8 @@
.'<li>'.&mt('Your LON-CAPA account must be of a type for which LON-CAPA can reset a password.').'</li>'
.'</ul>';
my $onchange = 'javascript:verifyDomain(this,this.form);';
- $msg .= '<form name="forgotpw" method="post" action="/adm/resetpw" onsubmit="return validInfo();">'.
+ my $validationtext = '';
+ $msg .= '<form name="'.$formname.'" method="post" action="/adm/resetpw" onsubmit="return validInfo();">'.
&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').
@@ -461,16 +485,17 @@
'<input type="text" name="useremail" id="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);
+ } else {
+ $validationtext = $recaptcha_badge;
}
}
- $msg .= &Apache::lonhtmlcommon::end_pick_box().
- '<br /><br /><input type="submit" name="resetter" value="'.$lt{'proc'}.'" /></form>';
+ $msg .= &Apache::lonhtmlcommon::end_pick_box().$validationtext.
+ '<br /><br /><input type="submit" name="resetter" value="'.$lt{'proc'}.'" />'."\n".
+ $hdn_captcha.'</form>';
return $msg;
}
More information about the LON-CAPA-cvs
mailing list