--- loncom/interface/domainprefs.pm 2018/01/01 01:29:38 1.324 +++ loncom/interface/domainprefs.pm 2022/01/15 20:21:06 1.402 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # Handler to set domain-wide configuration settings # -# $Id: domainprefs.pm,v 1.324 2018/01/01 01:29:38 raeburn Exp $ +# $Id: domainprefs.pm,v 1.402 2022/01/15 20:21:06 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -104,7 +104,7 @@ $datatable - HTML containing form eleme In the case of course requests, radio buttons are displayed for each institutional affiliate type (and also default, and _LC_adv) for each of the course types -(official, unofficial, community, textbook, and placement). +(official, unofficial, community, textbook, placement, and lti). In each case the radio buttons allow the selection of one of four values: 0, approval, validate, autolimit=N (where N is blank, or a positive integer). @@ -176,6 +176,7 @@ use Locale::Language; use DateTime::TimeZone; use DateTime::Locale; use Time::HiRes qw( sleep ); +use Net::CIDR; my $registered_cleanup; my $modified_urls; @@ -219,9 +220,10 @@ sub handler { 'serverstatuses','requestcourses','helpsettings', 'coursedefaults','usersessions','loadbalancing', 'requestauthor','selfenrollment','inststatus', - 'ltitools','ssl','trust','lti'],$dom); + 'ltitools','ssl','trust','lti','privacy','passwords', + 'proctoring','wafproxy','ipaccess'],$dom); my %encconfig = - &Apache::lonnet::get_dom('encconfig',['ltitools','lti'],$dom); + &Apache::lonnet::get_dom('encconfig',['ltitools','lti','proctoring'],$dom,undef,1); if (ref($domconfig{'ltitools'}) eq 'HASH') { if (ref($encconfig{'ltitools'}) eq 'HASH') { foreach my $id (keys(%{$domconfig{'ltitools'}})) { @@ -246,12 +248,25 @@ sub handler { } } } - my @prefs_order = ('rolecolors','login','defaults','quotas','autoenroll', - 'autoupdate','autocreate','directorysrch','contacts', - 'usercreation','selfcreation','usermodification','scantron', - 'requestcourses','requestauthor','coursecategories', - 'serverstatuses','helpsettings','coursedefaults', - 'ltitools','selfenrollment','usersessions','ssl','trust','lti'); + if (ref($domconfig{'proctoring'}) eq 'HASH') { + if (ref($encconfig{'proctoring'}) eq 'HASH') { + foreach my $provider (keys(%{$domconfig{'proctoring'}})) { + if ((ref($domconfig{'proctoring'}{$provider}) eq 'HASH') && + (ref($encconfig{'proctoring'}{$provider}) eq 'HASH')) { + foreach my $item ('key','secret') { + $domconfig{'proctoring'}{$provider}{$item} = $encconfig{'proctoring'}{$provider}{$item}; + } + } + } + } + } + my @prefs_order = ('rolecolors','login','ipaccess','defaults','wafproxy','passwords', + 'quotas','autoenroll','autoupdate','autocreate','directorysrch', + 'contacts','privacy','usercreation','selfcreation', + 'usermodification','scantron','requestcourses','requestauthor', + 'coursecategories','serverstatuses','helpsettings','coursedefaults', + 'ltitools','proctoring','selfenrollment','usersessions','ssl', + 'trust','lti'); my %existing; if (ref($domconfig{'loadbalancing'}) eq 'HASH') { %existing = %{$domconfig{'loadbalancing'}}; @@ -282,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, }, @@ -291,13 +309,36 @@ sub handler { help => 'Domain_Configuration_LangTZAuth', header => [{col1 => 'Setting', col2 => 'Value'}, - {col1 => 'Internal Authentication', - col2 => 'Value'}, {col1 => 'Institutional user types', col2 => 'Name displayed'}], print => \&print_defaults, modify => \&modify_defaults, }, + 'wafproxy' => + { text => 'Web Application Firewall/Reverse Proxy', + help => 'Domain_Configuration_WAF_Proxy', + header => [{col1 => 'Domain(s)', + col2 => 'Servers and WAF/Reverse Proxy alias(es)', + }, + {col1 => 'Domain(s)', + col2 => 'WAF Configuration',}], + print => \&print_wafproxy, + modify => \&modify_wafproxy, + }, + 'passwords' => + { text => 'Passwords (Internal authentication)', + help => 'Domain_Configuration_Passwords', + header => [{col1 => 'Resetting Forgotten Password', + col2 => 'Settings'}, + {col1 => 'Encryption of Stored Passwords (Internal Auth)', + col2 => 'Settings'}, + {col1 => 'Rules for LON-CAPA Passwords', + col2 => 'Settings'}, + {col1 => 'Course Owner Changing Student Passwords', + col2 => 'Settings'}], + print => \&print_passwords, + modify => \&modify_passwords, + }, 'quotas' => { text => 'Blogs, personal web pages, webDAV/quotas, portfolios', help => 'Domain_Configuration_Quotas', @@ -352,6 +393,8 @@ sub handler { col2 => 'Value',}, {col1 => 'Recipient(s) for notifications', col2 => 'Value',}, + {col1 => 'Nightly status check e-mail', + col2 => 'Settings',}, {col1 => 'Ask helpdesk form settings', col2 => 'Value',},], print => \&print_contacts, @@ -392,11 +435,12 @@ sub handler { modify => \&modify_usermodification, }, 'scantron' => - { text => 'Bubblesheet format file', + { text => 'Bubblesheet format', help => 'Domain_Configuration_Scantron_Format', - header => [ {col1 => 'Item', - col2 => '', - }], + header => [ {col1 => 'Bubblesheet format file', + col2 => ''}, + {col1 => 'Bubblesheet data upload formats', + col2 => 'Settings'}], print => \&print_scantron, modify => \&modify_scantron, }, @@ -482,10 +526,16 @@ sub handler { modify => \&modify_selfenrollment, }, 'privacy' => - {text => 'User Privacy', + {text => 'Availability of User Information', help => 'Domain_Configuration_User_Privacy', - header => [{col1 => 'Setting', - col2 => 'Value',}], + header => [{col1 => 'Role assigned in different domain', + col2 => 'Approval options'}, + {col1 => 'Role assigned in different domain to user of type', + col2 => 'User information available in that domain'}, + {col1 => "Role assigned in user's domain", + col2 => 'Information viewable by privileged user'}, + {col1 => "Role assigned in user's domain", + col2 => 'Information viewable by unprivileged user'}], print => \&print_privacy, modify => \&modify_privacy, }, @@ -520,6 +570,14 @@ sub handler { print => \&print_ltitools, modify => \&modify_ltitools, }, + 'proctoring' => + {text => 'Remote Proctoring Integration', + help => 'Domain_Configuration_Proctoring', + header => [{col1 => 'Name', + col2 => 'Configuration'}], + print => \&print_proctoring, + modify => \&modify_proctoring, + }, 'ssl' => {text => 'LON-CAPA Network (SSL)', help => 'Domain_Configuration_Network_SSL', @@ -566,6 +624,14 @@ sub handler { print => \&print_lti, modify => \&modify_lti, }, + 'ipaccess' => + {text => 'IP-based access control', + help => 'Domain_Configuration_IP_Access', + header => [{col1 => 'Setting', + col2 => 'Value'},], + print => \&print_ipaccess, + modify => \&modify_ipaccess, + }, ); if (keys(%servers) > 1) { $prefs{'login'} = { text => 'Log-in page options', @@ -577,7 +643,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, }; @@ -618,6 +687,8 @@ $javascript_validations $coursebrowserjs END + } elsif (grep(/^ipaccess$/,@actions)) { + $js .= &Apache::loncommon::coursebrowser_javascript($env{'request.role.domain'}); } if (grep(/^selfcreation$/,@actions)) { $js .= &selfcreate_javascript(); @@ -625,6 +696,9 @@ END if (grep(/^contacts$/,@actions)) { $js .= &contacts_javascript(); } + if (grep(/^scantron$/,@actions)) { + $js .= &scantron_javascript(); + } &Apache::lonconfigsettings::display_settings($r,$dom,$phase,$context,\@prefs_order,\%prefs,\%domconfig,$confname,$js); } else { # check if domconfig user exists for the domain. @@ -747,12 +821,22 @@ sub process_changes { $output = &modify_loadbalancing($dom,%domconfig); } elsif ($action eq 'ltitools') { $output = &modify_ltitools($r,$dom,$action,$lastactref,%domconfig); + } elsif ($action eq 'proctoring') { + $output = &modify_proctoring($r,$dom,$action,$lastactref,%domconfig); } elsif ($action eq 'ssl') { $output = &modify_ssl($dom,$lastactref,%domconfig); } elsif ($action eq 'trust') { $output = &modify_trust($dom,$lastactref,%domconfig); } elsif ($action eq 'lti') { $output = &modify_lti($r,$dom,$action,$lastactref,%domconfig); + } elsif ($action eq 'privacy') { + $output = &modify_privacy($dom,%domconfig); + } elsif ($action eq 'passwords') { + $output = &modify_passwords($r,$dom,$confname,$lastactref,%domconfig); + } elsif ($action eq 'wafproxy') { + $output = &modify_wafproxy($dom,$action,$lastactref,%domconfig); + } elsif ($action eq 'ipaccess') { + $output = &modify_ipaccess($dom,$lastactref,%domconfig); } return $output; } @@ -765,6 +849,8 @@ sub print_config_box { $output = &coursecategories_javascript($settings); } elsif ($action eq 'defaults') { $output = &defaults_javascript($settings); + } elsif ($action eq 'passwords') { + $output = &passwords_javascript(); } elsif ($action eq 'helpsettings') { my (%privs,%levelscurrent); my %full=(); @@ -781,6 +867,22 @@ sub print_config_box { $output = &Apache::lonuserutils::custom_roledefs_js($context,$crstype,$formname,\%full, \@templateroles); + } elsif ($action eq 'ltitools') { + $output .= <itools_javascript($settings); + } elsif ($action eq 'lti') { + $output .= <i_javascript($settings); + } elsif ($action eq 'proctoring') { + $output .= &proctoring_javascript($settings); + } elsif ($action eq 'wafproxy') { + $output .= &wafproxy_javascript($dom); + } elsif ($action eq 'autoupdate') { + $output .= &autoupdate_javascript(); + } elsif ($action eq 'autoenroll') { + $output .= &autoenroll_javascript(); + } elsif ($action eq 'login') { + $output .= &saml_javascript(); + } elsif ($action eq 'ipaccess') { + $output .= &ipaccess_javascript($settings); } $output .= ' @@ -797,20 +899,24 @@ sub print_config_box { if ($numheaders > 1) { my $colspan = ''; my $rightcolspan = ''; + 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') { $rightcolspan = ' colspan="3"'; } + if ($action eq 'passwords') { + $leftnobr = ' LC_nobreak'; + } $output .= ' + + + + + '; } else { $output .= $item->{'print'}->('bottom',$dom,$settings,\$rowtotal); } @@ -907,8 +1047,10 @@ sub print_config_box { $rowtotal ++; } elsif (($action eq 'usermodification') || ($action eq 'coursedefaults') || ($action eq 'defaults') || ($action eq 'directorysrch') || - ($action eq 'helpsettings')) { + ($action eq 'helpsettings') || ($action eq 'wafproxy')) { $output .= $item->{'print'}->('bottom',$dom,$settings,\$rowtotal); + } elsif ($action eq 'scantron') { + $output .= $item->{'print'}->($r,'bottom',$dom,$confname,$settings,\$rowtotal); } elsif ($action eq 'ssl') { $output .= $item->{'print'}->('connto',$dom,$settings,\$rowtotal).'
- + '; $rowtotal ++; @@ -818,12 +924,16 @@ sub print_config_box { ($action eq 'usermodification') || ($action eq 'defaults') || ($action eq 'coursedefaults') || ($action eq 'selfenrollment') || ($action eq 'usersessions') || ($action eq 'ssl') || ($action eq 'directorysrch') || ($action eq 'trust') || ($action eq 'helpsettings') || - ($action eq 'contacts')) { + ($action eq 'contacts') || ($action eq 'privacy') || ($action eq 'wafproxy')) { $output .= $item->{'print'}->('top',$dom,$settings,\$rowtotal); + } elsif ($action eq 'passwords') { + $output .= $item->{'print'}->('top',$dom,$confname,$settings,\$rowtotal); } elsif ($action eq 'coursecategories') { $output .= $item->{'print'}->('top',$dom,$item,$settings,\$rowtotal); + } 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 { @@ -849,12 +959,15 @@ sub print_config_box { if (($action eq 'autoupdate') || ($action eq 'usercreation') || ($action eq 'selfcreation') || ($action eq 'selfenrollment') || ($action eq 'usersessions') || ($action eq 'coursecategories') || - ($action eq 'trust') || ($action eq 'contacts') || ($action eq 'defaults')) { + ($action eq 'trust') || ($action eq 'contacts') || + ($action eq 'privacy') || ($action eq 'passwords')) { if ($action eq 'coursecategories') { $output .= &print_coursecategories('middle',$dom,$item,$settings,\$rowtotal); $colspan = ' colspan="2"'; } elsif ($action eq 'trust') { $output .= $item->{'print'}->('shared',$dom,$settings,\$rowtotal); + } elsif ($action eq 'passwords') { + $output .= $item->{'print'}->('middle',$dom,$confname,$settings,\$rowtotal); } else { $output .= $item->{'print'}->('middle',$dom,$settings,\$rowtotal); } @@ -900,6 +1013,33 @@ sub print_config_box { '."\n"; if ($action eq 'coursecategories') { $output .= &print_coursecategories('bottom',$dom,$item,$settings,\$rowtotal); + } elsif (($action eq 'contacts') || ($action eq 'privacy') || ($action eq 'passwords')) { + if ($action eq 'passwords') { + $output .= $item->{'print'}->('lower',$dom,$confname,$settings,\$rowtotal); + } else { + $output .= $item->{'print'}->('lower',$dom,$settings,\$rowtotal); + } + $output .= ' + +
'.&mt($item->{'header'}->[0]->{'col1'}).''.&mt($item->{'header'}->[0]->{'col1'}).' '.&mt($item->{'header'}->[0]->{'col2'}).'
+
+ + + + '."\n"; + if ($action eq 'passwords') { + $output .= $item->{'print'}->('bottom',$dom,$confname,$settings,\$rowtotal); + } else { + $output .= $item->{'print'}->('bottom',$dom,$settings,\$rowtotal); + } + $output .= ' +
'.&mt($item->{'header'}->[3]->{'col1'}).''.&mt($item->{'header'}->[3]->{'col2'}).'
+
@@ -932,7 +1074,7 @@ sub print_config_box { '.&mt($item->{'header'}->[3]->{'col2'}).''. $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).' @@ -956,7 +1098,7 @@ sub print_config_box { '; - if ($numheaders == 4) { + if ($numheaders == 5) { $output .= ' @@ -968,7 +1110,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 ++; @@ -1088,10 +1250,9 @@ sub print_config_box { $output .= &print_quotas($dom,$settings,\$rowtotal,$action); } elsif (($action eq 'autoenroll') || ($action eq 'autocreate') || ($action eq 'serverstatuses') || ($action eq 'loadbalancing') || - ($action eq 'ltitools') || ($action eq 'lti')) { + ($action eq 'ltitools') || ($action eq 'lti') || + ($action eq 'proctoring') || ($action eq 'ipaccess')) { $output .= $item->{'print'}->($dom,$settings,\$rowtotal); - } elsif ($action eq 'scantron') { - $output .= &print_scantronformat($r,$dom,$confname,$settings,\$rowtotal); } } $output .= ' @@ -1104,9 +1265,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'}; @@ -1193,6 +1357,7 @@ sub print_login { } } my @images = ('img','logo','domlogo','login'); + my @alttext = ('img','logo','domlogo'); my @logintext = ('textcol','bgcol'); my @bgs = ('pgbg','mainbg','sidebg'); my @links = ('link','alink','vlink'); @@ -1234,6 +1399,13 @@ sub print_login { $designs{'showlogo'}{$item} = $settings->{'showlogo'}{$item}; } } + foreach my $item (@alttext) { + if (ref($settings->{'alttext'}) eq 'HASH') { + if ($settings->{'alttext'}->{$item} ne '') { + $designs{'alttext'}{$item} = $settings->{'alttext'}{$item}; + } + } + } foreach my $item (@logintext) { if ($settings->{$item} ne '') { $designs{'logintext'}{$item} = $settings->{$item}; @@ -1300,18 +1472,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') { @@ -1408,14 +1572,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}.''; @@ -1436,7 +1592,89 @@ sub print_login { } else { $datatable .= ''; } - $datatable .= ''; + $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 .= '
 
'; } @@ -1475,10 +1713,205 @@ sub login_choices { headtag => "Custom markup", action => "Action", current => "Current", + samllanding => "Dual login?", + samloptions => "Options", + alttext => "Alt text", ); 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_ipaccess { + my ($dom,$settings,$rowtotal) = @_; + my $css_class; + my $itemcount = 0; + my $datatable; + my %ordered; + if (ref($settings) eq 'HASH') { + 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; + } + } + } + my $maxnum = scalar(keys(%ordered)); + if (keys(%ordered)) { + my @items = sort { $a <=> $b } keys(%ordered); + for (my $i=0; $i<@items; $i++) { + $css_class = $itemcount%2?' class="LC_odd_row"':''; + my $item = $ordered{$items[$i]}; + my ($name,$ipranges,%commblocks,%courses); + if (ref($settings->{$item}) eq 'HASH') { + $name = $settings->{$item}->{'name'}; + $ipranges = $settings->{$item}->{'ip'}; + if (ref($settings->{$item}->{'commblocks'}) eq 'HASH') { + %commblocks = %{$settings->{$item}->{'commblocks'}}; + } + if (ref($settings->{$item}->{'courses'}) eq 'HASH') { + %courses = %{$settings->{$item}->{'courses'}}; + } + } + my $chgstr = ' onchange="javascript:reorderIPaccess(this.form,'."'ipaccess_pos_".$item."'".');"'; + $datatable .= '' + .''.(' 'x2). + ''. + ''. + &ipaccess_options($i,$itemcount,$dom,$name,$ipranges,\%commblocks,\%courses). + ''; + $itemcount ++; + } + } + $css_class = $itemcount%2?' class="LC_odd_row"':''; + my $chgstr = ' onchange="javascript:reorderIPaccess(this.form,'."'ipaccess_pos_add'".');"'; + $datatable .= ''."\n". + ''."\n". + ' '."\n". + ''.&mt('Add').''."\n". + ''. + &ipaccess_options('add',$itemcount,$dom). + ''."\n". + ''."\n"; + $$rowtotal ++; + return $datatable; +} + +sub ipaccess_options { + my ($num,$itemcount,$dom,$name,$ipranges,$blocksref,$coursesref) = @_; + my (%currblocks,%currcourses,$output); + if (ref($blocksref) eq 'HASH') { + %currblocks = %{$blocksref}; + } + if (ref($coursesref) eq 'HASH') { + %currcourses = %{$coursesref}; + } + $output = '
'.&mt('Location(s)').''. + ''.&mt('Name').': '. + ''. + '
'. + '
'.&mt('IP Range(s)').''. + &mt('Format for each IP range').': '.&mt('A.B.C.D/N or A.B.C.D-E.F.G.H').'
'. + &mt('Range(s) will be stored as IP netblock(s) in CIDR notation (comma separated)').'
'. + '
'. + '
'.&mt('Functionality Blocked?').''. + &blocker_checkboxes($num,$blocksref).'
'. + '
'.&mt('Courses/Communities allowed').''. + ''; + foreach my $cid (sort(keys(%currcourses))) { + my %courseinfo = &Apache::lonnet::coursedescription($cid,{'one_time' => 1}); + $output .= ''; + } + $output .= '
'. + ''. + ' ('.$cid.')
'.&mt('Add').': '. + ''. + &Apache::loncommon::selectcourse_link('display','ipaccess_cnum_'.$num,'ipaccess_cdom_'.$num,'ipaccess_cdesc_'.$num,$dom,undef,'Course/Community'). + ''. + ''. + '
'."\n". + '
'; + return $output; +} + +sub blocker_checkboxes { + my ($num,$blocks) = @_; + my ($typeorder,$types) = &commblocktype_text(); + my $numinrow = 6; + my $output = ''; + for (my $i=0; $i<@{$typeorder}; $i++) { + my $block = $typeorder->[$i]; + my $blockstatus; + if (ref($blocks) eq 'HASH') { + if ($blocks->{$block} eq 'on') { + $blockstatus = 'checked="checked"'; + } + } + my $rem = $i%($numinrow); + if ($rem == 0) { + if ($i > 0) { + $output .= ''; + } + $output .= ''; + } + if ($i == scalar(@{$typeorder})-1) { + my $colsleft = $numinrow-$rem; + if ($colsleft > 1) { + $output .= ''; + } + $output .= '
'; + } else { + $output .= ''; + } + } else { + $output .= ''; + } + my $item = 'ipaccess_block_'.$num; + if ($blockstatus) { + $blockstatus = ' '.$blockstatus; + } + $output .= ''."\n". + '
'; + return $output; +} + +sub commblocktype_text { + my %types = &Apache::lonlocal::texthash( + 'com' => 'Messaging', + 'chat' => 'Chat Room', + 'boards' => 'Discussion', + 'port' => 'Portfolio', + 'groups' => 'Groups', + 'blogs' => 'Blogs', + 'about' => 'User Information', + 'printout' => 'Printouts', + 'passwd' => 'Change Password', + 'grades' => 'Gradebook', + 'search' => 'Course search', + 'wishlist' => 'Stored links', + 'annotate' => 'Annotations', + ); + my $typeorder = ['com','chat','boards','port','groups','blogs','about','wishlist','printout','grades','search','annotate','passwd']; + return ($typeorder,\%types); +} + sub print_rolecolors { my ($phase,$role,$dom,$confname,$settings,$rowtotal) = @_; my %choices = &color_font_choices(); @@ -1596,7 +2029,7 @@ sub display_color_options { my $datatable = ''. ''.$choices->{'font'}.''; if (!$is_custom->{'font'}) { - $datatable .= ''.&mt('Default in use:').' '.$defaults->{'font'}.''; + $datatable .= ''.&mt('Default in use:').' '.$defaults->{'font'}.''; } else { $datatable .= ' '; } @@ -1605,12 +2038,12 @@ sub display_color_options { $datatable .= ''. ' '. - ' '; + ' '; unless ($role eq 'login') { $datatable .= ''. ''.$choices->{'fontmenu'}.''; if (!$is_custom->{'fontmenu'}) { - $datatable .= ''.&mt('Default in use:').' '.$defaults->{'fontmenu'}.''; + $datatable .= ''.&mt('Default in use:').' '.$defaults->{'fontmenu'}.''; } else { $datatable .= ' '; } @@ -1620,7 +2053,7 @@ sub display_color_options { ' '. - ' '; + ' '; } my $switchserver = &check_switchserver($dom,$confname); foreach my $img (@{$images}) { @@ -1628,7 +2061,7 @@ sub display_color_options { $css_class = $itemcount%2?' class="LC_odd_row"':''; $datatable .= ''. ''.$choices->{$img}; - my ($imgfile,$img_import,$login_hdr_pick,$logincolors); + my ($imgfile,$img_import,$login_hdr_pick,$logincolors,$alttext); if ($role eq 'login') { if ($img eq 'login') { $login_hdr_pick = @@ -1636,8 +2069,13 @@ sub display_color_options { $logincolors = &login_text_colors($img,$role,$logintext,$phase,$choices, $designs,$defaults); - } elsif ($img ne 'domlogo') { - $datatable.= &logo_display_options($img,$defaults,$designs); + } else { + if ($img ne 'domlogo') { + $datatable.= &logo_display_options($img,$defaults,$designs); + } + if (ref($designs->{'alttext'}) eq 'HASH') { + $alttext = $designs->{'alttext'}{$img}; + } } } $datatable .= ''; @@ -1729,6 +2167,11 @@ sub display_color_options { $datatable .=' '; } } + if (($role eq 'login') && ($img ne 'login')) { + $datatable .= (' ' x2).' '; + } $datatable .= ''; } $itemcount ++; @@ -1738,7 +2181,7 @@ sub display_color_options { my $bgs_def; foreach my $item (@{$bgs}) { if (!$is_custom->{$item}) { - $bgs_def .= ''.$choices->{$item}.'    
'.$defaults->{'bgs'}{$item}.''; + $bgs_def .= ''.$choices->{$item}.'    
'.$defaults->{'bgs'}{$item}.''; } } if ($bgs_def) { @@ -1766,7 +2209,7 @@ sub display_color_options { my $links_def; foreach my $item (@{$links}) { if (!$is_custom->{$item}) { - $links_def .= ''.$choices->{$item}.'
'.$defaults->{'links'}{$item}.''; + $links_def .= ''.$choices->{$item}.'
'.$defaults->{'links'}{$item}.''; } } if ($links_def) { @@ -1852,17 +2295,15 @@ sub image_changes { my ($is_custom,$alt_text,$img_import,$showfile,$fullsize,$role,$img,$imgfile,$logincolors) = @_; my $output; if ($img eq 'login') { - # suppress image for Log-in header + $output = ''.$logincolors; # suppress image for Log-in header } elsif (!$is_custom) { if ($img ne 'domlogo') { - $output .= &mt('Default image:').'
'; + $output = &mt('Default image:').'
'; } else { - $output .= &mt('Default in use:').'
'; + $output = &mt('Default in use:').'
'; } } - if ($img eq 'login') { # suppress image for Log-in header - $output .= ''.$logincolors; - } else { + if ($img ne 'login') { if ($img_import) { $output .= ''; } @@ -1893,7 +2334,7 @@ sub print_quotas { my $typecount = 0; my ($css_class,%titles); if ($context eq 'requestcourses') { - @usertools = ('official','unofficial','community','textbook','placement'); + @usertools = ('official','unofficial','community','textbook','placement','lti'); @options =('norequest','approval','validate','autolimit'); %validations = &Apache::lonnet::auto_courserequest_checks($dom); %titles = &courserequest_titles(); @@ -2357,7 +2798,7 @@ sub print_studentcode { my ($settings,$rowtotal) = @_; my $rownum = 0; my ($output,%current); - my @crstypes = ('official','unofficial','community','textbook','placement'); + my @crstypes = ('official','unofficial','community','textbook','placement','lti'); if (ref($settings) eq 'HASH') { if (ref($settings->{'uniquecode'}) eq 'HASH') { foreach my $type (@crstypes) { @@ -2484,7 +2925,7 @@ sub print_textbookcourses { $datatable .= ''; } $datatable .= ' '."\n". - ''.&mt('Add').''."\n". + ''.&mt('Add').''."\n". ''. ''.&mt('Subject:').' '."\n". (' 'x2). @@ -2501,13 +2942,13 @@ sub print_textbookcourses { } else { $datatable .= ''; } + $datatable .= ''."\n"; } - $datatable .= ''."\n". - ''.&mt('LON-CAPA course:').' '. + $datatable .= ''.&mt('LON-CAPA course:').' '. &Apache::loncommon::select_dom_form($env{'request.role.domain'},$type.'_addbook_cdom'). ''. &Apache::loncommon::selectcourse_link - ('display',$type.'_addbook_cnum',$type.'_addbook_cdom',undef,undef,undef,'Course'); + ('display',$type.'_addbook_cnum',$type.'_addbook_cdom',undef,undef,undef,'Course'). ''."\n". ''."\n"; $itemcount ++; @@ -2733,6 +3174,217 @@ function toggleLTITools(form,setting,ite ENDSCRIPT } +sub wafproxy_javascript { + my ($dom) = @_; + return <<"ENDSCRIPT"; + + +ENDSCRIPT +} + +sub proctoring_javascript { + my ($settings) = @_; + my (%ordered,$total,%jstext); + $total = 0; + if (ref($settings) eq 'HASH') { + foreach my $item (keys(%{$settings})) { + if (ref($settings->{$item}) eq 'HASH') { + my $num = $settings->{$item}{'order'}; + $ordered{$num} = $item; + } + } + $total = scalar(keys(%{$settings})); + } else { + %ordered = ( + 0 => 'proctorio', + 1 => 'examity', + ); + $total = 2; + } + my @jsarray = (); + foreach my $item (sort {$a <=> $b } (keys(%ordered))) { + push(@jsarray,$ordered{$item}); + } + my $jstext = ' var proctors = Array('."'".join("','",@jsarray)."'".');'."\n"; + return <<"ENDSCRIPT"; + + +ENDSCRIPT +} + + sub lti_javascript { my ($settings) = @_; my $togglejs = <i_toggle_js(); @@ -2740,14 +3392,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}); @@ -2806,32 +3460,109 @@ ENDSCRIPT } sub lti_toggle_js { + my %lcauthparmtext = &Apache::lonlocal::texthash ( + localauth => 'Local auth argument', + krb => 'Kerberos domain', + ); + my $crsincalert = &mt('"User\'s identity sent" needs to be set to "Yes" first,[_1] before setting "Course\'s identity sent" to "Yes"',"\n"); + &js_escape(\$crsincalert); return <<"ENDSCRIPT"; + +ENDSCRIPT +} + +sub autoupdate_javascript { + return <<"ENDSCRIPT"; + + +ENDSCRIPT +} + +sub autoenroll_javascript { + return <<"ENDSCRIPT"; + + +ENDSCRIPT +} + +sub saml_javascript { + return <<"ENDSCRIPT"; + + +ENDSCRIPT +} + +sub ipaccess_javascript { + my ($settings) = @_; + my (%ordered,$total,%jstext); + $total = 0; + if (ref($settings) eq 'HASH') { + foreach my $item (keys(%{$settings})) { + if (ref($settings->{$item}) eq 'HASH') { + my $num = $settings->{$item}{'order'}; + $ordered{$num} = $item; + } + } + $total = scalar(keys(%{$settings})); + } + my @jsarray = (); + foreach my $item (sort {$a <=> $b } (keys(%ordered))) { + push(@jsarray,$ordered{$item}); + } + my $jstext = ' var ipaccess = Array('."'".join("','",@jsarray)."'".');'."\n"; + return <<"ENDSCRIPT"; + + +ENDSCRIPT + +} + sub print_scantronformat { my ($r,$dom,$confname,$settings,$rowtotal) = @_; my $itemcount = 1; @@ -7600,8 +10473,8 @@ sub print_scantronformat { if ($configuserok eq 'ok') { if ($author_ok eq 'ok') { my %legacyfile = ( - default => $Apache::lonnet::perlvar{'lonTabDir'}.'/default_scantronformat.tab', - custom => $Apache::lonnet::perlvar{'lonTabDir'}.'/scantronformat.tab', + default => $Apache::lonnet::perlvar{'lonTabDir'}.'/default_scantronformat.tab', + custom => $Apache::lonnet::perlvar{'lonTabDir'}.'/scantronformat.tab', ); my %md5chk; foreach my $type (keys(%legacyfile)) { @@ -7610,7 +10483,7 @@ sub print_scantronformat { } if ($md5chk{'default'} ne $md5chk{'custom'}) { foreach my $type (keys(%legacyfile)) { - ($scantronurls{$type},my $error) = + ($scantronurls{$type},my $error) = &legacy_scantronformat($r,$dom,$confname, $type,$legacyfile{$type}, $scantronurls{$type}, @@ -7621,13 +10494,13 @@ sub print_scantronformat { } if (keys(%error) == 0) { $is_custom = 1; - $confhash{'scantron'}{'scantronformat'} = + $confhash{'scantron'}{'scantronformat'} = $scantronurls{'custom'}; - my $putresult = + my $putresult = &Apache::lonnet::put_dom('configuration', \%confhash,$dom); if ($putresult ne 'ok') { - $error{'custom'} = + $error{'custom'} = ''. &mt('An error occurred updating the domain configuration: [_1]',$putresult).''; } @@ -7747,6 +10620,129 @@ sub legacy_scantronformat { return ($url,$error); } +sub print_scantronconfig { + my ($dom,$settings,$rowtotal) = @_; + my $itemcount = 2; + my $is_checked = ' checked="checked"'; + my %optionson = ( + hdr => ' checked="checked"', + pad => ' checked="checked"', + rem => ' checked="checked"', + ); + my %optionsoff = ( + hdr => '', + pad => '', + rem => '', + ); + my $currcsvsty = 'none'; + my ($datatable,%csvfields,%checked,%onclick,%csvoptions); + my @fields = &scantroncsv_fields(); + my %titles = &scantronconfig_titles(); + if (ref($settings) eq 'HASH') { + if (ref($settings->{config}) eq 'HASH') { + if ($settings->{config}->{dat}) { + $checked{'dat'} = $is_checked; + } + if (ref($settings->{config}->{csv}) eq 'HASH') { + if (ref($settings->{config}->{csv}->{fields}) eq 'HASH') { + %csvfields = %{$settings->{config}->{csv}->{fields}}; + if (keys(%csvfields) > 0) { + $checked{'csv'} = $is_checked; + $currcsvsty = 'block'; + } + } + if (ref($settings->{config}->{csv}->{options}) eq 'HASH') { + %csvoptions = %{$settings->{config}->{csv}->{options}}; + foreach my $option (keys(%optionson)) { + unless ($csvoptions{$option}) { + $optionsoff{$option} = $optionson{$option}; + $optionson{$option} = ''; + } + } + } + } + } else { + $checked{'dat'} = $is_checked; + } + } else { + $checked{'dat'} = $is_checked; + } + $onclick{'csv'} = ' onclick="toggleScantron(this.form);"'; + my $css_class = $itemcount%2? ' class="LC_odd_row"':''; + $datatable = ''.&mt('Supported formats').''. + ''; + foreach my $item ('dat','csv') { + my $id; + if ($item eq 'csv') { + $id = 'id="scantronconfcsv" '; + } + $datatable .= ''.(' 'x3); + if ($item eq 'csv') { + $datatable .= '
'. + ''.&mt('CSV Column Mapping').''. + ''."\n"; + foreach my $col (@fields) { + my $selnone; + if ($csvfields{$col} eq '') { + $selnone = ' selected="selected"'; + } + $datatable .= ''. + ''; + } + $datatable .= '
'.&mt('Field').''.&mt('Location').'
'.$titles{$col}.'
'. + '
'. + ''.&mt('CSV Options').''; + foreach my $option ('hdr','pad','rem') { + $datatable .= ''.$titles{$option}.':'. + ''.(' 'x2)."\n". + '
'; + } + $datatable .= '
'; + $itemcount ++; + } + } + $datatable .= ''; + $$rowtotal ++; + return $datatable; +} + +sub scantronconfig_titles { + return &Apache::lonlocal::texthash( + dat => 'Standard format (.dat)', + csv => 'Comma separated values (.csv)', + hdr => 'Remove first line in file (contains column titles)', + pad => 'Prepend 0s to PaperID', + rem => 'Remove leading spaces (except Question Response columns)', + CODE => 'CODE', + ID => 'Student ID', + PaperID => 'Paper ID', + FirstName => 'First Name', + LastName => 'Last Name', + FirstQuestion => 'First Question Response', + Section => 'Section', + ); +} + +sub scantroncsv_fields { + return ('PaperID','LastName','FirstName','ID','Section','CODE','FirstQuestion'); +} + sub print_coursecategories { my ($position,$dom,$hdritem,$settings,$rowtotal) = @_; my $datatable; @@ -7789,7 +10785,7 @@ sub print_coursecategories { ''.$lt{$type}.' '; } - $datatable .= ''; + $datatable .= '
'; $itemcount ++; } $$rowtotal += $itemcount; @@ -7875,7 +10871,7 @@ sub print_coursecategories { $can_catcomm_dom.' value="dom" />'.$level{'dom'}.' '. '
'. - ''. + ''. ''.$title{'togglecatsplace'}.''. ''.&mt('Add subcategory:').''; + $text .= ''.&mt('Add subcategory:').''; } } } @@ -8426,7 +11484,7 @@ sub build_category_rows { sub modifiable_userdata_row { my ($context,$item,$settings,$numinrow,$rowcount,$usertypes,$fieldsref,$titlesref, - $rowid,$customcss,$rowstyle) = @_; + $rowid,$customcss,$rowstyle,$itemdesc) = @_; my ($role,$rolename,$statustype); $role = $item; if ($context eq 'cancreate') { @@ -8447,6 +11505,10 @@ sub modifiable_userdata_row { } else { $rolename = $role; } + } elsif ($context eq 'lti') { + $rolename = &mt('Institutional data used (if available)'); + } elsif ($context eq 'privacy') { + $rolename = $itemdesc; } else { if ($role eq 'cr') { $rolename = &mt('Custom role'); @@ -8489,30 +11551,39 @@ sub modifiable_userdata_row { ''; my $rem; my %checks; + my %current; if (ref($settings) eq 'HASH') { - if (ref($settings->{$context}) eq 'HASH') { + my $hashref; + if ($context eq 'lti') { + if (ref($settings) eq 'HASH') { + $hashref = $settings->{'instdata'}; + } + } elsif ($context eq 'privacy') { + my ($key,$inner) = split(/_/,$role); + if (ref($settings) eq 'HASH') { + if (ref($settings->{$key}) eq 'HASH') { + $hashref = $settings->{$key}->{$inner}; + } + } + } elsif (ref($settings->{$context}) eq 'HASH') { if (ref($settings->{$context}->{$role}) eq 'HASH') { - my $hashref = $settings->{$context}->{$role}; - if ($role eq 'emailusername') { - if ($statustype) { - if (ref($settings->{$context}->{$role}->{$statustype}) eq 'HASH') { - $hashref = $settings->{$context}->{$role}->{$statustype}; - if (ref($hashref) eq 'HASH') { - foreach my $field (@fields) { - if ($hashref->{$field}) { - $checks{$field} = $hashref->{$field}; - } - } - } - } + $hashref = $settings->{'lti_instdata'}; + } + if ($role eq 'emailusername') { + if ($statustype) { + if (ref($settings->{$context}->{$role}->{$statustype}) eq 'HASH') { + $hashref = $settings->{$context}->{$role}->{$statustype}; } - } else { - if (ref($hashref) eq 'HASH') { - foreach my $field (@fields) { - if ($hashref->{$field}) { - $checks{$field} = ' checked="checked" '; - } - } + } + } + } + if (ref($hashref) eq 'HASH') { + foreach my $field (@fields) { + if ($hashref->{$field}) { + if ($role eq 'emailusername') { + $checks{$field} = $hashref->{$field}; + } else { + $checks{$field} = ' checked="checked" '; } } } @@ -8531,8 +11602,26 @@ sub modifiable_userdata_row { my $check = ' '; unless ($role eq 'emailusername') { if (exists($checks{$fields[$i]})) { - $check = $checks{$fields[$i]} - } else { + $check = $checks{$fields[$i]}; + } elsif ($context eq 'privacy') { + if ($role =~ /^priv_(domain|course)$/) { + if (ref($settings) ne 'HASH') { + $check = ' checked="checked" '; + } + } elsif ($role =~ /^priv_(author|community)$/) { + if (ref($settings) ne 'HASH') { + unless ($fields[$i] eq 'id') { + $check = ' checked="checked" '; + } + } + } elsif ($role =~ /^(unpriv|othdom)_/) { + if (ref($settings) ne 'HASH') { + if (($fields[$i] eq 'lastname') || ($fields[$i] eq 'firstname')) { + $check = ' checked="checked" '; + } + } + } + } elsif ($context ne 'lti') { if ($role eq 'st') { if (ref($settings) ne 'HASH') { $check = ' checked="checked" '; @@ -8542,6 +11631,7 @@ sub modifiable_userdata_row { } $output .= ''; } if ($colsleft > 1) { @@ -8760,12 +11855,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') { @@ -8773,6 +11870,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); @@ -9019,6 +12130,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'; @@ -9059,6 +12253,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; } @@ -9138,6 +12357,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"). + '
      '; + foreach my $key ('text','img','alt','url','title','notsso') { + if ($currsaml{$lonhost}{$key} eq '') { + $resulttext .= '
    • '.&mt("$notlt{$key} not in use").'
    • '; + } else { + my $value = "'$currsaml{$lonhost}{$key}'"; + if ($key eq 'img') { + $value = ''; + } + $resulttext .= '
    • '.&mt("$notlt{$key} set to: [_1]", + $value).'
    • '; + } + } + $resulttext .= '
  • '; + } else { + $resulttext .= '
  • '.&mt("$title{$item} not in use for [_1]",$lonhost).'
  • '; + } + } + } } elsif ($item eq 'captcha') { if (ref($loginhash{'login'}) eq 'HASH') { my $chgtxt; @@ -9240,6 +12491,283 @@ sub color_font_choices { return %choices; } +sub modify_ipaccess { + my ($dom,$lastactref,%domconfig) = @_; + my (@allpos,%changes,%confhash,$errors,$resulttext); + my (@items,%deletions,%itemids,@warnings); + my ($typeorder,$types) = &commblocktype_text(); + if ($env{'form.ipaccess_add'}) { + my $name = $env{'form.ipaccess_name_add'}; + my ($newid,$error) = &get_ipaccess_id($dom,$name); + if ($newid) { + $itemids{'add'} = $newid; + push(@items,'add'); + $changes{$newid} = 1; + } else { + $error = &mt('Failed to acquire unique ID for new IP access control item'); + $errors .= '
  • '.$error.'
  • '; + } + } + if (ref($domconfig{'ipaccess'}) eq 'HASH') { + my @todelete = &Apache::loncommon::get_env_multiple('form.ipaccess_del'); + if (@todelete) { + map { $deletions{$_} = 1; } @todelete; + } + my $maxnum = $env{'form.ipaccess_maxnum'}; + for (my $i=0; $i<$maxnum; $i++) { + my $itemid = $env{'form.ipaccess_id_'.$i}; + $itemid =~ s/\D+//g; + if (ref($domconfig{'ipaccess'}{$itemid}) eq 'HASH') { + if ($deletions{$itemid}) { + $changes{$itemid} = $domconfig{'ipaccess'}{$itemid}{'name'}; + } else { + push(@items,$i); + $itemids{$i} = $itemid; + } + } + } + } + foreach my $idx (@items) { + my $itemid = $itemids{$idx}; + next unless ($itemid); + my %current; + unless ($idx eq 'add') { + if (ref($domconfig{'ipaccess'}{$itemid}) eq 'HASH') { + %current = %{$domconfig{'ipaccess'}{$itemid}}; + } + } + my $position = $env{'form.ipaccess_pos_'.$itemid}; + $position =~ s/\D+//g; + if ($position ne '') { + $allpos[$position] = $itemid; + } + my $name = $env{'form.ipaccess_name_'.$idx}; + $name =~ s/^\s+|\s+$//g; + $confhash{$itemid}{'name'} = $name; + my $possrange = $env{'form.ipaccess_range_'.$idx}; + $possrange =~ s/^\s+|\s+$//g; + unless ($possrange eq '') { + $possrange =~ s/[\r\n]+/\s/g; + $possrange =~ s/\s*-\s*/-/g; + $possrange =~ s/\s+/,/g; + $possrange =~ s/,+/,/g; + if ($possrange ne '') { + my (@ok,$count); + $count = 0; + foreach my $poss (split(/\,/,$possrange)) { + $count ++; + $poss = &validate_ip_pattern($poss); + if ($poss ne '') { + push(@ok,$poss); + } + } + my $diff = $count - scalar(@ok); + if ($diff) { + $errors .= '
  • '. + &mt('[quant,_1,IP] invalid and excluded from saved value for IP range(s) for [_2]', + $diff,$name). + '
  • '; + } + if (@ok) { + my @cidr_list; + foreach my $item (@ok) { + @cidr_list = &Net::CIDR::cidradd($item,@cidr_list); + } + $confhash{$itemid}{'ip'} = join(',',@cidr_list); + } + } + } + foreach my $field ('name','ip') { + unless (($idx eq 'add') || ($changes{$itemid})) { + if ($current{$field} ne $confhash{$itemid}{$field}) { + $changes{$itemid} = 1; + last; + } + } + } + $confhash{$itemid}{'commblocks'} = {}; + + my %commblocks; + map { $commblocks{$_} = 1; } &Apache::loncommon::get_env_multiple('form.ipaccess_block_'.$idx); + foreach my $type (@{$typeorder}) { + if ($commblocks{$type}) { + $confhash{$itemid}{'commblocks'}{$type} = 'on'; + } + unless (($idx eq 'add') || ($changes{$itemid})) { + if (ref($current{'commblocks'}) eq 'HASH') { + if ($confhash{$itemid}{'commblocks'}{$type} ne $current{'commblocks'}{$type}) { + $changes{$itemid} = 1; + } + } elsif ($confhash{$itemid}{'commblocks'}{$type}) { + $changes{$itemid} = 1; + } + } + } + $confhash{$itemid}{'courses'} = {}; + my %crsdeletions; + my @delcrs = &Apache::loncommon::get_env_multiple('form.ipaccess_course_delete_'.$idx); + if (@delcrs) { + map { $crsdeletions{$_} = 1; } @delcrs; + } + if (ref($current{'courses'}) eq 'HASH') { + foreach my $cid (sort(keys(%{$current{'courses'}}))) { + if ($crsdeletions{$cid}) { + $changes{$itemid} = 1; + } else { + $confhash{$itemid}{'courses'}{$cid} = 1; + } + } + } + $env{'form.ipaccess_cnum_'.$idx} =~ s/^\s+|\s+$//g; + $env{'form.ipaccess_cdom_'.$idx} =~ s/^\s+|\s+$//g; + if (($env{'form.ipaccess_cnum_'.$idx} =~ /^$match_courseid$/) && + ($env{'form.ipaccess_cdom_'.$idx} =~ /^$match_domain$/)) { + if (&Apache::lonnet::homeserver($env{'form.ipaccess_cnum_'.$idx}, + $env{'form.ipaccess_cdom_'.$idx}) eq 'no_host') { + $errors .= '
  • '. + &mt('Invalid courseID [_1] omitted from list of allowed courses', + $env{'form.ipaccess_cdom_'.$idx}.'_'.$env{'form.ipaccess_cnum_'.$idx}). + '
  • '; + } else { + $confhash{$itemid}{'courses'}{$env{'form.ipaccess_cdom_'.$idx}.'_'.$env{'form.ipaccess_cnum_'.$idx}} = 1; + $changes{$itemid} = 1; + } + } + } + if (@allpos > 0) { + my $idx = 0; + foreach my $itemid (@allpos) { + if ($itemid ne '') { + $confhash{$itemid}{'order'} = $idx; + unless ($changes{$itemid}) { + if (ref($domconfig{'ipaccess'}) eq 'HASH') { + if (ref($domconfig{'ipaccess'}{$itemid}) eq 'HASH') { + if ($domconfig{'ipaccess'}{$itemid}{'order'} ne $idx) { + $changes{$itemid} = 1; + } + } + } + } + $idx ++; + } + } + } + if (keys(%changes)) { + my %defaultshash = ( + ipaccess => \%confhash, + ); + my $putresult = &Apache::lonnet::put_dom('configuration',\%defaultshash, + $dom); + if ($putresult eq 'ok') { + my $cachetime = 1800; + &Apache::lonnet::do_cache_new('ipaccess',$dom,\%confhash,$cachetime); + if (ref($lastactref) eq 'HASH') { + $lastactref->{'ipaccess'} = 1; + } + $resulttext = &mt('Changes made:').'
      '; + my %bynum; + foreach my $itemid (sort(keys(%changes))) { + if (ref($confhash{$itemid}) eq 'HASH') { + my $position = $confhash{$itemid}{'order'}; + if ($position =~ /^\d+$/) { + $bynum{$position} = $itemid; + } + } + } + if (keys(%deletions)) { + foreach my $itemid (sort { $a <=> $b } keys(%deletions)) { + $resulttext .= '
    • '.&mt('Deleted: [_1]',$changes{$itemid}).'
    • '; + } + } + foreach my $pos (sort { $a <=> $b } keys(%bynum)) { + my $itemid = $bynum{$pos}; + if (ref($confhash{$itemid}) eq 'HASH') { + $resulttext .= '
    • '.$confhash{$itemid}{'name'}.'
        '; + my $position = $pos + 1; + $resulttext .= '
      • '.&mt('Order: [_1]',$position).'
      • '; + if ($confhash{$itemid}{'ip'} eq '') { + $resulttext .= '
      • '.&mt('No IP Range(s) set').'
      • '; + } else { + $resulttext .= '
      • '.&mt('IP Range(s): [_1]',$confhash{$itemid}{'ip'}).'
      • '; + } + if (keys(%{$confhash{$itemid}{'commblocks'}})) { + $resulttext .= '
      • '.&mt('Functionality Blocked: [_1]', + join(', ', map { $types->{$_}; } sort(keys(%{$confhash{$itemid}{'commblocks'}})))). + '
      • '; + } else { + $resulttext .= '
      • '.&mt('No functionality blocked').'
      • '; + } + if (keys(%{$confhash{$itemid}{'courses'}})) { + my @courses; + foreach my $cid (sort(keys(%{$confhash{$itemid}{'courses'}}))) { + my %courseinfo = &Apache::lonnet::coursedescription($cid,{'one_time' => 1}); + push(@courses,$courseinfo{'description'}.' ('.$cid.')'); + } + $resulttext .= '
      • '.&mt('Courses/Communities allowed').':
        • '. + join('
        • ',@courses).'
        '; + } else { + $resulttext .= '
      • '.&mt('No courses allowed').'
      • '; + } + $resulttext .= '
    • '; + } + } + $resulttext .= '
    '; + } else { + $errors .= '
  • '.&mt('Failed to save changes').'
  • '; + } + } else { + $resulttext = &mt('No changes made'); + } + if ($errors) { + $resulttext .= '

    '.&mt('The following errors occurred: ').'

      '. + $errors.'

    '; + } + return $resulttext; +} + +sub get_ipaccess_id { + my ($domain,$location) = @_; + # get lock on ipaccess db + my $lockhash = { + lock => $env{'user.name'}. + ':'.$env{'user.domain'}, + }; + my $tries = 0; + my $gotlock = &Apache::lonnet::newput_dom('ipaccess',$lockhash,$domain); + my ($id,$error); + + while (($gotlock ne 'ok') && ($tries<10)) { + $tries ++; + sleep (0.1); + $gotlock = &Apache::lonnet::newput_dom('ipaccess',$lockhash,$domain); + } + if ($gotlock eq 'ok') { + my %currids = &Apache::lonnet::dump_dom('ipaccess',$domain); + if ($currids{'lock'}) { + delete($currids{'lock'}); + if (keys(%currids)) { + my @curr = sort { $a <=> $b } keys(%currids); + if ($curr[-1] =~ /^\d+$/) { + $id = 1 + $curr[-1]; + } + } else { + $id = 1; + } + if ($id) { + unless (&Apache::lonnet::newput_dom('ipaccess',{ $id => $location },$domain) eq 'ok') { + $error = 'nostore'; + } + } else { + $error = 'nonumber'; + } + } + my $dellockoutcome = &Apache::lonnet::del_dom('ipaccess',['lock'],$domain); + } else { + $error = 'nolock'; + } + return ($id,$error); +} + sub modify_rolecolors { my ($r,$dom,$confname,$roles,$lastactref,%domconfig) = @_; my ($resulttext,%rolehash); @@ -9347,13 +12875,18 @@ sub modify_colors { $domconfig->{$role} = {}; } foreach my $img (@images) { - if (($role eq 'login') && (($img eq 'img') || ($img eq 'logo'))) { - if (defined($env{'form.login_showlogo_'.$img})) { - $confhash->{$role}{'showlogo'}{$img} = 1; - } else { - $confhash->{$role}{'showlogo'}{$img} = 0; + if ($role eq 'login') { + if (($img eq 'img') || ($img eq 'logo')) { + if (defined($env{'form.login_showlogo_'.$img})) { + $confhash->{$role}{'showlogo'}{$img} = 1; + } else { + $confhash->{$role}{'showlogo'}{$img} = 0; + } } - } + if ($env{'form.login_alt_'.$img} ne '') { + $confhash->{$role}{'alttext'}{$img} = $env{'form.login_alt_'.$img}; + } + } if ( ! $env{'form.'.$role.'_'.$img.'.filename'} && !defined($domconfig->{$role}{$img}) && !$env{'form.'.$role.'_del_'.$img} @@ -9428,15 +12961,29 @@ sub modify_colors { $changes{$role}{'images'}{$img} = 1; } } - if (($role eq 'login') && (($img eq 'logo') || ($img eq 'img'))) { - if (ref($domconfig->{'login'}{'showlogo'}) eq 'HASH') { - if ($confhash->{$role}{'showlogo'}{$img} ne - $domconfig->{$role}{'showlogo'}{$img}) { - $changes{$role}{'showlogo'}{$img} = 1; + if ($role eq 'login') { + if (($img eq 'logo') || ($img eq 'img')) { + if (ref($domconfig->{'login'}{'showlogo'}) eq 'HASH') { + if ($confhash->{$role}{'showlogo'}{$img} ne + $domconfig->{$role}{'showlogo'}{$img}) { + $changes{$role}{'showlogo'}{$img} = 1; + } + } else { + if ($confhash->{$role}{'showlogo'}{$img} == 0) { + $changes{$role}{'showlogo'}{$img} = 1; + } } - } else { - if ($confhash->{$role}{'showlogo'}{$img} == 0) { - $changes{$role}{'showlogo'}{$img} = 1; + } + if ($img ne 'login') { + if (ref($domconfig->{$role}{'alttext'}) eq 'HASH') { + if ($confhash->{$role}{'alttext'}{$img} ne + $domconfig->{$role}{'alttext'}{$img}) { + $changes{$role}{'alttext'}{$img} = 1; + } + } else { + if ($confhash->{$role}{'alttext'}{$img} ne '') { + $changes{$role}{'alttext'}{$img} = 1; + } } } } @@ -9547,6 +13094,11 @@ sub default_change_checker { if ($confhash->{$role}{'showlogo'}{$img} == 0) { $changes->{$role}{'showlogo'}{$img} = 1; } + if (ref($confhash->{$role}{'alttext'}) eq 'HASH') { + if ($confhash->{$role}{'alttext'}{$img} ne '') { + $changes->{$role}{'alttext'}{$img} = 1; + } + } } } if ($confhash->{$role}{'font'}) { @@ -9585,6 +13137,13 @@ sub display_colorchgs { } else { $resulttext .= '
  • '.&mt("$choices{$item} set to not be displayed").'
  • '; } + } elsif (($role eq 'login') && ($key eq 'alttext')) { + if ($confhash->{$role}{$key}{$item} ne '') { + $resulttext .= '
  • '.&mt("$choices{$key for $choices{$item} set to [_1].", + $confhash->{$role}{$key}{$item}).'
  • '; + } else { + $resulttext .= '
  • '.&mt("$choices{$key} for $choices{$item} deleted.").'
  • '; + } } elsif ($confhash->{$role}{$item} eq '') { $resulttext .= '
  • '.&mt("$choices{$item} set to default").'
  • '; } else { @@ -9643,7 +13202,7 @@ sub check_configuser { my ($configuserok,%currroles); if ($uhome eq 'no_host') { srand( time() ^ ($$ + ($$ << 15)) ); # Seed rand. - my $configpass = &LONCAPA::Enrollment::create_password(); + my $configpass = &LONCAPA::Enrollment::create_password($dom); $configuserok = &Apache::lonnet::modifyuser($dom,$confname,'','internal', $configpass,'','','','','',undef,$servadm); @@ -9968,7 +13527,7 @@ sub modify_quotas { $context = $action; } if ($context eq 'requestcourses') { - @usertools = ('official','unofficial','community','textbook','placement'); + @usertools = ('official','unofficial','community','textbook','placement','lti'); @options =('norequest','approval','validate','autolimit'); %validations = &Apache::lonnet::auto_courserequest_checks($dom); %titles = &courserequest_titles(); @@ -10017,7 +13576,7 @@ sub modify_quotas { my @approvalnotify = &Apache::loncommon::get_env_multiple('form.'.$context.'notifyapproval'); @approvalnotify = sort(@approvalnotify); $confhash{'notify'}{'approval'} = join(',',@approvalnotify); - my @crstypes = ('official','unofficial','community','textbook','placement'); + my @crstypes = ('official','unofficial','community','textbook','placement','lti'); my @hasuniquecode = &Apache::loncommon::get_env_multiple('form.uniquecode'); foreach my $type (@hasuniquecode) { if (grep(/^\Q$type\E$/,@crstypes)) { @@ -10939,7 +14498,7 @@ sub modify_ltitools { if ($env{'form.ltitools_'.$extra.'_'.$i}) { $confhash{$itemid}{$extra} = 1; if ($env{'form.ltitools_'.$extra.'valid_'.$i} ne '') { - my $lifetime = $env{'form.ltitools_'.$extra.'valid_add'}; + my $lifetime = $env{'form.ltitools_'.$extra.'valid_'.$i}; $lifetime =~ s/^\s+|\s+$//g; if ($lifetime =~ /^\d+\.?\d*$/) { $confhash{$itemid}{$extra.'valid'} = $lifetime; @@ -11106,7 +14665,7 @@ sub modify_ltitools { my %ltienchash = ( $action => { %encconfig } ); - &Apache::lonnet::put_dom('encconfig',\%ltienchash,$dom); + &Apache::lonnet::put_dom('encconfig',\%ltienchash,$dom,undef,1); if (keys(%changes) > 0) { my $cachetime = 24*60*60; my %ltiall = %confhash; @@ -11166,7 +14725,7 @@ sub modify_ltitools { } } if (!$numconfig) { - $resulttext .= &mt('None'); + $resulttext .= ' '.&mt('None'); } $resulttext .= ''; foreach my $item ('passback','roster') { @@ -11345,6 +14904,536 @@ sub get_ltitools_id { return ($id,$error); } +sub modify_proctoring { + my ($r,$dom,$action,$lastactref,%domconfig) = @_; + my %domdefaults = &Apache::lonnet::get_domain_defaults($dom,1); + my (@allpos,%changes,%confhash,%encconfhash,$errors,$resulttext,%imgdeletions); + my $confname = $dom.'-domainconfig'; + my $servadm = $r->dir_config('lonAdmEMail'); + my ($configuserok,$author_ok,$switchserver) = &config_check($dom,$confname,$servadm); + my %providernames = &proctoring_providernames(); + my $maxnum = scalar(keys(%providernames)); + + my (%requserfields,%optuserfields,%defaults,%extended,%crsconf,@courseroles,@ltiroles); + my ($requref,$opturef,$defref,$extref,$crsref,$rolesref,$ltiref) = &proctoring_data(); + if (ref($requref) eq 'HASH') { + %requserfields = %{$requref}; + } + if (ref($opturef) eq 'HASH') { + %optuserfields = %{$opturef}; + } + if (ref($defref) eq 'HASH') { + %defaults = %{$defref}; + } + if (ref($extref) eq 'HASH') { + %extended = %{$extref}; + } + if (ref($crsref) eq 'HASH') { + %crsconf = %{$crsref}; + } + if (ref($rolesref) eq 'ARRAY') { + @courseroles = @{$rolesref}; + } + if (ref($ltiref) eq 'ARRAY') { + @ltiroles = @{$ltiref}; + } + + if (ref($domconfig{$action}) eq 'HASH') { + my @todeleteimages = &Apache::loncommon::get_env_multiple('form.proctoring_image_del'); + if (@todeleteimages) { + map { $imgdeletions{$_} = 1; } @todeleteimages; + } + } + my %customadds; + my @newcustom = &Apache::loncommon::get_env_multiple('form.proctoring_customadd'); + if (@newcustom) { + map { $customadds{$_} = 1; } @newcustom; + } + foreach my $provider (sort(keys(%providernames))) { + $confhash{$provider} = {}; + my $pos = $env{'form.proctoring_pos_'.$provider}; + $pos =~ s/\D+//g; + $allpos[$pos] = $provider; + my (%current,%currentenc); + my $showroles = 0; + if (ref($domconfig{$action}) eq 'HASH') { + if (ref($domconfig{$action}{$provider}) eq 'HASH') { + %current = %{$domconfig{$action}{$provider}}; + foreach my $item ('key','secret') { + $currentenc{$item} = $current{$item}; + delete($current{$item}); + } + } + } + if ($env{'form.proctoring_available_'.$provider}) { + $confhash{$provider}{'available'} = 1; + unless ($current{'available'}) { + $changes{$provider} = 1; + } + } else { + %{$confhash{$provider}} = %current; + %{$encconfhash{$provider}} = %currentenc; + $confhash{$provider}{'available'} = 0; + if ($current{'available'}) { + $changes{$provider} = 1; + } + } + if ($confhash{$provider}{'available'}) { + foreach my $field ('lifetime','version','sigmethod','url','key','secret') { + my $possval = $env{'form.proctoring_'.$provider.'_'.$field}; + if ($field eq 'lifetime') { + if ($possval =~ /^\d+$/) { + $confhash{$provider}{$field} = $possval; + } + } elsif ($field eq 'version') { + if ($possval =~ /^\d+\.\d+$/) { + $confhash{$provider}{$field} = $possval; + } + } elsif ($field eq 'sigmethod') { + if ($possval =~ /^\QHMAC-SHA\E(1|256)$/) { + $confhash{$provider}{$field} = $possval; + } + } elsif ($field eq 'url') { + $confhash{$provider}{$field} = $possval; + } elsif (($field eq 'key') || ($field eq 'secret')) { + $encconfhash{$provider}{$field} = $possval; + unless ($currentenc{$field} eq $possval) { + $changes{$provider} = 1; + } + } + unless (($field eq 'key') || ($field eq 'secret')) { + unless ($current{$field} eq $confhash{$provider}{$field}) { + $changes{$provider} = 1; + } + } + } + if ($imgdeletions{$provider}) { + $changes{$provider} = 1; + } elsif ($env{'form.proctoring_image_'.$provider.'.filename'} ne '') { + my ($imageurl,$error) = + &process_proctoring_image($r,$dom,$confname,'proctoring_image_'.$provider,$provider, + $configuserok,$switchserver,$author_ok); + if ($imageurl) { + $confhash{$provider}{'image'} = $imageurl; + $changes{$provider} = 1; + } + if ($error) { + &Apache::lonnet::logthis($error); + $errors .= '
  • '.$error.'
  • '; + } + } elsif (exists($current{'image'})) { + $confhash{$provider}{'image'} = $current{'image'}; + } + if (ref($requserfields{$provider}) eq 'ARRAY') { + if (@{$requserfields{$provider}} > 0) { + if (grep(/^user$/,@{$requserfields{$provider}})) { + if ($env{'form.proctoring_userincdom_'.$provider}) { + $confhash{$provider}{'incdom'} = 1; + } + unless ($current{'incdom'} eq $confhash{$provider}{'incdom'}) { + $changes{$provider} = 1; + } + } + if (grep(/^roles$/,@{$requserfields{$provider}})) { + $showroles = 1; + } + } + } + $confhash{$provider}{'fields'} = []; + if (ref($optuserfields{$provider}) eq 'ARRAY') { + if (@{$optuserfields{$provider}} > 0) { + my @optfields = &Apache::loncommon::get_env_multiple('form.proctoring_optional_'.$provider); + foreach my $field (@{$optuserfields{$provider}}) { + if (grep(/^\Q$field\E$/,@optfields)) { + push(@{$confhash{$provider}{'fields'}},$field); + } + } + } + if (ref($current{'fields'}) eq 'ARRAY') { + unless ($changes{$provider}) { + my @new = sort(@{$confhash{$provider}{'fields'}}); + my @old = sort(@{$current{'fields'}}); + my @diffs = &Apache::loncommon::compare_arrays(\@new,\@old); + if (@diffs) { + $changes{$provider} = 1; + } + } + } elsif (@{$confhash{$provider}{'fields'}}) { + $changes{$provider} = 1; + } + } + if (ref($defaults{$provider}) eq 'ARRAY') { + if (@{$defaults{$provider}} > 0) { + my %options; + if (ref($extended{$provider}) eq 'HASH') { + %options = %{$extended{$provider}}; + } + my @checked = &Apache::loncommon::get_env_multiple('form.proctoring_defaults_'.$provider); + foreach my $field (@{$defaults{$provider}}) { + if ((exists($options{$field})) && (ref($options{$field}) eq 'ARRAY')) { + my $poss = $env{'form.proctoring_defaults_'.$field.'_'.$provider}; + if (grep(/^\Q$poss\E$/,@{$options{$field}})) { + push(@{$confhash{$provider}{'defaults'}},$poss); + } + } elsif ((exists($options{$field})) && (ref($options{$field}) eq 'HASH')) { + foreach my $inner (keys(%{$options{$field}})) { + if (ref($options{$field}{$inner}) eq 'ARRAY') { + my $poss = $env{'form.proctoring_'.$inner.'_'.$provider}; + if (grep(/^\Q$poss\E$/,@{$options{$field}{$inner}})) { + $confhash{$provider}{'defaults'}{$inner} = $poss; + } + } else { + $confhash{$provider}{'defaults'}{$inner} = $env{'form.proctoring_'.$inner.'_'.$provider}; + } + } + } else { + if (grep(/^\Q$field\E$/,@checked)) { + push(@{$confhash{$provider}{'defaults'}},$field); + } + } + } + if (ref($confhash{$provider}{'defaults'}) eq 'ARRAY') { + if (ref($current{'defaults'}) eq 'ARRAY') { + unless ($changes{$provider}) { + my @new = sort(@{$confhash{$provider}{'defaults'}}); + my @old = sort(@{$current{'defaults'}}); + my @diffs = &Apache::loncommon::compare_arrays(\@new,\@old); + if (@diffs) { + $changes{$provider} = 1; + } + } + } elsif (ref($current{'defaults'}) eq 'ARRAY') { + if (@{$current{'defaults'}}) { + $changes{$provider} = 1; + } + } + } elsif (ref($confhash{$provider}{'defaults'}) eq 'HASH') { + if (ref($current{'defaults'}) eq 'HASH') { + unless ($changes{$provider}) { + foreach my $key (keys(%{$confhash{$provider}{'defaults'}})) { + unless ($confhash{$provider}{'defaults'}{$key} eq $current{'defaults'}{$key}) { + $changes{$provider} = 1; + last; + } + } + } + unless ($changes{$provider}) { + foreach my $key (keys(%{$current{'defaults'}})) { + unless ($current{'defaults'}{$key} eq $confhash{$provider}{'defaults'}{$key}) { + $changes{$provider} = 1; + last; + } + } + } + } elsif (keys(%{$confhash{$provider}{'defaults'}})) { + $changes{$provider} = 1; + } + } + } + } + if (ref($crsconf{$provider}) eq 'ARRAY') { + if (@{$crsconf{$provider}} > 0) { + $confhash{$provider}{'crsconf'} = []; + my @checked = &Apache::loncommon::get_env_multiple('form.proctoring_crsconf_'.$provider); + foreach my $crsfield (@{$crsconf{$provider}}) { + if (grep(/^\Q$crsfield\E$/,@checked)) { + push(@{$confhash{$provider}{'crsconf'}},$crsfield); + } + } + if (ref($current{'crsconf'}) eq 'ARRAY') { + unless ($changes{$provider}) { + my @new = sort(@{$confhash{$provider}{'crsconf'}}); + my @old = sort(@{$current{'crsconf'}}); + my @diffs = &Apache::loncommon::compare_arrays(\@new,\@old); + if (@diffs) { + $changes{$provider} = 1; + } + } + } elsif (@{$confhash{$provider}{'crsconf'}}) { + $changes{$provider} = 1; + } + } + } + if ($showroles) { + $confhash{$provider}{'roles'} = {}; + foreach my $role (@courseroles) { + my $poss = $env{'form.proctoring_roles_'.$role.'_'.$provider}; + if (grep(/^\Q$poss\E$/,@ltiroles)) { + $confhash{$provider}{'roles'}{$role} = $poss; + } + } + unless ($changes{$provider}) { + if (ref($current{'roles'}) eq 'HASH') { + foreach my $role (keys(%{$current{'roles'}})) { + unless ($current{'roles'}{$role} eq $confhash{$provider}{'roles'}{$role}) { + $changes{$provider} = 1; + last + } + } + unless ($changes{$provider}) { + foreach my $role (keys(%{$confhash{$provider}{'roles'}})) { + unless ($confhash{$provider}{'roles'}{$role} eq $current{'roles'}{$role}) { + $changes{$provider} = 1; + last; + } + } + } + } elsif (keys(%{$confhash{$provider}{'roles'}})) { + $changes{$provider} = 1; + } + } + } + if (ref($current{'custom'}) eq 'HASH') { + my @customdels = &Apache::loncommon::get_env_multiple('form.proctoring_customdel_'.$provider); + foreach my $key (keys(%{$current{'custom'}})) { + if (grep(/^\Q$key\E$/,@customdels)) { + $changes{$provider} = 1; + } else { + $confhash{$provider}{'custom'}{$key} = $env{'form.proctoring_customval_'.$key.'_'.$provider}; + if ($confhash{$provider}{'custom'}{$key} ne $current{'custom'}{$key}) { + $changes{$provider} = 1; + } + } + } + } + if ($customadds{$provider}) { + my $name = $env{'form.proctoring_custom_name_'.$provider}; + $name =~ s/(`)/'/g; + $name =~ s/^\s+//; + $name =~ s/\s+$//; + my $value = $env{'form.proctoring_custom_value_'.$provider}; + $value =~ s/(`)/'/g; + $value =~ s/^\s+//; + $value =~ s/\s+$//; + if ($name ne '') { + $confhash{$provider}{'custom'}{$name} = $value; + $changes{$provider} = 1; + } + } + } + } + if (@allpos > 0) { + my $idx = 0; + foreach my $provider (@allpos) { + if ($provider ne '') { + $confhash{$provider}{'order'} = $idx; + unless ($changes{$provider}) { + if (ref($domconfig{$action}) eq 'HASH') { + if (ref($domconfig{$action}{$provider}) eq 'HASH') { + if ($domconfig{$action}{$provider}{'order'} ne $idx) { + $changes{$provider} = 1; + } + } + } + } + $idx ++; + } + } + } + my %proc_hash = ( + $action => { %confhash } + ); + my $putresult = &Apache::lonnet::put_dom('configuration',\%proc_hash, + $dom); + if ($putresult eq 'ok') { + my %proc_enchash = ( + $action => { %encconfhash } + ); + &Apache::lonnet::put_dom('encconfig',\%proc_enchash,$dom,undef,1); + if (keys(%changes) > 0) { + my $cachetime = 24*60*60; + my %procall = %confhash; + foreach my $provider (keys(%procall)) { + if (ref($encconfhash{$provider}) eq 'HASH') { + foreach my $key ('key','secret') { + $procall{$provider}{$key} = $encconfhash{$provider}{$key}; + } + } + } + &Apache::lonnet::do_cache_new('proctoring',$dom,\%procall,$cachetime); + if (ref($lastactref) eq 'HASH') { + $lastactref->{'proctoring'} = 1; + } + $resulttext = &mt('Configuration for Provider(s) with changes:').'
      '; + my %bynum; + foreach my $provider (sort(keys(%changes))) { + my $position = $confhash{$provider}{'order'}; + $bynum{$position} = $provider; + } + foreach my $pos (sort { $a <=> $b } keys(%bynum)) { + my $provider = $bynum{$pos}; + my %lt = &proctoring_titles($provider); + my %fieldtitles = &proctoring_fieldtitles($provider); + if (!$confhash{$provider}{'available'}) { + $resulttext .= '
    • '.&mt('Proctoring integration unavailable for: [_1]',''.$providernames{$provider}.'').'
    • '; + } else { + $resulttext .= '
    • '.&mt('Proctoring integration available for: [_1]',''.$providernames{$provider}.''); + if ($confhash{$provider}{'image'}) { + $resulttext .= ' '. + ''.&mt('Proctoring icon').''; + } + $resulttext .= '
        '; + my $position = $pos + 1; + $resulttext .= '
      • '.&mt('Order: [_1]',$position).'
      • '; + foreach my $key ('version','sigmethod','url','lifetime') { + if ($confhash{$provider}{$key} ne '') { + $resulttext .= '
      • '.$lt{$key}.': '.$confhash{$provider}{$key}.'
      • '; + } + } + if ($encconfhash{$provider}{'key'} ne '') { + $resulttext .= '
      • '.$lt{'key'}.': '.$encconfhash{$provider}{'key'}.'
      • '; + } + if ($encconfhash{$provider}{'secret'} ne '') { + $resulttext .= '
      • '.$lt{'secret'}.': '; + my $num = length($encconfhash{$provider}{'secret'}); + $resulttext .= ('*'x$num).'
      • '; + } + my (@fields,$showroles); + if (ref($requserfields{$provider}) eq 'ARRAY') { + push(@fields,@{$requserfields{$provider}}); + } + if (ref($confhash{$provider}{'fields'}) eq 'ARRAY') { + push(@fields,@{$confhash{$provider}{'fields'}}); + } elsif (ref($confhash{$provider}{'fields'}) eq 'HASH') { + push(@fields,(keys(%{$confhash{$provider}{'fields'}}))); + } + if (@fields) { + if (grep(/^roles$/,@fields)) { + $showroles = 1; + } + $resulttext .= '
      • '.$lt{'udsl'}.': "'. + join('", "', map { $lt{$_}; } @fields).'"
      • '; + } + if (ref($requserfields{$provider}) eq 'ARRAY') { + if (grep(/^user$/,@{$requserfields{$provider}})) { + if ($confhash{$provider}{'incdom'}) { + $resulttext .= '
      • '.&mt('[_1] sent as [_2]',$lt{'user'},$lt{'uname:dom'}).'
      • '; + } else { + $resulttext .= '
      • '.&mt('[_1] sent as [_2]',$lt{'user'},$lt{'username'}).'
      • '; + } + } + } + if (ref($confhash{$provider}{'defaults'}) eq 'ARRAY') { + if (@{$confhash{$provider}{'defaults'}} > 0) { + $resulttext .= '
      • '.$lt{'defa'}; + foreach my $field (@{$confhash{$provider}{'defaults'}}) { + $resulttext .= ' "'.$fieldtitles{$field}.'",'; + } + $resulttext =~ s/,$//; + $resulttext .= '
      • '; + } + } elsif (ref($confhash{$provider}{'defaults'}) eq 'HASH') { + if (keys(%{$confhash{$provider}{'defaults'}})) { + $resulttext .= '
      • '.$lt{'defa'}.': 
          '; + foreach my $key (sort(keys(%{$confhash{$provider}{'defaults'}}))) { + if ($confhash{$provider}{'defaults'}{$key} ne '') { + $resulttext .= '
        • '.$fieldtitles{$key}.' = '.$confhash{$provider}{'defaults'}{$key}.'
        • '; + } + } + $resulttext .= '
      • '; + } + } + if (ref($crsconf{$provider}) eq 'ARRAY') { + if (@{$crsconf{$provider}} > 0) { + $resulttext .= '
      • '.&mt('Configurable in course:'); + my $numconfig = 0; + if (ref($confhash{$provider}{'crsconf'}) eq 'ARRAY') { + if (@{$confhash{$provider}{'crsconf'}} > 0) { + foreach my $field (@{$confhash{$provider}{'crsconf'}}) { + $numconfig ++; + if ($provider eq 'examity') { + $resulttext .= ' "'.$lt{'crs'.$field}.'",'; + } else { + $resulttext .= ' "'.$fieldtitles{$field}.'",'; + } + } + $resulttext =~ s/,$//; + } + } + if (!$numconfig) { + $resulttext .= ' '.&mt('None'); + } + $resulttext .= '
      • '; + } + } + if ($showroles) { + if (ref($confhash{$provider}{'roles'}) eq 'HASH') { + my $rolemaps; + foreach my $role (@courseroles) { + if ($confhash{$provider}{'roles'}{$role}) { + $rolemaps .= (' 'x2).&Apache::lonnet::plaintext($role,'Course').'='. + $confhash{$provider}{'roles'}{$role}.','; + } + } + if ($rolemaps) { + $rolemaps =~ s/,$//; + $resulttext .= '
      • '.&mt('Role mapping:').$rolemaps.'
      • '; + } + } + } + if (ref($confhash{$provider}{'custom'}) eq 'HASH') { + my $customlist; + if (keys(%{$confhash{$provider}{'custom'}})) { + foreach my $key (sort(keys(%{$confhash{$provider}{'custom'}}))) { + $customlist .= $key.'='.$confhash{$provider}{'custom'}{$key}.', '; + } + $customlist =~ s/,$//; + } + if ($customlist) { + $resulttext .= '
      • '.&mt('Custom items').': '.$customlist.'
      • '; + } + } + $resulttext .= '
    • '; + } + } + $resulttext .= '
    '; + } else { + $resulttext = &mt('No changes made.'); + } + } else { + $errors .= '
  • '.&mt('Failed to save changes').'
  • '; + } + if ($errors) { + $resulttext .= &mt('The following errors occurred: ').'
      '. + $errors.'
    '; + } + return $resulttext; +} + +sub process_proctoring_image { + my ($r,$dom,$confname,$caller,$provider,$configuserok,$switchserver,$author_ok) = @_; + my $filename = $env{'form.'.$caller.'.filename'}; + my ($error,$url); + my ($width,$height) = (21,21); + if ($configuserok eq 'ok') { + if ($switchserver) { + $error = &mt('Upload of Remote Proctoring Provider icon is not permitted to this server: [_1]', + $switchserver); + } elsif ($author_ok eq 'ok') { + my ($result,$imageurl,$madethumb) = + &publishlogo($r,'upload',$caller,$dom,$confname, + "proctoring/$provider/icon",$width,$height); + if ($result eq 'ok') { + if ($madethumb) { + my ($path,$imagefile) = ($imageurl =~ m{^(.+)/([^/]+)$}); + my $imagethumb = "$path/tn-".$imagefile; + $url = $imagethumb; + } else { + $url = $imageurl; + } + } else { + $error = &mt("Upload of [_1] failed because an error occurred publishing the file in RES space. Error was: [_2].",$filename,$result); + } + } else { + $error = &mt("Upload of [_1] failed because an author role could not be assigned to a Domain Configuration user ([_2]) in domain: [_3]. Error was: [_4].",$filename,$confname,$dom,$author_ok); + } + } else { + $error = &mt("Upload of [_1] failed because a Domain Configuration user ([_2]) could not be created in domain: [_3]. Error was: [_4].",$filename,$confname,$dom,$configuserok); + } + return ($url,$error); +} + sub modify_lti { my ($r,$dom,$action,$lastactref,%domconfig) = @_; my %domdefaults = &Apache::lonnet::get_domain_defaults($dom,1); @@ -11353,19 +15442,23 @@ sub modify_lti { my @courseroles = ('cc','in','ta','ep','st'); my @ltiroles = qw(Learner Instructor ContentDeveloper TeachingAssistant Mentor Member Manager Administrator); my @lticourseroles = qw(Instructor TeachingAssistant Mentor Learner); - my @coursetypes = ('official','unofficial','community','textbook','placement'); + my @coursetypes = ('official','unofficial','community','textbook','placement','lti'); my %coursetypetitles = &Apache::lonlocal::texthash ( official => 'Official', unofficial => 'Unofficial', community => 'Community', textbook => 'Textbook', placement => 'Placement Test', + lti => 'LTI Provider', ); + my %fieldtitles = &Apache::loncommon::personal_data_fieldtitles(); my %lt = <i_names(); map { $posslti{$_} = 1; } @ltiroles; map { $posslticrs{$_} = 1; } @lticourseroles; map { $posscrstype{$_} = 1; } @coursetypes; - + + my %menutitles = <imenu_titles(); + my (@items,%deletions,%itemids); if ($env{'form.lti_add'}) { my $consumer = $env{'form.lti_consumer_add'}; @@ -11386,15 +15479,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; } } } @@ -11402,12 +15495,12 @@ 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; } - foreach my $item ('consumer','key','secret','lifetime') { + foreach my $item ('consumer','key','secret','lifetime','requser','crsinc') { my $formitem = 'form.lti_'.$item.'_'.$idx; $env{$formitem} =~ s/(`)/'/g; if ($item eq 'lifetime') { @@ -11429,128 +15522,225 @@ sub modify_lti { if ($env{'form.lti_version_'.$idx} eq 'LTI-1p0') { $confhash{$itemid}{'version'} = $env{'form.lti_version_'.$idx}; } - if ($env{'form.lti_mapuser_'.$idx} eq 'sourcedid') { - $confhash{$itemid}{'mapuser'} = 'lis_person_sourcedid'; - } elsif ($env{'form.lti_mapuser_'.$idx} eq 'email') { - $confhash{$itemid}{'mapuser'} = 'lis_person_contact_email_primary'; - } elsif ($env{'form.lti_mapuser_'.$idx} eq 'other') { - my $mapuser = $env{'form.lti_customuser_'.$idx}; - $mapuser =~ s/(`)/'/g; - $mapuser =~ s/^\s+|\s+$//g; - $confhash{$itemid}{'mapuser'} = $mapuser; - } - foreach my $ltirole (@lticourseroles) { - my $possrole = $env{'form.lti_maprole_'.$ltirole.'_'.$idx}; - if (grep(/^\Q$possrole\E$/,@courseroles)) { - $confhash{$itemid}{'maproles'}{$ltirole} = $possrole; - } - } - my @possmakeuser = &Apache::loncommon::get_env_multiple('form.lti_makeuser_'.$idx); - my @makeuser; - foreach my $ltirole (sort(@possmakeuser)) { - if ($posslti{$ltirole}) { - push(@makeuser,$ltirole); - } - } - $confhash{$itemid}{'makeuser'} = \@makeuser; - if (($env{'form.lti_mapcrs_'.$idx} eq 'course_offering_sourcedid') || - ($env{'form.lti_mapcrs_'.$idx} eq 'context_id')) { - $confhash{$itemid}{'mapcrs'} = $env{'form.lti_mapcrs_'.$idx}; - } elsif ($env{'form.lti_mapcrs_'.$idx} eq 'other') { - my $mapcrs = $env{'form.lti_mapcrsfield_'.$idx}; - $mapcrs =~ s/(`)/'/g; - $mapcrs =~ s/^\s+|\s+$//g; - $confhash{$itemid}{'mapcrs'} = $mapcrs; - } - my @posstypes = &Apache::loncommon::get_env_multiple('form.lti_mapcrstype_'.$idx); - my @crstypes; - foreach my $type (sort(@posstypes)) { - if ($posscrstype{$type}) { - push(@crstypes,$type); - } - } - $confhash{$itemid}{'mapcrstype'} = \@crstypes; - if ($env{'form.lti_makecrs_'.$idx}) { - $confhash{$itemid}{'makecrs'} = 1; - } - my @possenroll = &Apache::loncommon::get_env_multiple('form.lti_selfenroll_'.$idx); - my @selfenroll; - foreach my $type (sort(@possenroll)) { - if ($posslticrs{$type}) { - push(@selfenroll,$type); + if ($confhash{$itemid}{'requser'}) { + if ($env{'form.lti_mapuser_'.$idx} eq 'sourcedid') { + $confhash{$itemid}{'mapuser'} = 'lis_person_sourcedid'; + } elsif ($env{'form.lti_mapuser_'.$idx} eq 'email') { + $confhash{$itemid}{'mapuser'} = 'lis_person_contact_email_primary'; + } elsif ($env{'form.lti_mapuser_'.$idx} eq 'other') { + my $mapuser = $env{'form.lti_customuser_'.$idx}; + $mapuser =~ s/(`)/'/g; + $mapuser =~ s/^\s+|\s+$//g; + $confhash{$itemid}{'mapuser'} = $mapuser; + } + my @possmakeuser = &Apache::loncommon::get_env_multiple('form.lti_makeuser_'.$idx); + my @makeuser; + foreach my $ltirole (sort(@possmakeuser)) { + if ($posslti{$ltirole}) { + push(@makeuser,$ltirole); + } + } + $confhash{$itemid}{'makeuser'} = \@makeuser; + if (@makeuser) { + my $lcauth = $env{'form.lti_lcauth_'.$idx}; + if ($lcauth =~ /^(internal|krb4|krb5|localauth)$/) { + $confhash{$itemid}{'lcauth'} = $lcauth; + if ($lcauth ne 'internal') { + my $lcauthparm = $env{'form.lti_lcauthparm_'.$idx}; + $lcauthparm =~ s/^(\s+|\s+)$//g; + $lcauthparm =~ s/`//g; + if ($lcauthparm ne '') { + $confhash{$itemid}{'lcauthparm'} = $lcauthparm; + } + } + } else { + $confhash{$itemid}{'lcauth'} = 'lti'; + } } - } - $confhash{$itemid}{'selfenroll'} = \@selfenroll; - if ($env{'form.lti_crssec_'.$idx}) { - if ($env{'form.lti_crssecsrc_'.$idx} eq 'course_section_sourcedid') { - $confhash{$itemid}{'section'} = $env{'form.lti_crssecsrc_'.$idx}; - } elsif ($env{'form.lti_crssecsrc_'.$idx} eq 'other') { - my $section = $env{'form.lti_customsection_'.$idx}; - $section =~ s/(`)/'/g; - $section =~ s/^\s+|\s+$//g; - if ($section ne '') { - $confhash{$itemid}{'section'} = $section; + my @possinstdata = &Apache::loncommon::get_env_multiple('form.lti_instdata_'.$idx); + if (@possinstdata) { + foreach my $field (@possinstdata) { + if (exists($fieldtitles{$field})) { + push(@{$confhash{$itemid}{'instdata'}}); + } } } - } - foreach my $field ('passback','roster') { - if ($env{'form.lti_'.$field.'_'.$idx}) { - $confhash{$itemid}{$field} = 1; + if ($env{'form.lti_callback_'.$idx}) { + if ($env{'form.lti_callbackparam_'.$idx}) { + my $callback = $env{'form.lti_callbackparam_'.$idx}; + $callback =~ s/^\s+|\s+$//g; + $confhash{$itemid}{'callback'} = $callback; + } } - } - unless (($idx eq 'add') || ($changes{$itemid})) { - foreach my $field ('mapuser','mapcrs','section','passback','roster') { - if ($domconfig{$action}{$itemid}{$field} ne $confhash{$itemid}{$field}) { - $changes{$itemid} = 1; + foreach my $field ('topmenu','inlinemenu') { + if ($env{'form.lti_'.$field.'_'.$idx}) { + $confhash{$itemid}{$field} = 1; } } - foreach my $field ('makeuser','mapcrstype','selfenroll') { - unless ($changes{$itemid}) { - if (ref($domconfig{$action}{$itemid}{$field}) eq 'ARRAY') { - if (ref($confhash{$itemid}{$field}) eq 'ARRAY') { - my @diffs = &Apache::loncommon::compare_arrays($domconfig{$action}{$itemid}{$field}, - $confhash{$itemid}{$field}); - if (@diffs) { - $changes{$itemid} = 1; - } - } elsif (@{$domconfig{$action}{$itemid}{$field}} > 0) { - $changes{$itemid} = 1; + if ($env{'form.lti_topmenu_'.$idx} || $env{'form.lti_inlinemenu_'.$idx}) { + $confhash{$itemid}{lcmenu} = []; + my @possmenu = &Apache::loncommon::get_env_multiple('form.lti_menuitem_'.$idx); + foreach my $field (@possmenu) { + if (exists($menutitles{$field})) { + if ($field eq 'grades') { + next unless ($env{'form.lti_inlinemenu_'.$idx}); } - } elsif (ref($confhash{$itemid}{$field}) eq 'ARRAY') { - if (@{$confhash{$itemid}{$field}} > 0) { - $changes{$itemid} = 1; + push(@{$confhash{$itemid}{lcmenu}},$field); + } + } + } + if ($confhash{$itemid}{'crsinc'}) { + if (($env{'form.lti_mapcrs_'.$idx} eq 'course_offering_sourcedid') || + ($env{'form.lti_mapcrs_'.$idx} eq 'context_id')) { + $confhash{$itemid}{'mapcrs'} = $env{'form.lti_mapcrs_'.$idx}; + } elsif ($env{'form.lti_mapcrs_'.$idx} eq 'other') { + my $mapcrs = $env{'form.lti_mapcrsfield_'.$idx}; + $mapcrs =~ s/(`)/'/g; + $mapcrs =~ s/^\s+|\s+$//g; + $confhash{$itemid}{'mapcrs'} = $mapcrs; + } + my @posstypes = &Apache::loncommon::get_env_multiple('form.lti_mapcrstype_'.$idx); + my @crstypes; + foreach my $type (sort(@posstypes)) { + if ($posscrstype{$type}) { + push(@crstypes,$type); + } + } + $confhash{$itemid}{'mapcrstype'} = \@crstypes; + if ($env{'form.lti_storecrs_'.$idx}) { + $confhash{$itemid}{'storecrs'} = 1; + } + if ($env{'form.lti_makecrs_'.$idx}) { + $confhash{$itemid}{'makecrs'} = 1; + } + foreach my $ltirole (@lticourseroles) { + my $possrole = $env{'form.lti_maprole_'.$ltirole.'_'.$idx}; + if (grep(/^\Q$possrole\E$/,@courseroles)) { + $confhash{$itemid}{'maproles'}{$ltirole} = $possrole; + } + } + my @possenroll = &Apache::loncommon::get_env_multiple('form.lti_selfenroll_'.$idx); + my @selfenroll; + foreach my $type (sort(@possenroll)) { + if ($posslticrs{$type}) { + push(@selfenroll,$type); + } + } + $confhash{$itemid}{'selfenroll'} = \@selfenroll; + if ($env{'form.lti_crssec_'.$idx}) { + if ($env{'form.lti_crssecsrc_'.$idx} eq 'course_section_sourcedid') { + $confhash{$itemid}{'section'} = $env{'form.lti_crssecsrc_'.$idx}; + } elsif ($env{'form.lti_crssecsrc_'.$idx} eq 'other') { + my $section = $env{'form.lti_customsection_'.$idx}; + $section =~ s/(`)/'/g; + $section =~ s/^\s+|\s+$//g; + if ($section ne '') { + $confhash{$itemid}{'section'} = $section; } - } + } + } + foreach my $field ('passback','roster') { + if ($env{'form.lti_'.$field.'_'.$idx}) { + $confhash{$itemid}{$field} = 1; + } + } + if ($env{'form.lti_passback_'.$idx}) { + if ($env{'form.lti_passbackformat_'.$idx} eq '1.0') { + $confhash{$itemid}{'passbackformat'} = '1.0'; + } else { + $confhash{$itemid}{'passbackformat'} = '1.1'; + } } } - unless ($changes{$itemid}) { - if (ref($domconfig{$action}{$itemid}{'maproles'}) eq 'HASH') { - if (ref($confhash{$itemid}{'maproles'}) eq 'HASH') { - foreach my $ltirole (keys(%{$domconfig{$action}{$itemid}{'maproles'}})) { - if ($domconfig{$action}{$itemid}{'maproles'}{$ltirole} ne - $confhash{$itemid}{'maproles'}{$ltirole}) { + unless (($idx eq 'add') || ($changes{$itemid})) { + if ($confhash{$itemid}{'crsinc'}) { + foreach my $field ('mapcrs','storecrs','makecrs','section','passback','roster') { + if ($domconfig{$action}{$itemid}{$field} ne $confhash{$itemid}{$field}) { + $changes{$itemid} = 1; + } + } + unless ($changes{$itemid}) { + if ($domconfig{$action}{$itemid}{'passback'} eq $confhash{$itemid}{'passback'}) { + if ($domconfig{$action}{$itemid}{'passbackformat'} ne $confhash{$itemid}{'passbackformat'}) { $changes{$itemid} = 1; - last; } } + } + foreach my $field ('mapcrstype','selfenroll') { unless ($changes{$itemid}) { - foreach my $ltirole (keys(%{$confhash{$itemid}{'maproles'}})) { - if ($confhash{$itemid}{'maproles'}{$ltirole} ne - $domconfig{$action}{$itemid}{'maproles'}{$ltirole}) { + if (ref($domconfig{$action}{$itemid}{$field}) eq 'ARRAY') { + if (ref($confhash{$itemid}{$field}) eq 'ARRAY') { + my @diffs = &Apache::loncommon::compare_arrays($domconfig{$action}{$itemid}{$field}, + $confhash{$itemid}{$field}); + if (@diffs) { + $changes{$itemid} = 1; + } + } elsif (@{$domconfig{$action}{$itemid}{$field}} > 0) { + $changes{$itemid} = 1; + } + } elsif (ref($confhash{$itemid}{$field}) eq 'ARRAY') { + if (@{$confhash{$itemid}{$field}} > 0) { $changes{$itemid} = 1; - last; } } } - } elsif (keys(%{$domconfig{$action}{$itemid}{'maproles'}}) > 0) { - $changes{$itemid} = 1; } - } elsif (ref($confhash{$itemid}{'maproles'}) eq 'HASH') { unless ($changes{$itemid}) { - if (keys(%{$confhash{$itemid}{'maproles'}}) > 0) { + if (ref($domconfig{$action}{$itemid}{'maproles'}) eq 'HASH') { + if (ref($confhash{$itemid}{'maproles'}) eq 'HASH') { + foreach my $ltirole (keys(%{$domconfig{$action}{$itemid}{'maproles'}})) { + if ($domconfig{$action}{$itemid}{'maproles'}{$ltirole} ne + $confhash{$itemid}{'maproles'}{$ltirole}) { + $changes{$itemid} = 1; + last; + } + } + unless ($changes{$itemid}) { + foreach my $ltirole (keys(%{$confhash{$itemid}{'maproles'}})) { + if ($confhash{$itemid}{'maproles'}{$ltirole} ne + $domconfig{$action}{$itemid}{'maproles'}{$ltirole}) { + $changes{$itemid} = 1; + last; + } + } + } + } elsif (keys(%{$domconfig{$action}{$itemid}{'maproles'}}) > 0) { + $changes{$itemid} = 1; + } + } elsif (ref($confhash{$itemid}{'maproles'}) eq 'HASH') { + unless ($changes{$itemid}) { + if (keys(%{$confhash{$itemid}{'maproles'}}) > 0) { + $changes{$itemid} = 1; + } + } + } + } + } + unless ($changes{$itemid}) { + foreach my $field ('mapuser','lcauth','lcauthparm','topmenu','inlinemenu','callback') { + if ($domconfig{$action}{$itemid}{$field} ne $confhash{$itemid}{$field}) { $changes{$itemid} = 1; } } + unless ($changes{$itemid}) { + foreach my $field ('makeuser','lcmenu') { + if (ref($domconfig{$action}{$itemid}{$field}) eq 'ARRAY') { + if (ref($confhash{$itemid}{$field}) eq 'ARRAY') { + my @diffs = &Apache::loncommon::compare_arrays($domconfig{$action}{$itemid}{$field}, + $confhash{$itemid}{$field}); + if (@diffs) { + $changes{$itemid} = 1; + } + } elsif (@{$domconfig{$action}{$itemid}{$field}} > 0) { + $changes{$itemid} = 1; + } + } elsif (ref($confhash{$itemid}{$field}) eq 'ARRAY') { + if (@{$confhash{$itemid}{$field}} > 0) { + $changes{$itemid} = 1; + } + } + } + } } } } @@ -11580,7 +15770,7 @@ sub modify_lti { my %ltienchash = ( $action => { %encconfig } ); - &Apache::lonnet::put_dom('encconfig',\%ltienchash,$dom); + &Apache::lonnet::put_dom('encconfig',\%ltienchash,$dom,undef,1); if (keys(%changes) > 0) { my $cachetime = 24*60*60; my %ltiall = %confhash; @@ -11606,7 +15796,7 @@ sub modify_lti { if (ref($confhash{$itemid}) ne 'HASH') { $resulttext .= '
  • '.&mt('Deleted: [_1]',$changes{$itemid}).'
  • '; } else { - $resulttext .= '
  • '.$confhash{$itemid}{'consumer'}.'
    • '; + $resulttext .= '
    • '.$confhash{$itemid}{'consumer'}.'
        '; my $position = $pos + 1; $resulttext .= '
      • '.&mt('Order: [_1]',$position).'
      • '; foreach my $item ('version','lifetime') { @@ -11622,84 +15812,152 @@ sub modify_lti { my $num = length($encconfig{$itemid}{'secret'}); $resulttext .= ('*'x$num).''; } - if ($confhash{$itemid}{'mapuser'}) { - my $shownmapuser; - if ($confhash{$itemid}{'mapuser'} eq 'lis_person_sourcedid') { - $shownmapuser = $lt{'sourcedid'}.' (lis_person_sourcedid)'; - } elsif ($confhash{$itemid}{'mapuser'} eq 'lis_person_contact_email_primary') { - $shownmapuser = $lt{'email'}.' (lis_person_contact_email_primary)'; + if ($confhash{$itemid}{'requser'}) { + if ($confhash{$itemid}{'callback'}) { + $resulttext .= '
      • '.&mt('Callback setting').': '.$confhash{$itemid}{'callback'}.'
      • '; } else { - $shownmapuser = &mt('Other').' ('.$confhash{$itemid}{'mapuser'}.')'; - } - $resulttext .= '
      • '.&mt('LON-CAPA username').': '.$shownmapuser.'
      • '; - } - if (ref($confhash{$itemid}{'maproles'}) eq 'HASH') { - my $rolemaps; - foreach my $role (@ltiroles) { - if ($confhash{$itemid}{'maproles'}{$role}) { - $rolemaps .= (' 'x2).$role.'='. - &Apache::lonnet::plaintext($confhash{$itemid}{'maproles'}{$role}, - 'Course').','; - } + $resulttext .= '
      • '.&mt('Callback to logout LON-CAPA on log out from Consumer').'
      • '; } - if ($rolemaps) { - $rolemaps =~ s/,$//; - $resulttext .= '
      • '.&mt('Role mapping:').$rolemaps.'
      • '; + if ($confhash{$itemid}{'mapuser'}) { + my $shownmapuser; + if ($confhash{$itemid}{'mapuser'} eq 'lis_person_sourcedid') { + $shownmapuser = $lt{'sourcedid'}.' (lis_person_sourcedid)'; + } elsif ($confhash{$itemid}{'mapuser'} eq 'lis_person_contact_email_primary') { + $shownmapuser = $lt{'email'}.' (lis_person_contact_email_primary)'; + } else { + $shownmapuser = &mt('Other').' ('.$confhash{$itemid}{'mapuser'}.')'; + } + $resulttext .= '
      • '.&mt('LON-CAPA username').': '.$shownmapuser.'
      • '; } - } - if (ref($confhash{$itemid}{'makeuser'}) eq 'ARRAY') { - if (@{$confhash{$itemid}{'makeuser'}} > 0) { - $resulttext .= '
      • '.&mt('Following roles may create user accounts: [_1]', - join(', ',@{$confhash{$itemid}{'makeuser'}})).'
      • '; - } else { - $resulttext .= '
      • '.&mt('User account creation not permitted.').'
      • '; + if (ref($confhash{$itemid}{'makeuser'}) eq 'ARRAY') { + if (@{$confhash{$itemid}{'makeuser'}} > 0) { + $resulttext .= '
      • '.&mt('Following roles may create user accounts: [_1]', + join(', ',@{$confhash{$itemid}{'makeuser'}})).'
        '; + if ($confhash{$itemid}{'lcauth'} eq 'lti') { + $resulttext .= &mt('New users will only be able to authenticate via LTI').'
      • '; + } else { + $resulttext .= &mt('New users will be assigned LON-CAPA authentication: [_1]', + $confhash{$itemid}{'lcauth'}); + if ($confhash{$itemid}{'lcauth'} eq 'internal') { + $resulttext .= '; '.&mt('a randomly generated password will be created'); + } elsif ($confhash{$itemid}{'lcauth'} eq 'localauth') { + if ($confhash{$itemid}{'lcauthparm'} ne '') { + $resulttext .= ' '.&mt('with argument: [_1]',$confhash{$itemid}{'lcauthparm'}); + } + } else { + $resulttext .= '; '.&mt('Kerberos domain: [_1]',$confhash{$itemid}{'lcauthparm'}); + } + } + $resulttext .= ''; + } else { + $resulttext .= '
      • '.&mt('User account creation not permitted.').'
      • '; + } } - } - if ($confhash{$itemid}{'mapcrs'}) { - $resulttext .= '
      • '.&mt('Unique course identifier').': '.$confhash{$itemid}{'mapcrs'}.'
      • '; - } - if (ref($confhash{$itemid}{'mapcrstype'}) eq 'ARRAY') { - if (@{$confhash{$itemid}{'mapcrstype'}} > 0) { - $resulttext .= '
      • '.&mt('Mapping for the following LON-CAPA course types: [_1]', - join(', ',map { $coursetypetitles{$_}; } @coursetypes)). - '
      • '; - } else { - $resulttext .= '
      • '.&mt('No mapping to LON-CAPA courses').'
      • '; + if (ref($confhash{$itemid}{'instdata'}) eq 'ARRAY') { + if (@{$confhash{$itemid}{'instdata'}} > 0) { + $resulttext .= '
      • '.&mt('Institutional data will be used when creating a new user for: [_1]', + join(', ',map { $fieldtitles{$_}; } @{$confhash{$itemid}{'instdata'}})).'
      • '; + } else { + $resulttext .= '
      • '.&mt('No institutional data used when creating a new user.').'
      • '; + } } - } - if ($confhash{$itemid}{'makecrs'}) { - $resulttext .= '
      • '.&mt('Instructor may create course (if absent).').'
      • '; - } else { - $resulttext .= '
      • '.&mt('Instructor may not create course (if absent).').'
      • '; - } - if (ref($confhash{$itemid}{'selfenroll'}) eq 'ARRAY') { - if (@{$confhash{$itemid}{'selfenroll'}} > 0) { - $resulttext .= '
      • '.&mt('Self-enrollment for following roles: [_1]', - join(', ',@{$confhash{$itemid}{'selfenroll'}})). - '
      • '; - } else { - $resulttext .= '
      • '.&mt('Self-enrollment not permitted').'
      • '; + foreach my $item ('topmenu','inlinemenu') { + $resulttext .= '
      • '.$lt{$item}.': '; + if ($confhash{$itemid}{$item}) { + $resulttext .= &mt('Yes'); + } else { + $resulttext .= &mt('No'); + } + $resulttext .= '
      • '; } - } - if ($confhash{$itemid}{'section'}) { - if ($confhash{$itemid}{'section'} eq 'course_section_sourcedid') { - $resulttext .= '
      • '.&mt('User section from standard field:'). - ' (course_section_sourcedid)'.'
      • '; - } else { - $resulttext .= '
      • '.&mt('User section from:').' '. - $confhash{$itemid}{'section'}.'
      • '; + if (ref($confhash{$itemid}{'lcmenu'}) eq 'ARRAY') { + if (@{$confhash{$itemid}{'lcmenu'}} > 0) { + $resulttext .= '
      • '.&mt('Menu items:').' '. + join(', ', map { $menutitles{$_}; } (@{$confhash{$itemid}{'lcmenu'}})).'
      • '; + } else { + $resulttext .= '
      • '.&mt('No menu items displayed in header or online menu').'
      • '; + } } - } else { - $resulttext .= '
      • '.&mt('No section assignment').'
      • '; - } - foreach my $item ('passback','roster') { - $resulttext .= '
      • '.$lt{$item}.' '; - if ($confhash{$itemid}{$item}) { - $resulttext .= &mt('Yes'); - } else { - $resulttext .= &mt('No'); + if ($confhash{$itemid}{'crsinc'}) { + if (ref($confhash{$itemid}{'maproles'}) eq 'HASH') { + my $rolemaps; + foreach my $role (@ltiroles) { + if ($confhash{$itemid}{'maproles'}{$role}) { + $rolemaps .= (' 'x2).$role.'='. + &Apache::lonnet::plaintext($confhash{$itemid}{'maproles'}{$role}, + 'Course').','; + } + } + if ($rolemaps) { + $rolemaps =~ s/,$//; + $resulttext .= '
      • '.&mt('Role mapping:').$rolemaps.'
      • '; + } + } + if ($confhash{$itemid}{'mapcrs'}) { + $resulttext .= '
      • '.&mt('Unique course identifier').': '.$confhash{$itemid}{'mapcrs'}.'
      • '; + } + if (ref($confhash{$itemid}{'mapcrstype'}) eq 'ARRAY') { + if (@{$confhash{$itemid}{'mapcrstype'}} > 0) { + $resulttext .= '
      • '.&mt('Mapping for the following LON-CAPA course types: [_1]', + join(', ',map { $coursetypetitles{$_}; } @coursetypes)). + '
      • '; + } else { + $resulttext .= '
      • '.&mt('No mapping to LON-CAPA courses').'
      • '; + } + } + if ($confhash{$itemid}{'storecrs'}) { + $resulttext .= '
      • '.&mt('Store mapping of course identifier to LON-CAPA CourseID').': '.$confhash{$itemid}{'storecrs'}.'
      • '; + } + if ($confhash{$itemid}{'makecrs'}) { + $resulttext .= '
      • '.&mt('Instructor may create course (if absent).').'
      • '; + } else { + $resulttext .= '
      • '.&mt('Instructor may not create course (if absent).').'
      • '; + } + if (ref($confhash{$itemid}{'selfenroll'}) eq 'ARRAY') { + if (@{$confhash{$itemid}{'selfenroll'}} > 0) { + $resulttext .= '
      • '.&mt('Self-enrollment for following roles: [_1]', + join(', ',@{$confhash{$itemid}{'selfenroll'}})). + '
      • '; + } else { + $resulttext .= '
      • '.&mt('Self-enrollment not permitted').'
      • '; + } + } + if ($confhash{$itemid}{'section'}) { + if ($confhash{$itemid}{'section'} eq 'course_section_sourcedid') { + $resulttext .= '
      • '.&mt('User section from standard field:'). + ' (course_section_sourcedid)'.'
      • '; + } else { + $resulttext .= '
      • '.&mt('User section from:').' '. + $confhash{$itemid}{'section'}.'
      • '; + } + } else { + $resulttext .= '
      • '.&mt('No section assignment').'
      • '; + } + foreach my $item ('passback','roster','topmenu','inlinemenu') { + $resulttext .= '
      • '.$lt{$item}.': '; + if ($confhash{$itemid}{$item}) { + $resulttext .= &mt('Yes'); + if ($item eq 'passback') { + if ($confhash{$itemid}{'passbackformat'} eq '1.0') { + $resulttext .= ' ('.&mt('Outcomes Extension (1.0)').')'; + } elsif ($confhash{$itemid}{'passbackformat'} eq '1.1') { + $resulttext .= ' ('.&mt('Outcomes Service (1.1)').')'; + } + } + } else { + $resulttext .= &mt('No'); + } + $resulttext .= '
      • '; + } + if (ref($confhash{$itemid}{'lcmenu'}) eq 'ARRAY') { + if (@{$confhash{$itemid}{'lcmenu'}} > 0) { + $resulttext .= '
      • '.&mt('Menu items:').' '. + join(', ', map { $menutitles{$_}; } (@{$confhash{$itemid}{'lcmenu'}})).'
      • '; + } else { + $resulttext .= '
      • '.&mt('No menu items displayed in header or online menu').'
      • '; + } + } } - $resulttext .= ''; } $resulttext .= '
    • '; } @@ -11774,7 +16032,7 @@ sub modify_autoenroll { my %title = ( run => 'Auto-enrollment active', sender => 'Sender for notification messages', coowners => 'Automatic assignment of co-ownership to instructors of record (institutional data)', - failsafe => 'Failsafe for no drops if institutional data missing for a section'); + autofailsafe => 'Failsafe for no drops if institutional data missing for a section'); my @offon = ('off','on'); my $sender_uname = $env{'form.sender_uname'}; my $sender_domain = $env{'form.sender_domain'}; @@ -11784,17 +16042,23 @@ sub modify_autoenroll { $sender_domain = ''; } my $coowners = $env{'form.autoassign_coowners'}; + my $autofailsafe = $env{'form.autoenroll_autofailsafe'}; + $autofailsafe =~ s{^\s+|\s+$}{}g; + if ($autofailsafe =~ /\D/) { + undef($autofailsafe); + } my $failsafe = $env{'form.autoenroll_failsafe'}; - $failsafe =~ s{^\s+|\s+$}{}g; - if ($failsafe =~ /\D/) { - undef($failsafe); + unless (($failsafe eq 'zero') || ($failsafe eq 'any')) { + $failsafe = 'off'; + undef($autofailsafe); } my %autoenrollhash = ( autoenroll => { 'run' => $env{'form.autoenroll_run'}, 'sender_uname' => $sender_uname, 'sender_domain' => $sender_domain, 'co-owners' => $coowners, - 'autofailsafe' => $failsafe, + 'autofailsafe' => $autofailsafe, + 'failsafe' => $failsafe, } ); my $putresult = &Apache::lonnet::put_dom('configuration',\%autoenrollhash, @@ -11822,9 +16086,12 @@ sub modify_autoenroll { } elsif ($coowners) { $changes{'coowners'} = 1; } - if ($currautoenroll{'autofailsafe'} ne $failsafe) { + if ($currautoenroll{'autofailsafe'} ne $autofailsafe) { $changes{'autofailsafe'} = 1; } + if ($currautoenroll{'failsafe'} ne $failsafe) { + $changes{'failsafe'} = 1; + } if (keys(%changes) > 0) { $resulttext = &mt('Changes made:').'
        '; if ($changes{'run'}) { @@ -11845,11 +16112,24 @@ sub modify_autoenroll { } } if ($changes{'autofailsafe'}) { - if ($failsafe ne '') { - $resulttext .= '
      • '.&mt('Failsafe for no drops if institutional data missing for a section set to: [_1]',$failsafe).'
      • '; + if ($autofailsafe ne '') { + $resulttext .= '
      • '.&mt('Failsafe for no drops if institutional data missing for a section set to: [_1]',$autofailsafe).'
      • '; + } else { + $resulttext .= '
      • '.&mt('Failsafe for no drops if institutional data missing for a section not in use').'
      • '; + } + } + if ($changes{'failsafe'}) { + if ($failsafe eq 'off') { + unless ($changes{'autofailsafe'}) { + $resulttext .= '
      • '.&mt('Failsafe for no drops if institutional data missing for a section not in use').'
      • '; + } + } elsif ($failsafe eq 'zero') { + $resulttext .= '
      • '.&mt('Failsafe applies if retrieved section enrollment is zero').'
      • '; } else { - $resulttext .= '
      • '.&mt('Failsafe for no drops if institutional data missing for a section: deleted'); + $resulttext .= '
      • '.&mt('Failsafe applies if retrieved section enrollment is zero or greater').'
      • '; } + } + if (($changes{'autofailsafe'}) || ($changes{'failsafe'})) { &Apache::lonnet::get_domain_defaults($dom,1); if (ref($lastactref) eq 'HASH') { $lastactref->{'domdefaults'} = 1; @@ -11876,8 +16156,10 @@ sub modify_autoupdate { } my @offon = ('off','on'); my %title = &Apache::lonlocal::texthash ( - run => 'Auto-update:', - classlists => 'Updates to user information in classlists?' + run => 'Auto-update:', + classlists => 'Updates to user information in classlists?', + unexpired => 'Skip updates for users without active or future roles?', + lastactive => 'Skip updates for inactive users?', ); my ($othertitle,$usertypes,$types) = &Apache::loncommon::sorted_inst_types($dom); my %fieldtitles = &Apache::lonlocal::texthash ( @@ -11921,12 +16203,23 @@ sub modify_autoupdate { my %updatehash = ( autoupdate => { run => $env{'form.autoupdate_run'}, classlists => $env{'form.classlists'}, + unexpired => $env{'form.unexpired'}, fields => {%fields}, lockablenames => \@lockablenames, } ); + my $lastactivedays; + if ($env{'form.lastactive'}) { + $lastactivedays = $env{'form.lastactivedays'}; + $lastactivedays =~ s/^\s+|\s+$//g; + unless ($lastactivedays =~ /^\d+$/) { + undef($lastactivedays); + $env{'form.lastactive'} = 0; + } + } + $updatehash{'autoupdate'}{'lastactive'} = $lastactivedays; foreach my $key (keys(%currautoupdate)) { - if (($key eq 'run') || ($key eq 'classlists')) { + if (($key eq 'run') || ($key eq 'classlists') || ($key eq 'unexpired') || ($key eq 'lastactive')) { if (exists($updatehash{autoupdate}{$key})) { if ($currautoupdate{$key} ne $updatehash{autoupdate}{$key}) { $changes{$key} = 1; @@ -11972,6 +16265,16 @@ sub modify_autoupdate { $changes{'lockablenames'} = 1; } } + unless (grep(/^unexpired$/,keys(%currautoupdate))) { + if ($updatehash{'autoupdate'}{'unexpired'}) { + $changes{'unexpired'} = 1; + } + } + unless (grep(/^lastactive$/,keys(%currautoupdate))) { + if ($updatehash{'autoupdate'}{'lastactive'} ne '') { + $changes{'lastactive'} = 1; + } + } foreach my $item (@{$types},'default') { if (defined($fields{$item})) { if (ref($currautoupdate{'fields'}) eq 'HASH') { @@ -12034,6 +16337,11 @@ sub modify_autoupdate { my $newvalue; if ($key eq 'run') { $newvalue = $offon[$env{'form.autoupdate_run'}]; + } elsif ($key eq 'lastactive') { + $newvalue = $offon[$env{'form.lastactive'}]; + unless ($lastactivedays eq '') { + $newvalue .= '; '.&mt('inactive = no activity in last [quant,_1,day]',$lastactivedays); + } } else { $newvalue = $offon[$env{'form.'.$key}]; } @@ -12346,8 +16654,9 @@ sub modify_contacts { my (%others,%to,%bcc,%includestr,%includeloc); my @contacts = ('supportemail','adminemail'); my @mailings = ('errormail','packagesmail','helpdeskmail','otherdomsmail', - 'lonstatusmail','requestsmail','updatesmail','idconflictsmail'); - my @toggles = ('reporterrors','reportupdates'); + 'lonstatusmail','requestsmail','updatesmail','idconflictsmail','hostipmail'); + my @toggles = ('reporterrors','reportupdates','reportstatus'); + my @lonstatus = ('threshold','sysmail','weights','excluded'); my ($fields,$fieldtitles,$fieldoptions,$possoptions) = &helpform_fields(); foreach my $type (@mailings) { @{$newsetting{$type}} = @@ -12380,6 +16689,41 @@ sub modify_contacts { $contacts_hash{'contacts'}{$item} = $env{'form.'.$item}; } } + my ($lonstatus_defs,$lonstatus_names) = &Apache::loncommon::lon_status_items(); + foreach my $item (@lonstatus) { + if ($item eq 'excluded') { + my (%serverhomes,@excluded); + map { $serverhomes{$_} = 1; } values(%Apache::lonnet::serverhomeIDs); + my @possexcluded = &Apache::loncommon::get_env_multiple('form.errorexcluded'); + if (@possexcluded) { + foreach my $id (sort(@possexcluded)) { + if ($serverhomes{$id}) { + push(@excluded,$id); + } + } + } + if (@excluded) { + $contacts_hash{'contacts'}{'lonstatus'}{$item} = \@excluded; + } + } elsif ($item eq 'weights') { + foreach my $type ('E','W','N','U') { + $env{'form.error'.$item.'_'.$type} =~ s/^\s+|\s+$//g; + if ($env{'form.error'.$item.'_'.$type} =~ /^\d+$/) { + unless ($env{'form.error'.$item.'_'.$type} == $lonstatus_defs->{$type}) { + $contacts_hash{'contacts'}{'lonstatus'}{$item}{$type} = + $env{'form.error'.$item.'_'.$type}; + } + } + } + } elsif (($item eq 'threshold') || ($item eq 'sysmail')) { + $env{'form.error'.$item} =~ s/^\s+|\s+$//g; + if ($env{'form.error'.$item} =~ /^\d+$/) { + unless ($env{'form.error'.$item} == $lonstatus_defs->{$item}) { + $contacts_hash{'contacts'}{'lonstatus'}{$item} = $env{'form.error'.$item}; + } + } + } + } if ((ref($fields) eq 'ARRAY') && (ref($possoptions) eq 'HASH')) { foreach my $field (@{$fields}) { if (ref($possoptions->{$field}) eq 'ARRAY') { @@ -12518,6 +16862,76 @@ sub modify_contacts { } } } + if (ref($currsetting{'lonstatus'}) eq 'HASH') { + foreach my $key ('excluded','weights','threshold','sysmail') { + if ($key eq 'excluded') { + if ((ref($contacts_hash{contacts}{lonstatus}) eq 'HASH') && + (ref($contacts_hash{contacts}{lonstatus}{excluded}) eq 'ARRAY')) { + if ((ref($currsetting{'lonstatus'}{$key}) eq 'ARRAY') && + (@{$currsetting{'lonstatus'}{$key}})) { + my @diffs = + &Apache::loncommon::compare_arrays($contacts_hash{contacts}{lonstatus}{excluded}, + $currsetting{'lonstatus'}{$key}); + if (@diffs) { + push(@{$changes{'lonstatus'}},$key); + } + } elsif (@{$contacts_hash{contacts}{lonstatus}{excluded}}) { + push(@{$changes{'lonstatus'}},$key); + } + } elsif ((ref($currsetting{'lonstatus'}{$key}) eq 'ARRAY') && + (@{$currsetting{'lonstatus'}{$key}})) { + push(@{$changes{'lonstatus'}},$key); + } + } elsif ($key eq 'weights') { + if ((ref($contacts_hash{contacts}{lonstatus}) eq 'HASH') && + (ref($contacts_hash{contacts}{lonstatus}{$key}) eq 'HASH')) { + if (ref($currsetting{'lonstatus'}{$key}) eq 'HASH') { + foreach my $type ('E','W','N','U') { + unless ($contacts_hash{contacts}{lonstatus}{$key}{$type} eq + $currsetting{'lonstatus'}{$key}{$type}) { + push(@{$changes{'lonstatus'}},$key); + last; + } + } + } else { + foreach my $type ('E','W','N','U') { + if ($contacts_hash{contacts}{lonstatus}{$key}{$type} ne '') { + push(@{$changes{'lonstatus'}},$key); + last; + } + } + } + } elsif (ref($currsetting{'lonstatus'}{$key}) eq 'HASH') { + foreach my $type ('E','W','N','U') { + if ($currsetting{'lonstatus'}{$key}{$type} ne '') { + push(@{$changes{'lonstatus'}},$key); + last; + } + } + } + } elsif (($key eq 'threshold') || ($key eq 'sysmail')) { + if (ref($contacts_hash{contacts}{lonstatus}) eq 'HASH') { + if ($currsetting{'lonstatus'}{$key} =~ /^\d+$/) { + if ($currsetting{'lonstatus'}{$key} != $contacts_hash{contacts}{lonstatus}{$key}) { + push(@{$changes{'lonstatus'}},$key); + } + } elsif ($contacts_hash{contacts}{lonstatus}{$key} =~ /^\d+$/) { + push(@{$changes{'lonstatus'}},$key); + } + } elsif ($currsetting{'lonstatus'}{$key} =~ /^\d+$/) { + push(@{$changes{'lonstatus'}},$key); + } + } + } + } else { + if (ref($contacts_hash{contacts}{lonstatus}) eq 'HASH') { + foreach my $key ('excluded','weights','threshold','sysmail') { + if (exists($contacts_hash{contacts}{lonstatus}{$key})) { + push(@{$changes{'lonstatus'}},$key); + } + } + } + } } else { my %default; $default{'supportemail'} = $Apache::lonnet::perlvar{'lonSupportEMail'}; @@ -12529,6 +16943,7 @@ sub modify_contacts { $default{'lonstatusmail'} = 'adminemail'; $default{'requestsmail'} = 'adminemail'; $default{'updatesmail'} = 'adminemail'; + $default{'hostipmail'} = 'adminemail'; foreach my $item (@contacts) { if ($to{$item} ne $default{$item}) { $changes{$item} = 1; @@ -12562,6 +16977,13 @@ sub modify_contacts { } } } + if (ref($contacts_hash{contacts}{lonstatus}) eq 'HASH') { + foreach my $key ('excluded','weights','threshold','sysmail') { + if (exists($contacts_hash{contacts}{lonstatus}{$key})) { + push(@{$changes{'lonstatus'}},$key); + } + } + } } foreach my $item (@toggles) { if (($env{'form.'.$item} == 1) && ($currsetting{$item} == 0)) { @@ -12687,22 +17109,79 @@ sub modify_contacts { } } my @offon = ('off','on'); + my $corelink = &core_link_msu(); if ($changes{'reporterrors'}) { $resulttext .= '
      • '. &mt('E-mail error reports to [_1] set to "'. $offon[$env{'form.reporterrors'}].'".', - &Apache::loncommon::modal_link('http://loncapa.org/core.html', - &mt('LON-CAPA core group - MSU'),600,500)). + $corelink). '
      • '; } if ($changes{'reportupdates'}) { $resulttext .= '
      • '. &mt('E-mail record of completed LON-CAPA updates to [_1] set to "'. $offon[$env{'form.reportupdates'}].'".', - &Apache::loncommon::modal_link('http://loncapa.org/core.html', - &mt('LON-CAPA core group - MSU'),600,500)). + $corelink). + '
      • '; + } + if ($changes{'reportstatus'}) { + $resulttext .= '
      • '. + &mt('E-mail status if errors above threshold to [_1] set to "'. + $offon[$env{'form.reportstatus'}].'".', + $corelink). '
      • '; } + if (ref($changes{'lonstatus'}) eq 'ARRAY') { + $resulttext .= '
      • '. + &mt('Nightly status check e-mail settings').':
          '; + my (%defval,%use_def,%shown); + $defval{'threshold'} = $lonstatus_defs->{'threshold'}; + $defval{'sysmail'} = $lonstatus_defs->{'sysmail'}; + $defval{'weights'} = + join(', ',map { $lonstatus_names->{$_}.'='.$lonstatus_defs->{$_}; } ('E','W','N','U')); + $defval{'excluded'} = &mt('None'); + if (ref($contacts_hash{'contacts'}{'lonstatus'}) eq 'HASH') { + foreach my $item ('threshold','sysmail','weights','excluded') { + if (exists($contacts_hash{'contacts'}{'lonstatus'}{$item})) { + if (($item eq 'threshold') || ($item eq 'sysmail')) { + $shown{$item} = $contacts_hash{'contacts'}{'lonstatus'}{$item}; + } elsif ($item eq 'weights') { + if (ref($contacts_hash{'contacts'}{'lonstatus'}{$item}) eq 'HASH') { + foreach my $type ('E','W','N','U') { + $shown{$item} .= $lonstatus_names->{$type}.'='; + if (exists($contacts_hash{'contacts'}{'lonstatus'}{$item}{$type})) { + $shown{$item} .= $contacts_hash{'contacts'}{'lonstatus'}{$item}{$type}; + } else { + $shown{$item} .= $lonstatus_defs->{$type}; + } + $shown{$item} .= ', '; + } + $shown{$item} =~ s/, $//; + } else { + $shown{$item} = $defval{$item}; + } + } elsif ($item eq 'excluded') { + if (ref($contacts_hash{'contacts'}{'lonstatus'}{$item}) eq 'ARRAY') { + $shown{$item} = join(', ',@{$contacts_hash{'contacts'}{'lonstatus'}{$item}}); + } else { + $shown{$item} = $defval{$item}; + } + } + } else { + $shown{$item} = $defval{$item}; + } + } + } else { + foreach my $item ('threshold','weights','excluded','sysmail') { + $shown{$item} = $defval{$item}; + } + } + foreach my $item ('threshold','weights','excluded','sysmail') { + $resulttext .= '
        • '.&mt($titles->{'error'.$item}.' -- [_1]', + $shown{$item}).'
        • '; + } + $resulttext .= '
      • '; + } if ((ref($changes{'helpform'}) eq 'ARRAY') && (ref($fields) eq 'ARRAY')) { my (@optional,@required,@unused,$maxsizechg); foreach my $field (@{$changes{'helpform'}}) { @@ -12754,6 +17233,778 @@ sub modify_contacts { return $resulttext; } +sub modify_privacy { + my ($dom,%domconfig) = @_; + my ($resulttext,%current,%changes); + if (ref($domconfig{'privacy'}) eq 'HASH') { + %current = %{$domconfig{'privacy'}}; + } + my @fields = ('lastname','firstname','middlename','generation','permanentemail','id'); + my @items = ('domain','author','course','community'); + my %names = &Apache::lonlocal::texthash ( + domain => 'Assigned domain role(s)', + author => 'Assigned co-author role(s)', + course => 'Assigned course role(s)', + community => 'Assigned community role', + ); + my %roles = &Apache::lonlocal::texthash ( + domain => 'Domain role', + author => 'Co-author role', + course => 'Course role', + community => 'Community role', + ); + my %titles = &Apache::lonlocal::texthash ( + approval => 'Approval for role in different domain', + othdom => 'User information available in other domain', + priv => 'Information viewable by privileged user in same domain', + unpriv => 'Information viewable by unprivileged user in same domain', + instdom => 'Other domain shares institution/provider', + extdom => 'Other domain has different institution/provider', + none => 'Not allowed', + user => 'User authorizes', + domain => 'Domain Coordinator authorizes', + auto => 'Unrestricted', + ); + my %fieldnames = &Apache::lonlocal::texthash ( + id => 'Student/Employee ID', + permanentemail => 'E-mail address', + lastname => 'Last Name', + firstname => 'First Name', + middlename => 'Middle Name', + generation => 'Generation', + ); + my ($othertitle,$usertypes,$types) = + &Apache::loncommon::sorted_inst_types($dom); + my (%by_ip,%by_location,@intdoms,@instdoms); + &build_location_hashes(\@intdoms,\%by_ip,\%by_location,\@instdoms); + + my %privacyhash = ( + 'approval' => { + instdom => {}, + extdom => {}, + }, + 'othdom' => {}, + 'priv' => {}, + 'unpriv' => {}, + ); + foreach my $item (@items) { + if (@instdoms > 1) { + if ($env{'form.privacy_approval_instdom'.$item} =~ /^(none|user|domain|auto)$/) { + $privacyhash{'approval'}{'instdom'}{$item} = $env{'form.privacy_approval_instdom_'.$item}; + } + if (ref($current{'approval'}) eq 'HASH') { + if (ref($current{'approval'}{'instdom'}) eq 'HASH') { + unless ($privacyhash{'approval'}{'instdom'}{$item} eq $current{'approval'}{'instdom'}{$item}) { + $changes{'approval'} = 1; + } + } + } elsif ($privacyhash{'approval'}{'instdom'}{$item} ne 'auto') { + $changes{'approval'} = 1; + } + } + if (keys(%by_location) > 0) { + if ($env{'form.privacy_approval_extdom_'.$item} =~ /^(none|user|domain|auto)$/) { + $privacyhash{'approval'}{'extdom'}{$item} = $env{'form.privacy_approval_extdom_'.$item}; + } + if (ref($current{'approval'}) eq 'HASH') { + if (ref($current{'approval'}{'extdom'}) eq 'HASH') { + unless ($privacyhash{'approval'}{'extdom'}{$item} eq $current{'approval'}{'extdom'}{$item}) { + $changes{'approval'} = 1; + } + } + } elsif ($privacyhash{'approval'}{'extdom'}{$item} ne 'auto') { + $changes{'approval'} = 1; + } + } + foreach my $status ('priv','unpriv') { + my @possibles = sort(&Apache::loncommon::get_env_multiple('form.privacy_'.$status.'_'.$item)); + my @newvalues; + foreach my $field (@possibles) { + if (grep(/^\Q$field\E$/,@fields)) { + $privacyhash{$status}{$item}{$field} = 1; + push(@newvalues,$field); + } + } + @newvalues = sort(@newvalues); + if (ref($current{$status}) eq 'HASH') { + if (ref($current{$status}{$item}) eq 'HASH') { + my @currvalues = sort(keys(%{$current{$status}{$item}})); + my @diffs = &Apache::loncommon::compare_arrays(\@currvalues,\@newvalues); + if (@diffs > 0) { + $changes{$status} = 1; + } + } + } else { + my @stdfields; + foreach my $field (@fields) { + if ($field eq 'id') { + next if ($status eq 'unpriv'); + next if (($status eq 'priv') && ($item eq 'community')); + } + push(@stdfields,$field); + } + my @diffs = &Apache::loncommon::compare_arrays(\@stdfields,\@newvalues); + if (@diffs > 0) { + $changes{$status} = 1; + } + } + } + } + if ((@instdoms > 1) || (keys(%by_location) > 0)) { + my @statuses; + if (ref($types) eq 'ARRAY') { + @statuses = @{$types}; + } + foreach my $type (@statuses,'default') { + my @possfields = &Apache::loncommon::get_env_multiple('form.privacy_othdom_'.$type); + my @newvalues; + foreach my $field (sort(@possfields)) { + if (grep(/^\Q$field\E$/,@fields)) { + $privacyhash{'othdom'}{$type}{$field} = 1; + push(@newvalues,$field); + } + } + @newvalues = sort(@newvalues); + if (ref($current{'othdom'}) eq 'HASH') { + if (ref($current{'othdom'}{$type}) eq 'HASH') { + my @currvalues = sort(keys(%{$current{'othdom'}{$type}})); + my @diffs = &Apache::loncommon::compare_arrays(\@currvalues,\@newvalues); + if (@diffs > 0) { + $changes{'othdom'} = 1; + } + } + } else { + my @stdfields = ('lastname','firstname','middlename','generation','permanentemail'); + my @diffs = &Apache::loncommon::compare_arrays(\@stdfields,\@newvalues); + if (@diffs > 0) { + $changes{'othdom'} = 1; + } + } + } + } + my %confighash = ( + privacy => \%privacyhash, + ); + my $putresult = &Apache::lonnet::put_dom('configuration',\%confighash,$dom); + if ($putresult eq 'ok') { + if (keys(%changes) > 0) { + $resulttext = &mt('Changes made: ').'
          '; + foreach my $key ('approval','othdom','priv','unpriv') { + if ($changes{$key}) { + $resulttext .= '
        • '.$titles{$key}.':
            '; + if ($key eq 'approval') { + if (keys(%{$privacyhash{$key}{instdom}})) { + $resulttext .= '
          • '.$titles{'instdom'}.'
              '; + foreach my $item (@items) { + $resulttext .= '
            • '.$roles{$item}.': '.$titles{$privacyhash{$key}{instdom}{$item}}.'
            • '; + } + $resulttext .= '
          • '; + } + if (keys(%{$privacyhash{$key}{extdom}})) { + $resulttext .= '
          • '.$titles{'extdom'}.'
              '; + foreach my $item (@items) { + $resulttext .= '
            • '.$roles{$item}.': '.$titles{$privacyhash{$key}{extdom}{$item}}.'
            • '; + } + $resulttext .= '
          • '; + } + } elsif ($key eq 'othdom') { + my @statuses; + if (ref($types) eq 'ARRAY') { + @statuses = @{$types}; + } + if (ref($privacyhash{$key}) eq 'HASH') { + foreach my $status (@statuses,'default') { + if ($status eq 'default') { + $resulttext .= '
          • '.$othertitle.': '; + } elsif (ref($usertypes) eq 'HASH') { + $resulttext .= '
          • '.$usertypes->{$status}.': '; + } else { + next; + } + if (ref($privacyhash{$key}{$status}) eq 'HASH') { + if (keys(%{$privacyhash{$key}{$status}})) { + $resulttext .= join(', ', map { $fieldnames{$_}; } (sort(keys(%{$privacyhash{$key}{$status}})))); + } else { + $resulttext .= &mt('none'); + } + } + $resulttext .= '
          • '; + } + } + } else { + foreach my $item (@items) { + if (ref($privacyhash{$key}{$item}) eq 'HASH') { + $resulttext .= '
          • '.$names{$item}.': '; + if (keys(%{$privacyhash{$key}{$item}})) { + $resulttext .= join(', ', map { $fieldnames{$_}; } (sort(keys(%{$privacyhash{$key}{$item}})))); + } else { + $resulttext .= &mt('none'); + } + $resulttext .= '
          • '; + } + } + } + $resulttext .= '
        • '; + } + } + } else { + $resulttext = &mt('No changes made to user information settings'); + } + } else { + $resulttext = ''. + &mt('An error occurred: [_1]',$putresult).''; + } + return $resulttext; +} + +sub modify_passwords { + my ($r,$dom,$confname,$lastactref,%domconfig) = @_; + my ($resulttext,%current,%changes,%newvalues,@oktypes,$errors, + $updatedefaults,$updateconf); + my $customfn = 'resetpw.html'; + if (ref($domconfig{'passwords'}) eq 'HASH') { + %current = %{$domconfig{'passwords'}}; + } + my %domdefaults = &Apache::lonnet::get_domain_defaults($dom,1); + my ($othertitle,$usertypes,$types) = &Apache::loncommon::sorted_inst_types($dom); + if (ref($types) eq 'ARRAY') { + @oktypes = @{$types}; + } + push(@oktypes,'default'); + + my %titles = &Apache::lonlocal::texthash ( + intauth_cost => 'Encryption cost for bcrypt (positive integer)', + intauth_check => 'Check bcrypt cost if authenticated', + intauth_switch => 'Existing crypt-based switched to bcrypt on authentication', + permanent => 'Permanent e-mail address', + critical => 'Critical notification address', + notify => 'Notification address', + min => 'Minimum password length', + max => 'Maximum password length', + chars => 'Required characters', + expire => 'Password expiration (days)', + numsaved => 'Number of previous passwords to save', + reset => 'Resetting Forgotten Password', + intauth => 'Encryption of Stored Passwords (Internal Auth)', + rules => 'Rules for LON-CAPA Passwords', + crsownerchg => 'Course Owner Changing Student Passwords', + username => 'Username', + email => 'E-mail address', + ); + +# +# Retrieve current domain configuration for internal authentication from $domconfig{'defaults'}. +# + my (%curr_defaults,%save_defaults); + if (ref($domconfig{'defaults'}) eq 'HASH') { + foreach my $key (keys(%{$domconfig{'defaults'}})) { + if ($key =~ /^intauth_(cost|check|switch)$/) { + $curr_defaults{$key} = $domconfig{'defaults'}{$key}; + } else { + $save_defaults{$key} = $domconfig{'defaults'}{$key}; + } + } + } + my %staticdefaults = ( + 'resetlink' => 2, + 'resetcase' => \@oktypes, + 'resetprelink' => 'both', + 'resetemail' => ['critical','notify','permanent'], + 'intauth_cost' => 10, + 'intauth_check' => 0, + 'intauth_switch' => 0, + ); + $staticdefaults{'min'} = $Apache::lonnet::passwdmin; + foreach my $type (@oktypes) { + $staticdefaults{'resetpostlink'}{$type} = ['email','username']; + } + my $linklife = $env{'form.passwords_link'}; + $linklife =~ s/^\s+|\s+$//g; + if (($linklife =~ /^\d+(|\.\d*)$/) && ($linklife > 0)) { + $newvalues{'resetlink'} = $linklife; + if ($current{'resetlink'}) { + if ($current{'resetlink'} ne $linklife) { + $changes{'reset'} = 1; + } + } elsif (!ref($domconfig{passwords}) eq 'HASH') { + if ($staticdefaults{'resetlink'} ne $linklife) { + $changes{'reset'} = 1; + } + } + } elsif ($current{'resetlink'}) { + $changes{'reset'} = 1; + } + my @casesens; + my @posscase = &Apache::loncommon::get_env_multiple('form.passwords_case_sensitive'); + foreach my $case (sort(@posscase)) { + if (grep(/^\Q$case\E$/,@oktypes)) { + push(@casesens,$case); + } + } + $newvalues{'resetcase'} = \@casesens; + if (ref($current{'resetcase'}) eq 'ARRAY') { + my @diffs = &Apache::loncommon::compare_arrays($current{'resetcase'},\@casesens); + if (@diffs > 0) { + $changes{'reset'} = 1; + } + } elsif (!ref($domconfig{passwords}) eq 'HASH') { + my @diffs = &Apache::loncommon::compare_arrays($staticdefaults{'resetcase'},\@casesens); + if (@diffs > 0) { + $changes{'reset'} = 1; + } + } + if ($env{'form.passwords_prelink'} =~ /^(both|either)$/) { + $newvalues{'resetprelink'} = $env{'form.passwords_prelink'}; + if (exists($current{'resetprelink'})) { + if ($current{'resetprelink'} ne $newvalues{'resetprelink'}) { + $changes{'reset'} = 1; + } + } elsif (!ref($domconfig{passwords}) eq 'HASH') { + if ($staticdefaults{'resetprelink'} ne $newvalues{'resetprelink'}) { + $changes{'reset'} = 1; + } + } + } elsif ($current{'resetprelink'}) { + $changes{'reset'} = 1; + } + foreach my $type (@oktypes) { + my @possplink = &Apache::loncommon::get_env_multiple('form.passwords_postlink_'.$type); + my @postlink; + foreach my $item (sort(@possplink)) { + if ($item =~ /^(email|username)$/) { + push(@postlink,$item); + } + } + $newvalues{'resetpostlink'}{$type} = \@postlink; + unless ($changes{'reset'}) { + if (ref($current{'resetpostlink'}) eq 'HASH') { + if (ref($current{'resetpostlink'}{$type}) eq 'ARRAY') { + my @diffs = &Apache::loncommon::compare_arrays($current{'resetpostlink'}{$type},\@postlink); + if (@diffs > 0) { + $changes{'reset'} = 1; + } + } else { + $changes{'reset'} = 1; + } + } elsif (!ref($domconfig{passwords}) eq 'HASH') { + my @diffs = &Apache::loncommon::compare_arrays($staticdefaults{'resetpostlink'}{$type},\@postlink); + if (@diffs > 0) { + $changes{'reset'} = 1; + } + } + } + } + my @possemailsrc = &Apache::loncommon::get_env_multiple('form.passwords_emailsrc'); + my @resetemail; + foreach my $item (sort(@possemailsrc)) { + if ($item =~ /^(permanent|critical|notify)$/) { + push(@resetemail,$item); + } + } + $newvalues{'resetemail'} = \@resetemail; + unless ($changes{'reset'}) { + if (ref($current{'resetemail'}) eq 'ARRAY') { + my @diffs = &Apache::loncommon::compare_arrays($current{'resetemail'},\@resetemail); + if (@diffs > 0) { + $changes{'reset'} = 1; + } + } elsif (!ref($domconfig{passwords}) eq 'HASH') { + my @diffs = &Apache::loncommon::compare_arrays($staticdefaults{'resetemail'},\@resetemail); + if (@diffs > 0) { + $changes{'reset'} = 1; + } + } + } + if ($env{'form.passwords_stdtext'} == 0) { + $newvalues{'resetremove'} = 1; + unless ($current{'resetremove'}) { + $changes{'reset'} = 1; + } + } elsif ($current{'resetremove'}) { + $changes{'reset'} = 1; + } + if ($env{'form.passwords_customfile.filename'} ne '') { + my $servadm = $r->dir_config('lonAdmEMail'); + my ($configuserok,$author_ok,$switchserver) = + &config_check($dom,$confname,$servadm); + my $error; + if ($configuserok eq 'ok') { + if ($switchserver) { + $error = &mt("Upload of file containing domain-specific text is not permitted to this server: [_1]",$switchserver); + } else { + if ($author_ok eq 'ok') { + my ($result,$customurl) = + &publishlogo($r,'upload','passwords_customfile',$dom, + $confname,'customtext/resetpw','','',$customfn); + if ($result eq 'ok') { + $newvalues{'resetcustom'} = $customurl; + $changes{'reset'} = 1; + } else { + $error = &mt("Upload of [_1] failed because an error occurred publishing the file in RES space. Error was: [_2].",$customfn,$result); + } + } else { + $error = &mt("Upload of [_1] failed because an author role could not be assigned to a Domain Configuration user ([_2]) in domain: [_3]. Error was: [_4].",$customfn,$confname,$dom,$author_ok); + } + } + } else { + $error = &mt("Upload of [_1] failed because a Domain Configuration user ([_2]) could not be created in domain: [_3]. Error was: [_4].",$customfn,$confname,$dom,$configuserok); + } + if ($error) { + &Apache::lonnet::logthis($error); + $errors .= '
        • '.$error.'
        • '; + } + } elsif ($current{'resetcustom'}) { + if ($env{'form.passwords_custom_del'}) { + $changes{'reset'} = 1; + } else { + $newvalues{'resetcustom'} = $current{'resetcustom'}; + } + } + $env{'form.intauth_cost'} =~ s/^\s+|\s+$//g; + if (($env{'form.intauth_cost'} ne '') && ($env{'form.intauth_cost'} =~ /^\d+$/)) { + $save_defaults{'intauth_cost'} = $env{'form.intauth_cost'}; + if ($save_defaults{'intauth_cost'} ne $curr_defaults{'intauth_cost'}) { + $changes{'intauth'} = 1; + } + } else { + $save_defaults{'intauth_cost'} = $curr_defaults{'intauth_cost'}; + } + if ($env{'form.intauth_check'} =~ /^(0|1|2)$/) { + $save_defaults{'intauth_check'} = $env{'form.intauth_check'}; + if ($save_defaults{'intauth_check'} ne $curr_defaults{'intauth_check'}) { + $changes{'intauth'} = 1; + } + } else { + $save_defaults{'intauth_check'} = $curr_defaults{'intauth_check'}; + } + if ($env{'form.intauth_switch'} =~ /^(0|1|2)$/) { + $save_defaults{'intauth_switch'} = $env{'form.intauth_switch'}; + if ($save_defaults{'intauth_switch'} ne $curr_defaults{'intauth_switch'}) { + $changes{'intauth'} = 1; + } + } else { + $save_defaults{'intauth_check'} = $curr_defaults{'intauth_check'}; + } + foreach my $item ('cost','check','switch') { + if ($save_defaults{'intauth_'.$item} ne $domdefaults{'intauth_'.$item}) { + $domdefaults{'intauth_'.$item} = $save_defaults{'intauth_'.$item}; + $updatedefaults = 1; + } + } + foreach my $rule ('min','max','expire','numsaved') { + $env{'form.passwords_'.$rule} =~ s/^\s+|\s+$//g; + my $ruleok; + if ($rule eq 'expire') { + if (($env{'form.passwords_'.$rule} =~ /^\d+(|\.\d*)$/) && + ($env{'form.passwords_'.$rule} ne '0')) { + $ruleok = 1; + } + } elsif ($rule eq 'min') { + if ($env{'form.passwords_'.$rule} =~ /^\d+$/) { + if ($env{'form.passwords_'.$rule} >= $Apache::lonnet::passwdmin) { + $ruleok = 1; + } + } + } elsif (($env{'form.passwords_'.$rule} =~ /^\d+$/) && + ($env{'form.passwords_'.$rule} ne '0')) { + $ruleok = 1; + } + if ($ruleok) { + $newvalues{$rule} = $env{'form.passwords_'.$rule}; + if (exists($current{$rule})) { + if ($newvalues{$rule} ne $current{$rule}) { + $changes{'rules'} = 1; + } + } elsif ($rule eq 'min') { + if ($staticdefaults{$rule} ne $newvalues{$rule}) { + $changes{'rules'} = 1; + } + } else { + $changes{'rules'} = 1; + } + } elsif (exists($current{$rule})) { + $changes{'rules'} = 1; + } + } + my @posschars = &Apache::loncommon::get_env_multiple('form.passwords_chars'); + my @chars; + foreach my $item (sort(@posschars)) { + if ($item =~ /^(uc|lc|num|spec)$/) { + push(@chars,$item); + } + } + $newvalues{'chars'} = \@chars; + unless ($changes{'rules'}) { + if (ref($current{'chars'}) eq 'ARRAY') { + my @diffs = &Apache::loncommon::compare_arrays($current{'chars'},\@chars); + if (@diffs > 0) { + $changes{'rules'} = 1; + } + } else { + if (@chars > 0) { + $changes{'rules'} = 1; + } + } + } + my %crsownerchg = ( + by => [], + for => [], + ); + foreach my $item ('by','for') { + my @posstypes = &Apache::loncommon::get_env_multiple('form.passwords_crsowner_'.$item); + foreach my $type (sort(@posstypes)) { + if (grep(/^\Q$type\E$/,@oktypes)) { + push(@{$crsownerchg{$item}},$type); + } + } + } + $newvalues{'crsownerchg'} = \%crsownerchg; + if (ref($current{'crsownerchg'}) eq 'HASH') { + foreach my $item ('by','for') { + if (ref($current{'crsownerchg'}{$item}) eq 'ARRAY') { + my @diffs = &Apache::loncommon::compare_arrays($current{'crsownerchg'}{$item},$crsownerchg{$item}); + if (@diffs > 0) { + $changes{'crsownerchg'} = 1; + last; + } + } + } + } elsif (!(ref($domconfig{passwords}) eq 'HASH')) { + foreach my $item ('by','for') { + if (@{$crsownerchg{$item}} > 0) { + $changes{'crsownerchg'} = 1; + last; + } + } + } + + my %confighash = ( + defaults => \%save_defaults, + passwords => \%newvalues, + ); + &process_captcha('passwords',\%changes,$confighash{'passwords'},$domconfig{'passwords'}); + + my $putresult = &Apache::lonnet::put_dom('configuration',\%confighash,$dom); + if ($putresult eq 'ok') { + if (keys(%changes) > 0) { + $resulttext = &mt('Changes made: ').'
            '; + foreach my $key ('reset','intauth','rules','crsownerchg') { + if ($changes{$key}) { + unless ($key eq 'intauth') { + $updateconf = 1; + } + $resulttext .= '
          • '.$titles{$key}.':
              '; + if ($key eq 'reset') { + if ($confighash{'passwords'}{'captcha'} eq 'original') { + $resulttext .= '
            • '.&mt('CAPTCHA validation set to use: original CAPTCHA').'
            • '; + } elsif ($confighash{'passwords'}{'captcha'} eq 'recaptcha') { + $resulttext .= '
            • '.&mt('CAPTCHA validation set to use: reCAPTCHA').' '. + &mt('version: [_1]',$confighash{'passwords'}{'recaptchaversion'}).'
              '; + if (ref($confighash{'passwords'}{'recaptchakeys'}) eq 'HASH') { + $resulttext .= &mt('Public key: [_1]',$confighash{'passwords'}{'recaptchakeys'}{'public'}).'
              '. + &mt('Private key: [_1]',$confighash{'passwords'}{'recaptchakeys'}{'private'}).'
            • '; + } + } else { + $resulttext .= '
            • '.&mt('No CAPTCHA validation').'
            • '; + } + if ($confighash{'passwords'}{'resetlink'}) { + $resulttext .= '
            • '.&mt('Reset link expiration set to [quant,_1,hour]',$confighash{'passwords'}{'resetlink'}).'
            • '; + } else { + $resulttext .= '
            • '.&mt('No reset link expiration set.').' '. + &mt('Will default to 2 hours').'
            • '; + } + if (ref($confighash{'passwords'}{'resetcase'}) eq 'ARRAY') { + if (@{$confighash{'passwords'}{'resetcase'}} == 0) { + $resulttext .= '
            • '.&mt('User input for username and/or e-mail address not case sensitive for "Forgot Password" web form').'
            • '; + } else { + my $casesens; + foreach my $type (@{$confighash{'passwords'}{'resetcase'}}) { + if ($type eq 'default') { + $casesens .= $othertitle.', '; + } elsif ($usertypes->{$type} ne '') { + $casesens .= $usertypes->{$type}.', '; + } + } + $casesens =~ s/\Q, \E$//; + $resulttext .= '
            • '.&mt('"Forgot Password" web form input for username and/or e-mail address is case-sensitive for: [_1]',$casesens).'
            • '; + } + } else { + $resulttext .= '
            • '.&mt('Case-sensitivity not set for "Forgot Password" web form').' '.&mt('Will default to case-sensitive for username and/or e-mail address for all').'
            • '; + } + if ($confighash{'passwords'}{'resetprelink'} eq 'either') { + $resulttext .= '
            • '.&mt('Users can enter either a username or an e-mail address in "Forgot Password" web form').'
            • '; + } else { + $resulttext .= '
            • '.&mt('Users can enter both a username and an e-mail address in "Forgot Password" web form').'
            • '; + } + if (ref($confighash{'passwords'}{'resetpostlink'}) eq 'HASH') { + my $output; + if (ref($types) eq 'ARRAY') { + foreach my $type (@{$types}) { + if (ref($confighash{'passwords'}{'resetpostlink'}{$type}) eq 'ARRAY') { + if (@{$confighash{'passwords'}{'resetpostlink'}{$type}} == 0) { + $output .= $usertypes->{$type}.' -- '.&mt('none'); + } else { + $output .= $usertypes->{$type}.' -- '. + join(', ',map { $titles{$_}; } (@{$confighash{'passwords'}{'resetpostlink'}{$type}})).'; '; + } + } + } + } + if (ref($confighash{'passwords'}{'resetpostlink'}{'default'}) eq 'ARRAY') { + if (@{$confighash{'passwords'}{'resetpostlink'}{'default'}} == 0) { + $output .= $othertitle.' -- '.&mt('none'); + } else { + $output .= $othertitle.' -- '. + join(', ',map { $titles{$_}; } (@{$confighash{'passwords'}{'resetpostlink'}{'default'}})); + } + } + if ($output) { + $resulttext .= '
            • '.&mt('Information required for new password form (by user type) set to: [_1]',$output).'
            • '; + } else { + $resulttext .= '
            • '.&mt('Information required for new password form not set.').' '.&mt('Will default to requiring both the username and an e-mail address').'
            • '; + } + } else { + $resulttext .= '
            • '.&mt('Information required for new password form not set.').' '.&mt('Will default to requiring both the username and an e-mail address').'
            • '; + } + if (ref($confighash{'passwords'}{'resetemail'}) eq 'ARRAY') { + if (@{$confighash{'passwords'}{'resetemail'}} > 0) { + $resulttext .= '
            • '.&mt('E-mail address(es) in LON-CAPA used for verification will include: [_1]',join(', ',map { $titles{$_}; } @{$confighash{'passwords'}{'resetemail'}})).'
            • '; + } else { + $resulttext .= '
            • '.&mt('E-mail address(es) in LON-CAPA used for verification will include: [_1]',join(', ',map { $titles{$_}; } @{$staticdefaults{'resetemail'}})).'
            • '; + } + } else { + $resulttext .= '
            • '.&mt('E-mail address(es) in LON-CAPA used for verification will include: [_1]',join(', ',map { $titles{$_}; } @{$staticdefaults{'resetemail'}})).'
            • '; + } + if ($confighash{'passwords'}{'resetremove'}) { + $resulttext .= '
            • '.&mt('Preamble to "Forgot Password" web form not shown').'
            • '; + } else { + $resulttext .= '
            • '.&mt('Preamble to "Forgot Password" web form is shown').'
            • '; + } + if ($confighash{'passwords'}{'resetcustom'}) { + my $customlink = &Apache::loncommon::modal_link($confighash{'passwords'}{'resetcustom'}, + &mt('custom text'),600,500,undef,undef, + undef,undef,'background-color:#ffffff'); + $resulttext .= '
            • '.&mt('Preamble to "Forgot Password" form includes: [_1]',$customlink).'
            • '; + } else { + $resulttext .= '
            • '.&mt('No custom text included in preamble to "Forgot Password" form').'
            • '; + } + } elsif ($key eq 'intauth') { + foreach my $item ('cost','switch','check') { + my $value = $save_defaults{$key.'_'.$item}; + if ($item eq 'switch') { + my %optiondesc = &Apache::lonlocal::texthash ( + 0 => 'No', + 1 => 'Yes', + 2 => 'Yes, and copy existing passwd file to passwd.bak file', + ); + if ($value =~ /^(0|1|2)$/) { + $value = $optiondesc{$value}; + } else { + $value = &mt('none -- defaults to No'); + } + } elsif ($item eq 'check') { + my %optiondesc = &Apache::lonlocal::texthash ( + 0 => 'No', + 1 => 'Yes, allow login then update passwd file using default cost (if higher)', + 2 => 'Yes, disallow login if stored cost is less than domain default', + ); + if ($value =~ /^(0|1|2)$/) { + $value = $optiondesc{$value}; + } else { + $value = &mt('none -- defaults to No'); + } + } + $resulttext .= '
            • '.&mt('[_1] set to "[_2]"',$titles{$key.'_'.$item},$value).'
            • '; + } + } elsif ($key eq 'rules') { + foreach my $rule ('min','max','expire','numsaved') { + if ($confighash{'passwords'}{$rule} eq '') { + if ($rule eq 'min') { + $resulttext .= '
            • '.&mt('[_1] not set.',$titles{$rule}); + ' '.&mt('Default of [_1] will be used', + $Apache::lonnet::passwdmin).'
            • '; + } else { + $resulttext .= '
            • '.&mt('[_1] set to none',$titles{$rule}).'
            • '; + } + } else { + $resulttext .= '
            • '.&mt('[_1] set to [_2]',$titles{$rule},$confighash{'passwords'}{$rule}).'
            • '; + } + } + if (ref($confighash{'passwords'}{'chars'}) eq 'ARRAY') { + if (@{$confighash{'passwords'}{'chars'}} > 0) { + 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', + ); + my $needed = '
              • '. + join('
              • ',map {$rulenames{$_} } @{$confighash{'passwords'}{'chars'}}). + '
              '; + $resulttext .= '
            • '.&mt('[_1] set to: [_2]',$titles{'chars'},$needed).'
            • '; + } else { + $resulttext .= '
            • '.&mt('[_1] set to none',$titles{'chars'}).'
            • '; + } + } else { + $resulttext .= '
            • '.&mt('[_1] set to none',$titles{'chars'}).'
            • '; + } + } elsif ($key eq 'crsownerchg') { + if (ref($confighash{'passwords'}{'crsownerchg'}) eq 'HASH') { + if ((@{$confighash{'passwords'}{'crsownerchg'}{'by'}} == 0) || + (@{$confighash{'passwords'}{'crsownerchg'}{'for'}} == 0)) { + $resulttext .= '
            • '.&mt('Course owner may not change student passwords.').'
            • '; + } else { + my %crsownerstr; + foreach my $item ('by','for') { + if (ref($confighash{'passwords'}{'crsownerchg'}{$item}) eq 'ARRAY') { + foreach my $type (@{$confighash{'passwords'}{'crsownerchg'}{$item}}) { + if ($type eq 'default') { + $crsownerstr{$item} .= $othertitle.', '; + } elsif ($usertypes->{$type} ne '') { + $crsownerstr{$item} .= $usertypes->{$type}.', '; + } + } + $crsownerstr{$item} =~ s/\Q, \E$//; + } + } + $resulttext .= '
            • '.&mt('Course owner (with status: [_1]) may change passwords for students (with status: [_2]).', + $crsownerstr{'by'},$crsownerstr{'for'}).'
            • '; + } + } else { + $resulttext .= '
            • '.&mt('Course owner may not change student passwords.').'
            • '; + } + } + $resulttext .= '
          • '; + } + } + $resulttext .= '
          '; + } else { + $resulttext = &mt('No changes made to password settings'); + } + my $cachetime = 24*60*60; + if ($updatedefaults) { + &Apache::lonnet::do_cache_new('domdefaults',$dom,\%domdefaults,$cachetime); + if (ref($lastactref) eq 'HASH') { + $lastactref->{'domdefaults'} = 1; + } + } + if ($updateconf) { + &Apache::lonnet::do_cache_new('passwdconf',$dom,$confighash{'passwords'},$cachetime); + if (ref($lastactref) eq 'HASH') { + $lastactref->{'passwdconf'} = 1; + } + } + } else { + $resulttext = ''. + &mt('An error occurred: [_1]',$putresult).''; + } + if ($errors) { + $resulttext .= '

          '.&mt('The following errors occurred: ').'

            '. + $errors.'

          '; + } + return $resulttext; +} + sub modify_usercreation { my ($dom,%domconfig) = @_; my ($resulttext,%curr_usercreation,%changes,%authallowed,%cancreate,%save_usercreate); @@ -12838,7 +18089,7 @@ sub modify_usercreation { } my @authen_contexts = ('author','course','domain'); - my @authtypes = ('int','krb4','krb5','loc'); + my @authtypes = ('int','krb4','krb5','loc','lti'); my %authhash; foreach my $item (@authen_contexts) { my @authallowed = &Apache::loncommon::get_env_multiple('form.'.$item.'_auth'); @@ -13567,7 +18818,7 @@ sub modify_selfcreation { $chgtext .= &mt('For self-created accounts verified by e-mail address, username is set as follows:'). '
            '; foreach my $status (@statuses) { - if ($type eq 'default') { + if ($status eq 'default') { $chgtext .= '
          • '.$othertitle.' -- '.$options{$cancreate{'emailverified'}{$status}}.'
          • '; } else { $chgtext .= '
          • '.$usertypes{$status}.' -- '.$options{$cancreate{'emailverified'}{$status}}.'
          • '; @@ -13836,19 +19087,25 @@ sub modify_selfcreation { } sub process_captcha { - my ($container,$changes,$newsettings,$current) = @_; - return unless ((ref($changes) eq 'HASH') && (ref($newsettings) eq 'HASH') || (ref($current) eq 'HASH')); + 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') { $newsettings->{'captcha'} = 'original'; } - if ($current->{'captcha'} ne $newsettings->{'captcha'}) { + my %current; + if (ref($currsettings) eq 'HASH') { + %current = %{$currsettings}; + } + if ($current{'captcha'} ne $newsettings->{'captcha'}) { if ($container eq 'cancreate') { if (ref($changes->{'cancreate'}) eq 'ARRAY') { push(@{$changes->{'cancreate'}},'captcha'); } elsif (!defined($changes->{'cancreate'})) { $changes->{'cancreate'} = ['captcha']; } + } elsif ($container eq 'passwords') { + $changes->{'reset'} = 1; } else { $changes->{'captcha'} = 1; } @@ -13870,9 +19127,9 @@ sub process_captcha { } $newsettings->{'recaptchaversion'} = $newversion; } - if (ref($current->{'recaptchakeys'}) eq 'HASH') { - $currpub = $current->{'recaptchakeys'}{'public'}; - $currpriv = $current->{'recaptchakeys'}{'private'}; + if (ref($current{'recaptchakeys'}) eq 'HASH') { + $currpub = $current{'recaptchakeys'}{'public'}; + $currpriv = $current{'recaptchakeys'}{'private'}; unless ($newsettings->{'captcha'} eq 'recaptcha') { $newsettings->{'recaptchakeys'} = { public => '', @@ -13880,8 +19137,8 @@ sub process_captcha { } } } - if ($current->{'captcha'} eq 'recaptcha') { - $currversion = $current->{'recaptchaversion'}; + if ($current{'captcha'} eq 'recaptcha') { + $currversion = $current{'recaptchaversion'}; if ($currversion ne '2') { $currversion = 1; } @@ -13893,6 +19150,8 @@ sub process_captcha { } elsif (!defined($changes->{'cancreate'})) { $changes->{'cancreate'} = ['recaptchaversion']; } + } elsif ($container eq 'passwords') { + $changes->{'reset'} = 1; } else { $changes->{'recaptchaversion'} = 1; } @@ -13904,6 +19163,8 @@ sub process_captcha { } elsif (!defined($changes->{'cancreate'})) { $changes->{'cancreate'} = ['recaptchakeys']; } + } elsif ($container eq 'passwords') { + $changes->{'reset'} = 1; } else { $changes->{'recaptchakeys'} = 1; } @@ -14019,8 +19280,8 @@ sub modify_defaults { my ($resulttext,$mailmsgtxt,%newvalues,%changes,@errors); my %domdefaults = &Apache::lonnet::get_domain_defaults($dom,1); my @items = ('auth_def','auth_arg_def','lang_def','timezone_def','datelocale_def', - 'portal_def','intauth_cost','intauth_check','intauth_switch'); - my @authtypes = ('internal','krb4','krb5','localauth'); + 'portal_def'); + my @authtypes = ('internal','krb4','krb5','localauth','lti'); foreach my $item (@items) { $newvalues{$item} = $env{'form.'.$item}; if ($item eq 'auth_def') { @@ -14061,24 +19322,6 @@ sub modify_defaults { push(@errors,$item); } } - } elsif ($item eq 'intauth_cost') { - if ($newvalues{$item} ne '') { - if ($newvalues{$item} =~ /\D/) { - push(@errors,$item); - } - } - } elsif ($item eq 'intauth_check') { - if ($newvalues{$item} ne '') { - unless ($newvalues{$item} =~ /^(0|1|2)$/) { - push(@errors,$item); - } - } - } elsif ($item eq 'intauth_switch') { - if ($newvalues{$item} ne '') { - unless ($newvalues{$item} =~ /^(0|1|2)$/) { - push(@errors,$item); - } - } } if (grep(/^\Q$item\E$/,@errors)) { $newvalues{$item} = $domdefaults{$item}; @@ -14087,6 +19330,18 @@ sub modify_defaults { } $domdefaults{$item} = $newvalues{$item}; } + my %staticdefaults = ( + 'intauth_cost' => 10, + 'intauth_check' => 0, + 'intauth_switch' => 0, + ); + foreach my $item ('intauth_cost','intauth_check','intauth_switch') { + if (exists($domdefaults{$item})) { + $newvalues{$item} = $domdefaults{$item}; + } else { + $newvalues{$item} = $staticdefaults{$item}; + } + } my %defaults_hash = ( defaults => \%newvalues, ); @@ -14212,30 +19467,9 @@ sub modify_defaults { krb4 => 'krb4', krb5 => 'krb5', localauth => 'loc', + lti => 'lti', ); $value = $authnames{$shortauth{$value}}; - } elsif ($item eq 'intauth_switch') { - my %optiondesc = &Apache::lonlocal::texthash ( - 0 => 'No', - 1 => 'Yes', - 2 => 'Yes, and copy existing passwd file to passwd.bak file', - ); - if ($value =~ /^(0|1|2)$/) { - $value = $optiondesc{$value}; - } else { - $value = &mt('none -- defaults to No'); - } - } elsif ($item eq 'intauth_check') { - my %optiondesc = &Apache::lonlocal::texthash ( - 0 => 'No', - 1 => 'Yes, allow login then update passwd file using default cost (if higher)', - 2 => 'Yes, disallow login if stored cost is less than domain default', - ); - if ($value =~ /^(0|1|2)$/) { - $value = $optiondesc{$value}; - } else { - $value = &mt('none -- defaults to No'); - } } $resulttext .= '
          • '.&mt('[_1] set to "[_2]"',$title->{$item},$value).'
          • '; $mailmsgtext .= "$title->{$item} set to $value\n"; @@ -14284,7 +19518,7 @@ sub modify_scantron { my $custom = 'custom.tab'; my $default = 'default.tab'; my $servadm = $r->dir_config('lonAdmEMail'); - my ($configuserok,$author_ok,$switchserver) = + my ($configuserok,$author_ok,$switchserver) = &config_check($dom,$confname,$servadm); if ($env{'form.scantronformat.filename'} ne '') { my $error; @@ -14319,6 +19553,67 @@ sub modify_scantron { if ($env{'form.scantronformat_del'}) { $confhash{'scantron'}{'scantronformat'} = ''; $changes{'scantronformat'} = 1; + } else { + $confhash{'scantron'}{'scantronformat'} = $domconfig{'scantron'}{'scantronformat'}; + } + } + } + my @options = ('hdr','pad','rem'); + my @fields = &scantroncsv_fields(); + my %titles = &scantronconfig_titles(); + my @formats = &Apache::loncommon::get_env_multiple('form.scantronconfig'); + my ($newdat,$currdat,%newcol,%currcol); + if (grep(/^dat$/,@formats)) { + $confhash{'scantron'}{config}{dat} = 1; + $newdat = 1; + } else { + $newdat = 0; + } + if (grep(/^csv$/,@formats)) { + my %bynum; + foreach my $field (@fields) { + if ($env{'form.scantronconfig_csv_'.$field} =~ /^(\d+)$/) { + my $posscol = $1; + if (($posscol < 20) && (!$bynum{$posscol})) { + $confhash{'scantron'}{config}{csv}{fields}{$field} = $posscol; + $bynum{$posscol} = $field; + $newcol{$field} = $posscol; + } + } + } + if (keys(%newcol)) { + foreach my $option (@options) { + if ($env{'form.scantroncsv_'.$option}) { + $confhash{'scantron'}{config}{csv}{options}{$option} = 1; + } + } + } + } + $currdat = 1; + if (ref($domconfig{'scantron'}) eq 'HASH') { + if (ref($domconfig{'scantron'}{'config'}) eq 'HASH') { + unless (exists($domconfig{'scantron'}{'config'}{'dat'})) { + $currdat = 0; + } + if (ref($domconfig{'scantron'}{'config'}{'csv'}) eq 'HASH') { + if (ref($domconfig{'scantron'}{'config'}{'csv'}{'fields'}) eq 'HASH') { + %currcol = %{$domconfig{'scantron'}{'config'}{'csv'}{'fields'}}; + } + } + } + } + if ($currdat != $newdat) { + $changes{'config'} = 1; + } else { + foreach my $field (@fields) { + if ($currcol{$field} ne '') { + if ($currcol{$field} ne $newcol{$field}) { + $changes{'config'} = 1; + last; + } + } elsif ($newcol{$field} ne '') { + $changes{'config'} = 1; + last; } } } @@ -14329,22 +19624,57 @@ sub modify_scantron { if (keys(%changes) > 0) { if (ref($confhash{'scantron'}) eq 'HASH') { $resulttext = &mt('Changes made:').'
              '; - if ($confhash{'scantron'}{'scantronformat'} eq '') { - $resulttext .= '
            • '.&mt('[_1] bubblesheet format file removed; [_2] file will be used for courses in this domain.',$custom,$default).'
            • '; - } else { - $resulttext .= '
            • '.&mt('Custom bubblesheet format file ([_1]) uploaded for use with courses in this domain.',$custom).'
            • '; + if ($changes{'scantronformat'}) { + if ($confhash{'scantron'}{'scantronformat'} eq '') { + $resulttext .= '
            • '.&mt('[_1] bubblesheet format file removed; [_2] file will be used for courses in this domain.',$custom,$default).'
            • '; + } else { + $resulttext .= '
            • '.&mt('Custom bubblesheet format file ([_1]) uploaded for use with courses in this domain.',$custom).'
            • '; + } + } + if ($changes{'config'}) { + if (ref($confhash{'scantron'}{'config'}) eq 'HASH') { + if ($confhash{'scantron'}{'config'}{'dat'}) { + $resulttext .= '
            • '.&mt('Bubblesheet data upload formats includes .dat format').'
            • '; + } + if (ref($confhash{'scantron'}{'config'}{'csv'}) eq 'HASH') { + if (ref($confhash{'scantron'}{'config'}{'csv'}{'fields'}) eq 'HASH') { + if (keys(%{$confhash{'scantron'}{'config'}{'csv'}{'fields'}})) { + $resulttext .= '
            • '.&mt('Bubblesheet data upload formats includes .csv format, with following fields/column numbers supported:').'
                '; + foreach my $field (@fields) { + if ($confhash{'scantron'}{'config'}{'csv'}{'fields'}{$field} ne '') { + my $showcol = $confhash{'scantron'}{'config'}{'csv'}{'fields'}{$field} + 1; + $resulttext .= '
              • '.$titles{$field}.': '.$showcol.'
              • '; + } + } + $resulttext .= '
            • '; + if (ref($confhash{'scantron'}{'config'}{'csv'}{'options'}) eq 'HASH') { + if (keys(%{$confhash{'scantron'}{'config'}{'csv'}{'options'}})) { + $resulttext .= '
            • '.&mt('Bubblesheet data upload formats includes .csv format, with following options:').'
                '; + foreach my $option (@options) { + if ($confhash{'scantron'}{'config'}{'csv'}{'options'}{$option} ne '') { + $resulttext .= '
              • '.$titles{$option}.'
              • '; + } + } + $resulttext .= '
            • '; + } + } + } + } + } + } else { + $resulttext .= '
            • '.&mt('No bubblesheet data upload formats set -- will default to assuming .dat format').'
            • '; + } } $resulttext .= '
            '; } else { $resulttext = &mt('Changes made to bubblesheet format file.'); } - $resulttext .= '
          '; &Apache::loncommon::devalidate_domconfig_cache($dom); if (ref($lastactref) eq 'HASH') { $lastactref->{'domainconfig'} = 1; } } else { - $resulttext = &mt('No changes made to bubblesheet format file'); + $resulttext = &mt('No changes made to bubblesheet format settings'); } } else { $resulttext = ''. @@ -14354,8 +19684,8 @@ sub modify_scantron { $resulttext = &mt('No changes made to bubblesheet format file'); } if ($errors) { - $resulttext .= &mt('The following errors occurred: ').'
            '. - $errors.'
          '; + $resulttext .= '

          '.&mt('The following errors occurred: ').'

            '. + $errors.'

          '; } return $resulttext; } @@ -14630,6 +19960,10 @@ sub modify_coursecategories { } $resulttext .= '
        '; } + &Apache::lonnet::do_cache_new('cats',$dom,$cathash,3600); + if (ref($lastactref) eq 'HASH') { + $lastactref->{'cats'} = 1; + } } $resulttext .= '
      '; if ($changes{'unauth'} || $changes{'auth'}) { @@ -15111,9 +20445,10 @@ sub modify_coursedefaults { my %defaultchecked = ( 'canuse_pdfforms' => 'off', 'uselcmath' => 'on', - 'usejsme' => 'on' + 'usejsme' => 'on', + 'inline_chem' => 'on', ); - my @toggles = ('canuse_pdfforms','uselcmath','usejsme'); + my @toggles = ('canuse_pdfforms','uselcmath','usejsme','inline_chem'); my @numbers = ('anonsurvey_threshold','uploadquota_official','uploadquota_unofficial', 'uploadquota_community','uploadquota_textbook','uploadquota_placement', 'mysqltables_official','mysqltables_unofficial','mysqltables_community', @@ -15176,7 +20511,6 @@ sub modify_coursedefaults { $defaultshash{'coursedefaults'}{$setting}{$type} = $newdef; } if ($currdef ne $newdef) { - my $staticdef; if ($item eq 'anonsurvey_threshold') { unless (($currdef eq '') && ($newdef == $staticdefaults{$item})) { $changes{$item} = 1; @@ -15192,11 +20526,12 @@ sub modify_coursedefaults { my $texengine; if ($env{'form.texengine'} =~ /^(MathJax|mimetex|tth)$/) { $texengine = $env{'form.texengine'}; - if ($defaultshash{'coursedefaults'}{'texengine'} eq '') { - unless ($texengine eq 'MathJax') { + my $currdef = $domconfig{'coursedefaults'}{'texengine'}; + if ($currdef eq '') { + unless ($texengine eq $Apache::lonnet::deftex) { $changes{'texengine'} = 1; } - } elsif ($defaultshash{'coursedefaults'}{'texengine'} ne $texengine) { + } elsif ($currdef ne $texengine) { $changes{'texengine'} = 1; } } @@ -15323,8 +20658,9 @@ sub modify_coursedefaults { my %domdefaults = &Apache::lonnet::get_domain_defaults($dom,1); if (($changes{'canuse_pdfforms'}) || ($changes{'uploadquota'}) || ($changes{'postsubmit'}) || ($changes{'coursecredits'}) || ($changes{'uselcmath'}) || ($changes{'usejsme'}) || - ($changes{'canclone'}) || ($changes{'mysqltables'}) || ($changes{'texengine'})) { - foreach my $item ('canuse_pdfforms','uselcmath','usejsme','texengine') { + ($changes{'canclone'}) || ($changes{'mysqltables'}) || ($changes{'texengine'}) || + ($changes{'inline_chem'})) { + foreach my $item ('canuse_pdfforms','uselcmath','usejsme','inline_chem','texengine') { if ($changes{$item}) { $domdefaults{$item}=$defaultshash{'coursedefaults'}{$item}; } @@ -15393,6 +20729,12 @@ sub modify_coursedefaults { } else { $resulttext .= '
    • '.&mt('Molecule editor uses JME (Java), if supported by client OS.').'
    • '; } + } elsif ($item eq 'inline_chem') { + if ($env{'form.'.$item} eq '1') { + $resulttext .= '
    • '.&mt('Chemical Reaction Response uses inline previewer').'
    • '; + } else { + $resulttext .= '
    • '.&mt('Chemical Reaction Response uses pop-up previewer').'
    • '; + } } elsif ($item eq 'texengine') { if ($defaultshash{'coursedefaults'}{'texengine'} ne '') { $resulttext .= '
    • '.&mt('Default method to display mathematics set to: "[_1]"', @@ -15744,6 +21086,344 @@ sub modify_selfenrollment { return $resulttext; } +sub modify_wafproxy { + my ($dom,$action,$lastactref,%domconfig) = @_; + my %servers = &Apache::lonnet::internet_dom_servers($dom); + my (%othercontrol,%canset,%values,%curralias,%currsaml,%currvalue,@warnings, + %wafproxy,%changes,%expirecache,%expiresaml); + foreach my $server (sort(keys(%servers))) { + my $serverhome = &Apache::lonnet::get_server_homeID($servers{$server}); + if ($serverhome eq $server) { + my $serverdom = &Apache::lonnet::host_domain($server); + if ($serverdom eq $dom) { + $canset{$server} = 1; + } + } + } + if (ref($domconfig{'wafproxy'}) eq 'HASH') { + %{$values{$dom}} = (); + if (ref($domconfig{'wafproxy'}{'alias'}) eq 'HASH') { + %curralias = %{$domconfig{'wafproxy'}{'alias'}}; + } + if (ref($domconfig{'wafproxy'}{'saml'}) eq 'HASH') { + %currsaml = %{$domconfig{'wafproxy'}{'saml'}}; + } + foreach my $item ('remoteip','ipheader','trusted','vpnint','vpnext','sslopt') { + $currvalue{$item} = $domconfig{'wafproxy'}{$item}; + } + } + my $output; + if (keys(%canset)) { + %{$wafproxy{'alias'}} = (); + %{$wafproxy{'saml'}} = (); + foreach my $key (sort(keys(%canset))) { + if ($env{'form.wafproxy_'.$dom}) { + $wafproxy{'alias'}{$key} = $env{'form.wafproxy_alias_'.$key}; + $wafproxy{'alias'}{$key} =~ s/^\s+|\s+$//g; + if ($wafproxy{'alias'}{$key} ne $curralias{$key}) { + $changes{'alias'} = 1; + } + if ($env{'form.wafproxy_alias_saml_'.$key}) { + $wafproxy{'saml'}{$key} = 1; + } + if ($wafproxy{'saml'}{$key} ne $currsaml{$key}) { + $changes{'saml'} = 1; + } + } else { + $wafproxy{'alias'}{$key} = ''; + $wafproxy{'saml'}{$key} = ''; + if ($curralias{$key}) { + $changes{'alias'} = 1; + } + if ($currsaml{$key}) { + $changes{'saml'} = 1; + } + } + if ($wafproxy{'alias'}{$key} eq '') { + if ($curralias{$key}) { + $expirecache{$key} = 1; + } + delete($wafproxy{'alias'}{$key}); + } + if ($wafproxy{'saml'}{$key} eq '') { + if ($currsaml{$key}) { + $expiresaml{$key} = 1; + } + delete($wafproxy{'saml'}{$key}); + } + } + unless (keys(%{$wafproxy{'alias'}})) { + delete($wafproxy{'alias'}); + } + unless (keys(%{$wafproxy{'saml'}})) { + delete($wafproxy{'saml'}); + } + # Localization for values in %warn occurs in &mt() calls separately. + my %warn = ( + trusted => 'trusted IP range(s)', + vpnint => 'internal IP range(s) for VPN sessions(s)', + vpnext => 'IP range(s) for backend WAF connections', + ); + foreach my $item ('remoteip','ipheader','trusted','vpnint','vpnext','sslopt') { + my $possible = $env{'form.wafproxy_'.$item}; + $possible =~ s/^\s+|\s+$//g; + if ($possible ne '') { + if ($item eq 'remoteip') { + if ($possible =~ /^[mhn]$/) { + $wafproxy{$item} = $possible; + } + } elsif ($item eq 'ipheader') { + if ($wafproxy{'remoteip'} eq 'h') { + $wafproxy{$item} = $possible; + } + } elsif ($item eq 'sslopt') { + if ($possible =~ /^0|1$/) { + $wafproxy{$item} = $possible; + } + } else { + my (@ok,$count); + if (($item eq 'vpnint') || ($item eq 'vpnext')) { + unless ($env{'form.wafproxy_vpnaccess'}) { + $possible = ''; + } + } elsif ($item eq 'trusted') { + unless ($wafproxy{'remoteip'} eq 'h') { + $possible = ''; + } + } + unless ($possible eq '') { + $possible =~ s/[\r\n]+/\s/g; + $possible =~ s/\s*-\s*/-/g; + $possible =~ s/\s+/,/g; + $possible =~ s/,+/,/g; + } + $count = 0; + if ($possible ne '') { + foreach my $poss (split(/\,/,$possible)) { + $count ++; + $poss = &validate_ip_pattern($poss); + if ($poss ne '') { + push(@ok,$poss); + } + } + my $diff = $count - scalar(@ok); + if ($diff) { + push(@warnings,'
    • '. + &mt('[quant,_1,IP] invalid and excluded from saved value for [_2]', + $diff,$warn{$item}). + '
    • '); + } + if (@ok) { + my @cidr_list; + foreach my $item (@ok) { + @cidr_list = &Net::CIDR::cidradd($item,@cidr_list); + } + $wafproxy{$item} = join(',',@cidr_list); + } + } + } + if ($wafproxy{$item} ne $currvalue{$item}) { + $changes{$item} = 1; + } + } elsif ($currvalue{$item}) { + $changes{$item} = 1; + } + } + } else { + if (keys(%curralias)) { + $changes{'alias'} = 1; + } + if (keys(%currsaml)) { + $changes{'saml'} = 1; + } + if (keys(%currvalue)) { + foreach my $key (keys(%currvalue)) { + $changes{$key} = 1; + } + } + } + if (keys(%changes)) { + my %defaultshash = ( + wafproxy => \%wafproxy, + ); + my $putresult = &Apache::lonnet::put_dom('configuration',\%defaultshash, + $dom); + if ($putresult eq 'ok') { + my $cachetime = 24*60*60; + my (%domdefaults,$updatedomdefs); + foreach my $item ('ipheader','trusted','vpnint','vpnext','sslopt') { + if ($changes{$item}) { + unless ($updatedomdefs) { + %domdefaults = &Apache::lonnet::get_domain_defaults($dom); + $updatedomdefs = 1; + } + if ($wafproxy{$item}) { + $domdefaults{'waf_'.$item} = $wafproxy{$item}; + } elsif (exists($domdefaults{'waf_'.$item})) { + delete($domdefaults{'waf_'.$item}); + } + } + } + if ($updatedomdefs) { + &Apache::lonnet::do_cache_new('domdefaults',$dom,\%domdefaults,$cachetime); + if (ref($lastactref) eq 'HASH') { + $lastactref->{'domdefaults'} = 1; + } + } + if ((exists($wafproxy{'alias'})) || (keys(%expirecache))) { + my %updates = %expirecache; + foreach my $key (keys(%expirecache)) { + &Apache::lonnet::devalidate_cache_new('proxyalias',$key); + } + if (ref($wafproxy{'alias'}) eq 'HASH') { + my $cachetime = 24*60*60; + foreach my $key (keys(%{$wafproxy{'alias'}})) { + $updates{$key} = 1; + &Apache::lonnet::do_cache_new('proxyalias',$key,$wafproxy{'alias'}{$key}, + $cachetime); + } + } + if (ref($lastactref) eq 'HASH') { + $lastactref->{'proxyalias'} = \%updates; + } + } + if ((exists($wafproxy{'saml'})) || (keys(%expiresaml))) { + my %samlupdates = %expiresaml; + foreach my $key (keys(%expiresaml)) { + &Apache::lonnet::devalidate_cache_new('proxysaml',$key); + } + if (ref($wafproxy{'saml'}) eq 'HASH') { + my $cachetime = 24*60*60; + foreach my $key (keys(%{$wafproxy{'saml'}})) { + $samlupdates{$key} = 1; + &Apache::lonnet::do_cache_new('proxysaml',$key,$wafproxy{'saml'}{$key}, + $cachetime); + } + } + if (ref($lastactref) eq 'HASH') { + $lastactref->{'proxysaml'} = \%samlupdates; + } + } + $output = &mt('Changes were made to Web Application Firewall/Reverse Proxy').'
        '; + foreach my $item ('alias','saml','remoteip','ipheader','trusted','vpnint','vpnext','sslopt') { + if ($changes{$item}) { + if ($item eq 'alias') { + my $numaliased = 0; + if (ref($wafproxy{'alias'}) eq 'HASH') { + my $shown; + if (keys(%{$wafproxy{'alias'}})) { + foreach my $server (sort(keys(%{$wafproxy{'alias'}}))) { + $shown .= '
      • '.&mt('[_1] aliased by [_2]', + &Apache::lonnet::hostname($server), + $wafproxy{'alias'}{$server}).'
      • '; + $numaliased ++; + } + if ($numaliased) { + $output .= '
      • '.&mt('Aliases for hostnames set to: [_1]', + '
          '.$shown.'
        ').'
      • '; + } + } + } + unless ($numaliased) { + $output .= '
      • '.&mt('Aliases deleted for hostnames').'
      • '; + } + } elsif ($item eq 'saml') { + my $shown; + if (ref($wafproxy{'saml'}) eq 'HASH') { + if (keys(%{$wafproxy{'saml'}})) { + $shown = join(', ',sort(keys(%{$wafproxy{'saml'}}))); + } + } + if ($shown) { + $output .= '
      • '.&mt('Alias used by SSO Auth for: [_1]', + $shown).'
      • '; + } else { + $output .= '
      • '.&mt('No alias used for SSO Auth').'
      • '; + } + } else { + if ($item eq 'remoteip') { + my %ip_methods = &remoteip_methods(); + if ($wafproxy{$item} =~ /^[mh]$/) { + $output .= '
      • '.&mt("Method for determining user's IP set to: [_1]", + $ip_methods{$wafproxy{$item}}).'
      • '; + } else { + if (($env{'form.wafproxy_'.$dom}) && (ref($wafproxy{'alias'}) eq 'HASH')) { + $output .= '
      • '.&mt("No method in use to get user's real IP (will report IP used by WAF)."). + '
      • '; + } else { + $output .= '
      • '.&mt('WAF/Reverse Proxy not in use').'
      • '; + } + } + } elsif ($item eq 'ipheader') { + if ($wafproxy{$item}) { + $output .= '
      • '.&mt('Request header with remote IP set to: [_1]', + $wafproxy{$item}).'
      • '; + } else { + $output .= '
      • '.&mt('Request header with remote IP deleted').'
      • '; + } + } elsif ($item eq 'trusted') { + if ($wafproxy{$item}) { + $output .= '
      • '.&mt('Trusted IP range(s) set to: [_1]', + $wafproxy{$item}).'
      • '; + } else { + $output .= '
      • '.&mt('Trusted IP range(s) deleted').'
      • '; + } + } elsif ($item eq 'vpnint') { + if ($wafproxy{$item}) { + $output .= '
      • '.&mt('Internal IP Range(s) for VPN sessions set to: [_1]', + $wafproxy{$item}).'
      • '; + } else { + $output .= '
      • '.&mt('Internal IP Range(s) for VPN sessions deleted').'
      • '; + } + } elsif ($item eq 'vpnext') { + if ($wafproxy{$item}) { + $output .= '
      • '.&mt('IP Range(s) for backend WAF connections set to: [_1]', + $wafproxy{$item}).'
      • '; + } else { + $output .= '
      • '.&mt('IP Range(s) for backend WAF connections deleted').'
      • '; + } + } elsif ($item eq 'sslopt') { + if ($wafproxy{$item}) { + $output .= '
      • '.&mt('WAF/Reverse Proxy expected to forward requests to https on LON-CAPA node, regardless of original protocol in web browser (http or https).').'
      • '; + } else { + $output .= '
      • '.&mt('WAF/Reverse Proxy expected to preserve original protocol in web browser (either http or https) when forwarding to LON-CAPA node.').'
      • '; + } + } + } + } + } + } else { + $output = ''. + &mt('An error occurred: [_1]',$putresult).''; + } + } elsif (keys(%canset)) { + $output = &mt('No changes made to Web Application Firewall/Reverse Proxy settings'); + } + if (@warnings) { + $output .= '
        '.&mt('Warnings:').'
          '. + join("\n",@warnings).'
        '; + } + return $output; +} + +sub validate_ip_pattern { + my ($pattern) = @_; + if ($pattern =~ /^([^-]+)\-([^-]+)$/) { + my ($start,$end) = ($1,$2); + if ((&Net::CIDR::cidrvalidate($start)) && (&Net::CIDR::cidrvalidate($end))) { + if (($start !~ m{/}) && ($end !~ m{/})) { + return $start.'-'.$end; + } + } + } elsif ($pattern ne '') { + $pattern = &Net::CIDR::cidrvalidate($pattern); + if ($pattern ne '') { + return $pattern; + } + } + return; +} + sub modify_usersessions { my ($dom,$lastactref,%domconfig) = @_; my @hostingtypes = ('version','excludedomain','includedomain'); @@ -15911,6 +21591,7 @@ sub modify_usersessions { } } $defaultshash{'usersessions'}{'offloadnow'} = {}; + $defaultshash{'usersessions'}{'offloadoth'} = {}; my @offloadnow = &Apache::loncommon::get_env_multiple('form.offloadnow'); my @okoffload; if (@offloadnow) { @@ -15927,6 +21608,22 @@ sub modify_usersessions { } } } + my @offloadoth = &Apache::loncommon::get_env_multiple('form.offloadoth'); + my @okoffloadoth; + if (@offloadoth) { + foreach my $server (@offloadoth) { + if (&Apache::lonnet::hostname($server) ne '') { + unless (grep(/^\Q$server\E$/,@okoffloadoth)) { + push(@okoffloadoth,$server); + } + } + } + if (@okoffloadoth) { + foreach my $lonhost (@okoffloadoth) { + $defaultshash{'usersessions'}{'offloadoth'}{$lonhost} = 1; + } + } + } if (ref($domconfig{'usersessions'}) eq 'HASH') { if (ref($domconfig{'usersessions'}{'spares'}) eq 'HASH') { if (ref($changes{'spares'}) eq 'HASH') { @@ -15937,26 +21634,38 @@ sub modify_usersessions { } else { $savespares = 1; } - if (ref($domconfig{'usersessions'}{'offloadnow'}) eq 'HASH') { - foreach my $lonhost (keys(%{$domconfig{'usersessions'}{'offloadnow'}})) { - unless ($defaultshash{'usersessions'}{'offloadnow'}{$lonhost}) { - $changes{'offloadnow'} = 1; - last; - } - } - unless ($changes{'offloadnow'}) { - foreach my $lonhost (keys(%{$defaultshash{'usersessions'}{'offloadnow'}})) { - unless ($domconfig{'usersessions'}{'offloadnow'}{$lonhost}) { - $changes{'offloadnow'} = 1; + foreach my $offload ('offloadnow','offloadoth') { + if (ref($domconfig{'usersessions'}{$offload}) eq 'HASH') { + foreach my $lonhost (keys(%{$domconfig{'usersessions'}{$offload}})) { + unless ($defaultshash{'usersessions'}{$offload}{$lonhost}) { + $changes{$offload} = 1; last; } } - } - } elsif (@okoffload) { + unless ($changes{$offload}) { + foreach my $lonhost (keys(%{$defaultshash{'usersessions'}{$offload}})) { + unless ($domconfig{'usersessions'}{$offload}{$lonhost}) { + $changes{$offload} = 1; + last; + } + } + } + } else { + if (($offload eq 'offloadnow') && (@okoffload)) { + $changes{'offloadnow'} = 1; + } + if (($offload eq 'offloadoth') && (@okoffloadoth)) { + $changes{'offloadoth'} = 1; + } + } + } + } else { + if (@okoffload) { $changes{'offloadnow'} = 1; } - } elsif (@okoffload) { - $changes{'offloadnow'} = 1; + if (@okoffloadoth) { + $changes{'offloadoth'} = 1; + } } my $nochgmsg = &mt('No changes made to settings for user session hosting/offloading.'); if ((keys(%changes) > 0) || ($savespares)) { @@ -15973,6 +21682,9 @@ sub modify_usersessions { if (ref($defaultshash{'usersessions'}{'offloadnow'}) eq 'HASH') { $domdefaults{'offloadnow'} = $defaultshash{'usersessions'}{'offloadnow'}; } + if (ref($defaultshash{'usersessions'}{'offloadoth'}) eq 'HASH') { + $domdefaults{'offloadoth'} = $defaultshash{'usersessions'}{'offloadoth'}; + } } my $cachetime = 24*60*60; &Apache::lonnet::do_cache_new('domdefaults',$dom,\%domdefaults,$cachetime); @@ -16013,14 +21725,18 @@ sub modify_usersessions { } else { foreach my $type (@{$types{$prefix}}) { if (defined($changes{$prefix}{$type})) { - my $newvalue; + my ($newvalue,$notinuse); if (ref($defaultshash{'usersessions'}) eq 'HASH') { if (ref($defaultshash{'usersessions'}{$prefix})) { if ($type eq 'version') { $newvalue = $defaultshash{'usersessions'}{$prefix}{$type}; - } elsif (ref($defaultshash{'usersessions'}{$prefix}{$type}) eq 'ARRAY') { - if (@{$defaultshash{'usersessions'}{$prefix}{$type}} > 0) { - $newvalue = join(', ',@{$defaultshash{'usersessions'}{$prefix}{$type}}); + } else { + if (ref($defaultshash{'usersessions'}{$prefix}{$type}) eq 'ARRAY') { + if (@{$defaultshash{'usersessions'}{$prefix}{$type}} > 0) { + $newvalue = join(', ',@{$defaultshash{'usersessions'}{$prefix}{$type}}); + } + } else { + $notinuse = 1; } } } @@ -16028,12 +21744,14 @@ sub modify_usersessions { if ($newvalue eq '') { if ($type eq 'version') { $resulttext .= '
      • '.&mt('[_1] set to: off',$lt{$type}).'
      • '; + } elsif ($notinuse) { + $resulttext .= '
      • '.&mt('[_1] set to: not in use',$lt{$type}).'
      • '; } else { $resulttext .= '
      • '.&mt('[_1] set to: none',$lt{$type}).'
      • '; } } else { if ($type eq 'version') { - $newvalue .= ' '.&mt('(or later)'); + $newvalue .= ' '.&mt('(or later)'); } $resulttext .= '
      • '.&mt('[_1] set to: [_2].',$lt{$type},$newvalue).'
      • '; } @@ -16046,16 +21764,31 @@ sub modify_usersessions { if ($changes{'offloadnow'}) { if (ref($defaultshash{'usersessions'}{'offloadnow'}) eq 'HASH') { if (keys(%{$defaultshash{'usersessions'}{'offloadnow'}}) > 0) { - $resulttext .= '
      • '.&mt('Switch active users on next access, for server(s):').'
          '; + $resulttext .= '
        • '.&mt('Switch any active user on next access, for server(s):').'
            '; foreach my $lonhost (sort(keys(%{$defaultshash{'usersessions'}{'offloadnow'}}))) { $resulttext .= '
          • '.$lonhost.'
          • '; } $resulttext .= '
          '; } else { - $resulttext .= '
        • '.&mt('No servers now set to switch active users on next access.'); + $resulttext .= '
        • '.&mt('No servers now set to switch any active user on next access.'); } } else { - $resulttext .= '
        • '.&mt('No servers now set to switch active users on next access.').'
        • '; + $resulttext .= '
        • '.&mt('No servers now set to switch any active user on next access.').'
        • '; + } + } + if ($changes{'offloadoth'}) { + if (ref($defaultshash{'usersessions'}{'offloadoth'}) eq 'HASH') { + if (keys(%{$defaultshash{'usersessions'}{'offloadoth'}}) > 0) { + $resulttext .= '
        • '.&mt('Switch other institutions on next access, for server(s):').'
            '; + foreach my $lonhost (sort(keys(%{$defaultshash{'usersessions'}{'offloadoth'}}))) { + $resulttext .= '
          • '.$lonhost.'
          • '; + } + $resulttext .= '
          '; + } else { + $resulttext .= '
        • '.&mt('No servers now set to switch other institutions on next access.'); + } + } else { + $resulttext .= '
        • '.&mt('No servers now set to switch other institutions on next access.').'
        • '; } } $resulttext .= '
        '; @@ -16101,12 +21834,17 @@ sub modify_ssl { if ($env{'form.'.$prefix.'_'.$type} =~ /^(no|req)$/) { $value = $env{'form.'.$prefix.'_'.$type}; } - if (ref($domconfig{$action}{$prefix}) eq 'HASH') { - if ($domconfig{$action}{$prefix}{$type} ne '') { - if ($value ne $domconfig{$action}{$prefix}{$type}) { + if (ref($domconfig{$action}) eq 'HASH') { + if (ref($domconfig{$action}{$prefix}) eq 'HASH') { + if ($domconfig{$action}{$prefix}{$type} ne '') { + if ($value ne $domconfig{$action}{$prefix}{$type}) { + $changes{$prefix}{$type} = 1; + } + $defaultshash{$action}{$prefix}{$type} = $value; + } else { + $defaultshash{$action}{$prefix}{$type} = $value; $changes{$prefix}{$type} = 1; } - $defaultshash{$action}{$prefix}{$type} = $value; } else { $defaultshash{$action}{$prefix}{$type} = $value; $changes{$prefix}{$type} = 1; @@ -16176,6 +21914,17 @@ sub modify_ssl { } } } + if (keys(%changes)) { + foreach my $prefix (keys(%changes)) { + if (ref($changes{$prefix}) eq 'HASH') { + if (scalar(keys(%{$changes{$prefix}})) == 0) { + delete($changes{$prefix}); + } + } else { + delete($changes{$prefix}); + } + } + } my $nochgmsg = &mt('No changes made to LON-CAPA SSL settings'); if (keys(%changes) > 0) { my $putresult = &Apache::lonnet::put_dom('configuration',\%defaultshash, @@ -16186,10 +21935,10 @@ sub modify_ssl { $domdefaults{'replication'} = $defaultshash{$action}{'replication'}; } if (ref($defaultshash{$action}{'connto'}) eq 'HASH') { - $domdefaults{'connto'} = $domconfig{$action}{'connto'}; + $domdefaults{'connto'} = $defaultshash{$action}{'connto'}; } if (ref($defaultshash{$action}{'connfrom'}) eq 'HASH') { - $domdefaults{'connfrom'} = $domconfig{$action}{'connfrom'}; + $domdefaults{'connfrom'} = $defaultshash{$action}{'connfrom'}; } } my $cachetime = 24*60*60; @@ -16205,18 +21954,24 @@ sub modify_ssl { $resulttext .= '
      • '.$titles{$prefix}.'
          '; foreach my $type (@{$types{$prefix}}) { if (defined($changes{$prefix}{$type})) { - my $newvalue; + my ($newvalue,$notinuse); if (ref($defaultshash{$action}) eq 'HASH') { if (ref($defaultshash{$action}{$prefix})) { if (($prefix eq 'connto') || ($prefix eq 'connfrom')) { $newvalue = $titles{$defaultshash{$action}{$prefix}{$type}}; - } elsif (ref($defaultshash{$action}{$prefix}{$type}) eq 'ARRAY') { - if (@{$defaultshash{$action}{$prefix}{$type}} > 0) { - $newvalue = join(', ',@{$defaultshash{$action}{$prefix}{$type}}); + } else { + if (ref($defaultshash{$action}{$prefix}{$type}) eq 'ARRAY') { + if (@{$defaultshash{$action}{$prefix}{$type}} > 0) { + $newvalue = join(', ',@{$defaultshash{$action}{$prefix}{$type}}); + } + } else { + $notinuse = 1; } } } - if ($newvalue eq '') { + if ($notinuse) { + $resulttext .= '
        • '.&mt('[_1] set to: not in use',$titles{$type}).'
        • '; + } elsif ($newvalue eq '') { $resulttext .= '
        • '.&mt('[_1] set to: none',$titles{$type}).'
        • '; } else { $resulttext .= '
        • '.&mt('[_1] set to: [_2].',$titles{$type},$newvalue).'
        • '; @@ -16330,17 +22085,21 @@ sub modify_trust { $resulttext .= '
        • '.$lt{$prefix}.'
            '; foreach my $type (@types) { if (defined($changes{$prefix}{$type})) { - my $newvalue; + my ($newvalue,$notinuse); if (ref($defaultshash{'trust'}) eq 'HASH') { if (ref($defaultshash{'trust'}{$prefix})) { if (ref($defaultshash{'trust'}{$prefix}{$type}) eq 'ARRAY') { if (@{$defaultshash{'trust'}{$prefix}{$type}} > 0) { $newvalue = join(', ',@{$defaultshash{'trust'}{$prefix}{$type}}); } + } else { + $notinuse = 1; } } } - if ($newvalue eq '') { + if ($notinuse) { + $resulttext .= '
          • '.&mt('[_1] set to: not in use',$lt{$type}).'
          • '; + } elsif ($newvalue eq '') { $resulttext .= '
          • '.&mt('[_1] set to: none',$lt{$type}).'
          • '; } else { $resulttext .= '
          • '.&mt('[_1] set to: [_2].',$lt{$type},$newvalue).'
          • '; @@ -16375,12 +22134,12 @@ sub modify_loadbalancing { my @sparestypes = ('primary','default'); my %typetitles = &sparestype_titles(); my $resulttext; - my (%currbalancer,%currtargets,%currrules,%existing); + my (%currbalancer,%currtargets,%currrules,%existing,%currcookies); if (ref($domconfig{'loadbalancing'}) eq 'HASH') { %existing = %{$domconfig{'loadbalancing'}}; } &get_loadbalancers_config(\%servers,\%existing,\%currbalancer, - \%currtargets,\%currrules); + \%currtargets,\%currrules,\%currcookies); my ($saveloadbalancing,%defaultshash,%changes); my ($alltypes,$othertypes,$titles) = &loadbalancing_titles($dom,$intdom,$usertypes,$types); @@ -16432,6 +22191,18 @@ sub modify_loadbalancing { } $defaultshash{'loadbalancing'}{$balancer}{'targets'}{$sparetype} = \@offloadto; } + if ($env{'form.loadbalancing_cookie_'.$i}) { + $defaultshash{'loadbalancing'}{$balancer}{'cookie'} = 1; + if (exists($currbalancer{$balancer})) { + unless ($currcookies{$balancer}) { + $changes{'curr'}{$balancer}{'cookie'} = 1; + } + } + } elsif (exists($currbalancer{$balancer})) { + if ($currcookies{$balancer}) { + $changes{'curr'}{$balancer}{'cookie'} = 1; + } + } if (ref($currtargets{$balancer}) eq 'HASH') { foreach my $sparetype (@sparestypes) { if (ref($currtargets{$balancer}{$sparetype}) eq 'ARRAY') { @@ -16585,24 +22356,33 @@ sub modify_loadbalancing { } } } - if (keys(%toupdate)) { - my %thismachine; - my $updatedhere; - my $cachetime = 60*60*24; - map { $thismachine{$_} = 1; } &Apache::lonnet::current_machine_ids(); - foreach my $lonhost (keys(%toupdate)) { - if ($thismachine{$lonhost}) { - unless ($updatedhere) { - &Apache::lonnet::do_cache_new('loadbalancing',$dom, - $defaultshash{'loadbalancing'}, - $cachetime); - $updatedhere = 1; - } - } else { - my $cachekey = &escape('loadbalancing').':'.&escape($dom); - &Apache::lonnet::remote_devalidate_cache($lonhost,[$cachekey]); - } + if ($changes{'curr'}{$balancer}{'cookie'}) { + if ($currcookies{$balancer}) { + $resulttext .= '
          • '.&mt('Load Balancer: [_1] -- cookie use disabled', + $balancer).'
          • '; + } else { + $resulttext .= '
          • '.&mt('Load Balancer: [_1] -- cookie use enabled', + $balancer).'
          • '; + } + } + } + } + if (keys(%toupdate)) { + my %thismachine; + my $updatedhere; + my $cachetime = 60*60*24; + map { $thismachine{$_} = 1; } &Apache::lonnet::current_machine_ids(); + foreach my $lonhost (keys(%toupdate)) { + if ($thismachine{$lonhost}) { + unless ($updatedhere) { + &Apache::lonnet::do_cache_new('loadbalancing',$dom, + $defaultshash{'loadbalancing'}, + $cachetime); + $updatedhere = 1; } + } else { + my $cachekey = &escape('loadbalancing').':'.&escape($dom); + &Apache::lonnet::remote_devalidate_cache($lonhost,[$cachekey]); } } } @@ -16814,12 +22594,12 @@ sub lonbalance_targets_js { } push(@alltypes,'default','_LC_adv','_LC_author','_LC_internetdom','_LC_external'); $allinsttypes = join("','",@alltypes); - my (%currbalancer,%currtargets,%currrules,%existing); + my (%currbalancer,%currtargets,%currrules,%existing,%currcookies); if (ref($settings) eq 'HASH') { %existing = %{$settings}; } &get_loadbalancers_config($servers,\%existing,\%currbalancer, - \%currtargets,\%currrules); + \%currtargets,\%currrules,\%currcookies); my $balancers = join("','",sort(keys(%currbalancer))); return <<"END"; @@ -17099,6 +22879,7 @@ function balancerChange(balnum,baltotal, END } + sub new_spares_js { my @sparestypes = ('primary','default'); my $types = join("','",@sparestypes); @@ -17172,7 +22953,7 @@ function updateNewSpares(formname,lonhos function checkNewSpares(lonhost,type) { var newSpare = document.getElementById('newspare_'+type+'_'+lonhost); var chosen = newSpare.options[newSpare.selectedIndex].value; - if (chosen != '') { + if (chosen != '') { var othertype; var othernewSpare; if (type == 'primary') { @@ -17306,7 +23087,7 @@ function toggleDisplay(domForm,caller) { var dispval = 'block'; var selfcreateRegExp = /^cancreate_emailverified/; if (caller == 'emailoptions') { - optionsElement = domForm.cancreate_email; + optionsElement = domForm.cancreate_email; } if (caller == 'studentsubmission') { optionsElement = domForm.postsubmit; @@ -17361,16 +23142,49 @@ sub devalidate_remote_domconfs { my %servers = &Apache::lonnet::internet_dom_servers($dom); my %thismachine; map { $thismachine{$_} = 1; } &Apache::lonnet::current_machine_ids(); - my @posscached = ('domainconfig','domdefaults','ltitools','usersessions','directorysrch'); + my @posscached = ('domainconfig','domdefaults','ltitools','usersessions', + 'directorysrch','passwdconf','cats','proxyalias','proxysaml', + 'ipaccess'); + my %cache_by_lonhost; + if (exists($cachekeys->{'samllanding'})) { + if (ref($cachekeys->{'samllanding'}) eq 'HASH') { + my %landing = %{$cachekeys->{'samllanding'}}; + my %domservers = &Apache::lonnet::get_servers($dom); + if (keys(%domservers)) { + foreach my $server (keys(%domservers)) { + my @cached; + next if ($thismachine{$server}); + if ($landing{$server}) { + push(@cached,&escape('samllanding').':'.&escape($server)); + } + if (@cached) { + $cache_by_lonhost{$server} = \@cached; + } + } + } + } + } if (keys(%servers)) { foreach my $server (keys(%servers)) { next if ($thismachine{$server}); my @cached; foreach my $name (@posscached) { if ($cachekeys->{$name}) { - push(@cached,&escape($name).':'.&escape($dom)); + if (($name eq 'proxyalias') || ($name eq 'proxysaml')) { + if (ref($cachekeys->{$name}) eq 'HASH') { + foreach my $key (keys(%{$cachekeys->{$name}})) { + push(@cached,&escape($name).':'.&escape($key)); + } + } + } else { + push(@cached,&escape($name).':'.&escape($dom)); + } } } + if ((exists($cache_by_lonhost{$server})) && + (ref($cache_by_lonhost{$server}) eq 'ARRAY')) { + push(@cached,@{$cache_by_lonhost{$server}}); + } if (@cached) { &Apache::lonnet::remote_devalidate_cache($server,\@cached); }
    '. ''; + my $prefix = 'canmodify'; if ($role eq 'emailusername') { unless ($checks{$fields[$i]} =~ /^(required|optional)$/) { $checks{$fields[$i]} = 'omit'; @@ -8552,13 +11642,18 @@ sub modifiable_userdata_row { $checked='checked="checked" '; } $output .= ''.(' ' x2); } $output .= ''.$fieldtitles{$fields[$i]}.''; } else { + if ($context eq 'lti') { + $prefix = 'lti'; + } elsif ($context eq 'privacy') { + $prefix = 'privacy'; + } $output .= ''; } @@ -8660,7 +11755,7 @@ sub insttypes_row { } $output .= ' '; } else { - if (($rem == 0) && (@{$types} > 0)) { + if ($rem == 0) { $output .= '