--- loncom/interface/domainprefs.pm 2021/09/01 00:21:52 1.385 +++ loncom/interface/domainprefs.pm 2021/11/22 22:19:58 1.390 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # Handler to set domain-wide configuration settings # -# $Id: domainprefs.pm,v 1.385 2021/09/01 00:21:52 raeburn Exp $ +# $Id: domainprefs.pm,v 1.390 2021/11/22 22:19:58 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -297,7 +297,10 @@ sub handler { {col1 => 'Log-in Help', col2 => 'Value'}, {col1 => 'Custom HTML in document head', - col2 => 'Value'}], + col2 => 'Value'}, + {col1 => 'SSO', + col2 => 'Dual login: SSO and non-SSO options'}, + ], print => \&print_login, modify => \&modify_login, }, @@ -632,7 +635,10 @@ sub handler { {col1 => 'Log-in Help', col2 => 'Value'}, {col1 => 'Custom HTML in document head', - col2 => 'Value'}], + col2 => 'Value'}, + {col1 => 'SSO', + col2 => 'Dual login: SSO and non-SSO options'}, + ], print => \&print_login, modify => \&modify_login, }; @@ -859,6 +865,8 @@ sub print_config_box { $output .= &wafproxy_javascript($dom); } elsif ($action eq 'autoupdate') { $output .= &autoupdate_javascript(); + } elsif ($action eq 'login') { + $output .= &saml_javascript(); } $output .= ' @@ -878,7 +886,7 @@ sub print_config_box { my $leftnobr = ''; if (($action eq 'rolecolors') || ($action eq 'defaults') || ($action eq 'directorysrch') || - (($action eq 'login') && ($numheaders < 4))) { + (($action eq 'login') && ($numheaders < 5))) { $colspan = ' colspan="2"'; } if ($action eq 'usersessions') { @@ -909,7 +917,7 @@ sub print_config_box { } elsif ($action eq 'scantron') { $output .= $item->{'print'}->($r,'top',$dom,$confname,$settings,\$rowtotal); } elsif ($action eq 'login') { - if ($numheaders == 4) { + if ($numheaders == 5) { $colspan = ' colspan="2"'; $output .= &print_login('service',$dom,$confname,$phase,$settings,\$rowtotal); } else { @@ -1050,7 +1058,7 @@ sub print_config_box { '. $item->{'print'}->('bottom',$dom,$settings,\$rowtotal); } elsif ($action eq 'login') { - if ($numheaders == 4) { + if ($numheaders == 5) { $output .= &print_login('page',$dom,$confname,$phase,$settings,\$rowtotal).'
'.&mt($item->{'header'}->[3]->{'col2'}).'
@@ -1074,7 +1082,7 @@ sub print_config_box { '; - if ($numheaders == 4) { + if ($numheaders == 5) { $output .= ' @@ -1086,7 +1094,27 @@ sub print_config_box { '; } $rowtotal ++; - $output .= &print_login('headtag',$dom,$confname,$phase,$settings,\$rowtotal); + $output .= &print_login('headtag',$dom,$confname,$phase,$settings,\$rowtotal).' +
'.&mt($item->{'header'}->[3]->{'col1'}).' '.&mt($item->{'header'}->[3]->{'col2'}).'
+ + + + + + '; + if ($numheaders == 5) { + $output .= ' + + + '; + } else { + $output .= ' + + + '; + } + $rowtotal ++; + $output .= &print_login('saml',$dom,$confname,$phase,$settings,\$rowtotal); } elsif ($action eq 'requestcourses') { $output .= &print_requestmail($dom,$action,$settings,\$rowtotal); $rowtotal ++; @@ -1221,9 +1249,12 @@ sub print_config_box { sub print_login { my ($caller,$dom,$confname,$phase,$settings,$rowtotal) = @_; - my ($css_class,$datatable); + my ($css_class,$datatable,$switchserver,%lt); my %choices = &login_choices(); - + if (($caller eq 'help') || ($caller eq 'headtag') || ($caller eq 'saml')) { + %lt = &login_file_options(); + $switchserver = &check_switchserver($dom,$confname); + } if ($caller eq 'service') { my %servers = &Apache::lonnet::internet_dom_servers($dom); my $choice = $choices{'disallowlogin'}; @@ -1417,18 +1448,10 @@ sub print_login { $datatable .= &display_color_options($dom,$confname,$phase,'login',$itemcount,\%choices,\%is_custom,\%defaults,\%designs,\@images,\@bgs,\@links,\%alt_text,$rowtotal,\@logintext); $datatable .= '
'.&mt($item->{'header'}->[4]->{'col1'}).''.&mt($item->{'header'}->[4]->{'col2'}).'
'.&mt($item->{'header'}->[3]->{'col1'}).''.&mt($item->{'header'}->[3]->{'col2'}).'
'; } elsif ($caller eq 'help') { - my ($defaulturl,$defaulttype,%url,%type,%lt,%langchoices); - my $switchserver = &check_switchserver($dom,$confname); + my ($defaulturl,$defaulttype,%url,%type,%langchoices); my $itemcount = 1; $defaulturl = '/adm/loginproblems.html'; $defaulttype = 'default'; - %lt = &Apache::lonlocal::texthash ( - del => 'Delete?', - rep => 'Replace:', - upl => 'Upload:', - default => 'Default', - custom => 'Custom', - ); %langchoices = &Apache::lonlocal::texthash(&get_languages_hash()); my @currlangs; if (ref($settings) eq 'HASH') { @@ -1525,14 +1548,6 @@ sub print_login { } } } - my %lt = &Apache::lonlocal::texthash( - del => 'Delete?', - rep => 'Replace:', - upl => 'Upload:', - curr => 'View contents', - none => 'None', - ); - my $switchserver = &check_switchserver($dom,$confname); foreach my $lonhost (sort(keys(%domservers))) { my $exempt = &check_exempt_addresses($currexempt{$lonhost}); $datatable .= ''.$domservers{$lonhost}.''; @@ -1556,6 +1571,88 @@ sub print_login { $datatable .= ''; } $datatable .= ''; + } elsif ($caller eq 'saml') { + my %domservers = &Apache::lonnet::get_servers($dom); + $datatable .= ''. + ''. + ''. + ''."\n"; + my (%saml,%samltext,%samlimg,%samlalt,%samlurl,%samltitle,%samlnotsso,%styleon,%styleoff); + foreach my $lonhost (keys(%domservers)) { + $samlurl{$lonhost} = '/adm/sso'; + $styleon{$lonhost} = 'display:none'; + $styleoff{$lonhost} = ''; + } + if (ref($settings->{'saml'}) eq 'HASH') { + foreach my $lonhost (keys(%{$settings->{'saml'}})) { + if (ref($settings->{'saml'}{$lonhost}) eq 'HASH') { + $saml{$lonhost} = 1; + $samltext{$lonhost} = $settings->{'saml'}{$lonhost}{'text'}; + $samlimg{$lonhost} = $settings->{'saml'}{$lonhost}{'img'}; + $samlalt{$lonhost} = $settings->{'saml'}{$lonhost}{'alt'}; + $samlurl{$lonhost} = $settings->{'saml'}{$lonhost}{'url'}; + $samltitle{$lonhost} = $settings->{'saml'}{$lonhost}{'title'}; + $samlnotsso{$lonhost} = $settings->{'saml'}{$lonhost}{'notsso'}; + $styleon{$lonhost} = ''; + $styleoff{$lonhost} = 'display:none'; + } else { + $styleon{$lonhost} = 'display:none'; + $styleoff{$lonhost} = ''; + } + } + } + my $itemcount = 1; + foreach my $lonhost (sort(keys(%domservers))) { + my $samlon = ' '; + my $samloff = ' checked="checked" '; + if ($saml{$lonhost}) { + $samlon = $samloff; + $samloff = ' '; + } + my $css_class = $itemcount%2?' class="LC_odd_row"':''; + $datatable .= ''. + ''. + ''. + ''; + $itemcount ++; + } + $datatable .= '
'.$choices{'hostid'}.''.$choices{'samllanding'}.''.$choices{'samloptions'}.'
'.$domservers{$lonhost}.''.(' 'x2). + ''. + ''. + ''. + ''. + ''. + ''. + ''. + ''. + ''. + ''. + '
'.&mt('SSO').''. + ''.&mt('Non-SSO').'
'.&mt('Text').''.&mt('Image').''.&mt('Alt Text').''.&mt('URL').''.&mt('Tool Tip').''.&mt('Text').'
'; + if ($samlimg{$lonhost}) { + $datatable .= '
'. + ' '.$lt{'rep'}.''; + } else { + $datatable .= $lt{'upl'}; + } + $datatable .='
'; + if ($switchserver) { + $datatable .= &mt('Upload to library server: [_1]',$switchserver); + } else { + $datatable .= ''; + } + $datatable .= '
 
'; } return $datatable; } @@ -1592,10 +1689,24 @@ sub login_choices { headtag => "Custom markup", action => "Action", current => "Current", + samllanding => "Dual login?", + samloptions => "Options", ); return %choices; } +sub login_file_options { + return &Apache::lonlocal::texthash( + del => 'Delete?', + rep => 'Replace:', + upl => 'Upload:', + curr => 'View contents', + default => 'Default', + custom => 'Custom', + none => 'None', + ); +} + sub print_rolecolors { my ($phase,$role,$dom,$confname,$settings,$rowtotal) = @_; my %choices = &color_font_choices(); @@ -3066,14 +3177,16 @@ sub lti_javascript { return $togglejs; } my (%ordered,$total,%jstext); - $total = 0; + $total = scalar(keys(%{$settings})); foreach my $item (keys(%{$settings})) { if (ref($settings->{$item}) eq 'HASH') { my $num = $settings->{$item}{'order'}; + if ($num eq '') { + $num = $total - 1; + } $ordered{$num} = $item; } } - $total = scalar(keys(%{$settings})); my @jsarray = (); foreach my $item (sort {$a <=> $b } (keys(%ordered))) { push(@jsarray,$ordered{$item}); @@ -3348,6 +3461,48 @@ function toggleLastActiveDays(form) { ENDSCRIPT } +sub saml_javascript { + return <<"ENDSCRIPT"; + + +ENDSCRIPT +} + sub print_autoenroll { my ($dom,$settings,$rowtotal) = @_; my $autorun = &Apache::lonnet::auto_run(undef,$dom), @@ -5784,6 +5939,9 @@ sub print_lti { foreach my $item (keys(%{$settings})) { if (ref($settings->{$item}) eq 'HASH') { my $num = $settings->{$item}{'order'}; + if ($num eq '') { + $num = scalar(keys(%{$settings})); + } $ordered{$num} = $item; } } @@ -7365,7 +7523,7 @@ sub print_wafproxy { my $itemcount = 0; my $datatable; my %servers = &Apache::lonnet::internet_dom_servers($dom); - my (%othercontrol,%otherdoms,%aliases,%values,$setdom,$showdom); + my (%othercontrol,%otherdoms,%aliases,%saml,%values,$setdom,$showdom); my %lt = &wafproxy_titles(); foreach my $server (sort(keys(%servers))) { my $serverhome = &Apache::lonnet::get_server_homeID($servers{$server}); @@ -7390,6 +7548,9 @@ sub print_wafproxy { $showdom = 1; } } + if (ref($settings->{'saml'}) eq 'HASH') { + $saml{$dom} = $settings->{'saml'}; + } } } } @@ -7409,6 +7570,9 @@ sub print_wafproxy { my %config = &Apache::lonnet::get_dom('configuration',['wafproxy'],$domain); if (ref($config{'wafproxy'}) eq 'HASH') { $aliases{$domain} = $config{'wafproxy'}{'alias'}; + if (exists($config{'wafproxy'}{'saml'})) { + $saml{$domain} = $config{'wafproxy'}{'saml'}; + } foreach my $item ('remoteip','ipheader','trusted','vpnint','vpnext') { $values{$domain}{$item} = $config{'wafproxy'}{$item}; } @@ -7427,14 +7591,22 @@ sub print_wafproxy { ''.&Apache::lonnet::hostname($server).' '; if ($othercontrol{$server}) { $dom_in_effect = $othercontrol{$server}; - my $current; + my ($current,$forsaml); if (ref($aliases{$dom_in_effect}) eq 'HASH') { $current = $aliases{$dom_in_effect}{$server}; } + if (ref($saml{$dom_in_effect}) eq 'HASH') { + if ($saml{$dom_in_effect}{$server}) { + $forsaml = 1; + } + } $aliasrows .= ''. &mt('Alias').': '; if ($current) { $aliasrows .= $current; + if ($forsaml) { + $aliasrows .= ' ('.&mt('also for Shibboleth').')'; + } } else { $aliasrows .= &mt('None'); } @@ -7443,16 +7615,30 @@ sub print_wafproxy { ''.$dom_in_effect.'').')'; } else { $dom_in_effect = $dom; - my $current; + my ($current,$samlon,$samloff); + $samloff = ' checked="checked"'; if (ref($aliases{$dom}) eq 'HASH') { if ($aliases{$dom}{$server}) { $current = $aliases{$dom}{$server}; } } + if (ref($saml{$dom}) eq 'HASH') { + if ($saml{$dom}{$server}) { + $samlon = $samloff; + undef($samloff); + } + } $aliasrows .= ''. &mt('Alias').': '. ''; + 'value="'.$current.'" size="30" />'. + (' 'x2).''. + &mt('Alias used for Shibboleth').':  '. + ''; } $aliasrows .= ''; $aliasinfo{$dom_in_effect} .= $aliasrows; @@ -11251,12 +11437,14 @@ sub usertype_update_row { sub modify_login { my ($r,$dom,$confname,$lastactref,%domconfig) = @_; my ($resulttext,$errors,$colchgtext,%changes,%colchanges,%newfile,%newurl, - %curr_loginvia,%loginhash,@currlangs,@newlangs,$addedfile,%title,@offon); + %curr_loginvia,%loginhash,@currlangs,@newlangs,$addedfile,%title,@offon, + %currsaml,%saml,%samltext,%samlimg,%samlalt,%samlurl,%samltitle,%samlnotsso); %title = ( coursecatalog => 'Display course catalog', adminmail => 'Display administrator E-mail address', helpdesk => 'Display "Contact Helpdesk" link', newuser => 'Link for visitors to create a user account', - loginheader => 'Log-in box header'); + loginheader => 'Log-in box header', + saml => 'Dual SSO and non-SSO login'); @offon = ('off','on'); if (ref($domconfig{login}) eq 'HASH') { if (ref($domconfig{login}{loginvia}) eq 'HASH') { @@ -11264,6 +11452,20 @@ sub modify_login { $curr_loginvia{$lonhost} = $domconfig{login}{loginvia}{$lonhost}; } } + if (ref($domconfig{login}{'saml'}) eq 'HASH') { + foreach my $lonhost (keys(%{$domconfig{login}{'saml'}})) { + if (ref($domconfig{login}{'saml'}{$lonhost}) eq 'HASH') { + $currsaml{$lonhost} = $domconfig{login}{'saml'}{$lonhost}; + $saml{$lonhost} = 1; + $samltext{$lonhost} = $domconfig{login}{'saml'}{$lonhost}{'text'}; + $samlurl{$lonhost} = $domconfig{login}{'saml'}{$lonhost}{'url'}; + $samlalt{$lonhost} = $domconfig{login}{'saml'}{$lonhost}{'alt'}; + $samlimg{$lonhost} = $domconfig{login}{'saml'}{$lonhost}{'img'}; + $samltitle{$lonhost} = $domconfig{login}{'saml'}{$lonhost}{'title'}; + $samlnotsso{$lonhost} = $domconfig{login}{'saml'}{$lonhost}{'notsso'}; + } + } + } } ($errors,%colchanges) = &modify_colors($r,$dom,$confname,['login'], \%domconfig,\%loginhash); @@ -11510,6 +11712,89 @@ sub modify_login { $errors .= '
  • '.$error.'
  • '; } } + my @delsamlimg = &Apache::loncommon::get_env_multiple('form.saml_img_del'); + my @newsamlimgs; + foreach my $lonhost (keys(%domservers)) { + if ($env{'form.saml_'.$lonhost}) { + if ($env{'form.saml_img_'.$lonhost.'.filename'}) { + push(@newsamlimgs,$lonhost); + } + foreach my $item ('text','alt','url','title','notsso') { + $env{'form.saml_'.$item.'_'.$lonhost} =~ s/^\s+|\s+$//g; + } + if ($saml{$lonhost}) { + if (grep(/^\Q$lonhost\E$/,@delsamlimg)) { +#FIXME Need to obsolete published image + delete($currsaml{$lonhost}{'img'}); + $changes{'saml'}{$lonhost} = 1; + } + if ($env{'form.saml_alt_'.$lonhost} ne $samlalt{$lonhost}) { + $changes{'saml'}{$lonhost} = 1; + } + if ($env{'form.saml_text_'.$lonhost} ne $samltext{$lonhost}) { + $changes{'saml'}{$lonhost} = 1; + } + if ($env{'form.saml_url_'.$lonhost} ne $samlurl{$lonhost}) { + $changes{'saml'}{$lonhost} = 1; + } + if ($env{'form.saml_title_'.$lonhost} ne $samltitle{$lonhost}) { + $changes{'saml'}{$lonhost} = 1; + } + if ($env{'form.saml_notsso_'.$lonhost} ne $samlnotsso{$lonhost}) { + $changes{'saml'}{$lonhost} = 1; + } + } else { + $changes{'saml'}{$lonhost} = 1; + } + foreach my $item ('text','alt','url','title','notsso') { + $currsaml{$lonhost}{$item} = $env{'form.saml_'.$item.'_'.$lonhost}; + } + } else { + if ($saml{$lonhost}) { + $changes{'saml'}{$lonhost} = 1; + delete($currsaml{$lonhost}); + } + } + } + foreach my $posshost (keys(%currsaml)) { + unless (exists($domservers{$posshost})) { + delete($currsaml{$posshost}); + } + } + %{$loginhash{'login'}{'saml'}} = %currsaml; + if (@newsamlimgs) { + my $error; + my ($configuserok,$author_ok,$switchserver) = &config_check($dom,$confname,$servadm); + if ($configuserok eq 'ok') { + if ($switchserver) { + $error = &mt("Upload of SSO Button Image is not permitted to this server: [_1].",$switchserver); + } elsif ($author_ok eq 'ok') { + foreach my $lonhost (@newsamlimgs) { + my $formelem = 'saml_img_'.$lonhost; + my ($result,$imgurl) = &publishlogo($r,'upload',$formelem,$dom,$confname, + "login/saml/$lonhost",'','', + $env{'form.saml_img_'.$lonhost.'.filename'}); + if ($result eq 'ok') { + $currsaml{$lonhost}{'img'} = $imgurl; + $loginhash{'login'}{'saml'}{$lonhost}{'img'} = $imgurl; + $changes{'saml'}{$lonhost} = 1; + } else { + my $puberror = &mt("Upload of SSO button image failed for [_1] because an error occurred publishing the file in RES space. Error was: [_2].", + $lonhost,$result); + $errors .= '
  • '.$puberror.'
  • '; + } + } + } else { + $error = &mt("Upload of SSO button image file(s) failed because an author role could not be assigned to a Domain Configuration user ([_1]) in domain: [_2]. Error was: [_3].",$confname,$dom,$author_ok); + } + } else { + $error = &mt("Upload of SSO button image file(s) failed because a Domain Configuration user ([_1]) could not be created in domain: [_2]. Error was: [_3].",$confname,$dom,$configuserok); + } + if ($error) { + &Apache::lonnet::logthis($error); + $errors .= '
  • '.$error.'
  • '; + } + } &process_captcha('login',\%changes,$loginhash{'login'},$domconfig{'login'}); my $defaulthelpfile = '/adm/loginproblems.html'; @@ -11550,6 +11835,31 @@ sub modify_login { } if (keys(%changes) > 0 || $colchgtext) { &Apache::loncommon::devalidate_domconfig_cache($dom); + if (exists($changes{'saml'})) { + my $hostid_in_use; + my @hosts = &Apache::lonnet::current_machine_ids(); + if (@hosts > 1) { + foreach my $hostid (@hosts) { + if (&Apache::lonnet::host_domain($hostid) eq $dom) { + $hostid_in_use = $hostid; + last; + } + } + } else { + $hostid_in_use = $r->dir_config('lonHostID'); + } + if (($hostid_in_use) && + (&Apache::lonnet::host_domain($hostid_in_use) eq $dom)) { + &Apache::lonnet::devalidate_cache_new('samllanding',$hostid_in_use); + } + if (ref($lastactref) eq 'HASH') { + if (ref($changes{'saml'}) eq 'HASH') { + my %updates; + map { $updates{$_} = 1; } keys(%{$changes{'saml'}}); + $lastactref->{'samllanding'} = \%updates; + } + } + } if (ref($lastactref) eq 'HASH') { $lastactref->{'domainconfig'} = 1; } @@ -11629,6 +11939,38 @@ sub modify_login { } } } + } elsif ($item eq 'saml') { + if (ref($changes{$item}) eq 'HASH') { + my %notlt = ( + text => 'Text for log-in by SSO', + img => 'SSO button image', + alt => 'Alt text for button image', + url => 'SSO URL', + title => 'Tooltip for SSO link', + notsso => 'Text for non-SSO log-in', + ); + foreach my $lonhost (sort(keys(%{$changes{$item}}))) { + if (ref($currsaml{$lonhost}) eq 'HASH') { + $resulttext .= '
  • '.&mt("$title{$item} in use for [_1]","$lonhost"). + '
  • '; + } else { + $resulttext .= '
  • '.&mt("$title{$item} not in use for [_1]",$lonhost).'
  • '; + } + } + } } elsif ($item eq 'captcha') { if (ref($loginhash{'login'}) eq 'HASH') { my $chgtxt; @@ -14410,15 +14752,15 @@ sub modify_lti { map { $deletions{$_} = 1; } @todelete; } my $maxnum = $env{'form.lti_maxnum'}; - for (my $i=0; $i<=$maxnum; $i++) { + for (my $i=0; $i<$maxnum; $i++) { my $itemid = $env{'form.lti_id_'.$i}; $itemid =~ s/\D+//g; if (ref($domconfig{$action}{$itemid}) eq 'HASH') { if ($deletions{$itemid}) { $changes{$itemid} = $domconfig{$action}{$itemid}{'consumer'}; } else { - push(@items,$i); - $itemids{$i} = $itemid; + push(@items,$i); + $itemids{$i} = $itemid; } } } @@ -14426,7 +14768,7 @@ sub modify_lti { foreach my $idx (@items) { my $itemid = $itemids{$idx}; next unless ($itemid); - my $position = $env{'form.lti_pos_'.$idx}; + my $position = $env{'form.lti_pos_'.$itemid}; $position =~ s/\D+//g; if ($position ne '') { $allpos[$position] = $itemid; @@ -14689,7 +15031,7 @@ sub modify_lti { if (ref($confhash{$itemid}) ne 'HASH') { $resulttext .= '
  • '.&mt('Deleted: [_1]',$changes{$itemid}).'
  • '; } else { - $resulttext .= '
  • '.$confhash{$itemid}{'consumer'}.'