--- loncom/interface/domainprefs.pm 2020/03/15 23:04:15 1.371 +++ loncom/interface/domainprefs.pm 2020/09/17 00:35:04 1.372 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # Handler to set domain-wide configuration settings # -# $Id: domainprefs.pm,v 1.371 2020/03/15 23:04:15 raeburn Exp $ +# $Id: domainprefs.pm,v 1.372 2020/09/17 00:35:04 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -219,9 +219,10 @@ sub handler { 'serverstatuses','requestcourses','helpsettings', 'coursedefaults','usersessions','loadbalancing', 'requestauthor','selfenrollment','inststatus', - 'ltitools','ssl','trust','lti','privacy','passwords'],$dom); + 'ltitools','ssl','trust','lti','privacy','passwords', + 'proctoring'],$dom); my %encconfig = - &Apache::lonnet::get_dom('encconfig',['ltitools','lti'],$dom); + &Apache::lonnet::get_dom('encconfig',['ltitools','lti','proctoring'],$dom); if (ref($domconfig{'ltitools'}) eq 'HASH') { if (ref($encconfig{'ltitools'}) eq 'HASH') { foreach my $id (keys(%{$domconfig{'ltitools'}})) { @@ -246,12 +247,25 @@ sub handler { } } } + 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','defaults','passwords','quotas','autoenroll', 'autoupdate','autocreate','directorysrch','contacts','privacy', 'usercreation','selfcreation','usermodification','scantron', 'requestcourses','requestauthor','coursecategories', 'serverstatuses','helpsettings','coursedefaults', - 'ltitools','selfenrollment','usersessions','ssl','trust','lti'); + 'ltitools','proctoring','selfenrollment','usersessions','ssl', + 'trust','lti'); my %existing; if (ref($domconfig{'loadbalancing'}) eq 'HASH') { %existing = %{$domconfig{'loadbalancing'}}; @@ -541,6 +555,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', @@ -771,6 +793,8 @@ 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') { @@ -815,6 +839,8 @@ sub print_config_box { $output .= <itools_javascript($settings); } elsif ($action eq 'lti') { $output .= <i_javascript($settings); + } elsif ($action eq 'proctoring') { + $output .= &proctoring_javascript($settings); } $output .= ' @@ -1162,7 +1188,8 @@ 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')) { $output .= $item->{'print'}->($dom,$settings,\$rowtotal); } } @@ -2803,6 +2830,102 @@ function toggleLTITools(form,setting,ite 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(); @@ -4620,7 +4743,7 @@ sub print_ltitools { } $datatable .= ''.(' ' x2)."\n"; + $lt{'crs'.$item}.'  '."\n"; } $datatable .= ''. '
'.&mt('Custom items sent on launch').''. @@ -4822,6 +4945,640 @@ sub ltitools_names { return %lt; } +sub print_proctoring { + my ($dom,$settings,$rowtotal) = @_; + my $itemcount = 1; + my (%ordered,%providernames,%current,%currentdef); + my $confname = $dom.'-domainconfig'; + my $switchserver = &check_switchserver($dom,$confname); + if (ref($settings) eq 'HASH') { + foreach my $item (keys(%{$settings})) { + if (ref($settings->{$item}) eq 'HASH') { + my $num = $settings->{$item}{'order'}; + $ordered{$num} = $item; + } + } + } else { + %ordered = ( + 1 => 'proctorio', + 2 => 'examity', + ); + } + %providernames = &proctoring_providernames(); + my $maxnum = scalar(keys(%ordered)); + 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}; + } + my $datatable; + my $css_class; + 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 $provider = $ordered{$items[$i]}; + my $optionsty = 'none'; + my ($available,$version,$lifetime,$imgsrc,$userincdom,$showroles, + %checkedfields,%rolemaps,%inuse,%crsconfig,%current); + if (ref($settings) eq 'HASH') { + if (ref($settings->{$provider}) eq 'HASH') { + %current = %{$settings->{$provider}}; + if ($current{'available'}) { + $optionsty = 'block'; + $available = 1; + } + if ($current{'lifetime'} =~ /^\d+$/) { + $lifetime = $current{'lifetime'}; + } + if ($current{'version'} =~ /^\d+\.\d+$/) { + $version = $current{'version'}; + } + if ($current{'image'} ne '') { + $imgsrc = ''.&mt('Proctoring service icon').''; + } + if (ref($current{'fields'}) eq 'ARRAY') { + map { $checkedfields{$_} = 1; } @{$current{'fields'}}; + } + $userincdom = $current{'incdom'}; + if (ref($current{'roles'}) eq 'HASH') { + %rolemaps = %{$current{'roles'}}; + $checkedfields{'roles'} = 1; + } + if (ref($current{'defaults'}) eq 'ARRAY') { + foreach my $val (@{$current{'defaults'}}) { + if (grep(/^\Q$val\E$/,@{$defaults{$provider}})) { + $inuse{$val} = 1; + } else { + foreach my $poss (keys(%{$extended{$provider}})) { + if (ref($extended{$provider}{$poss}) eq 'ARRAY') { + if (grep(/^\Q$val\E$/,@{$extended{$provider}{$poss}})) { + $inuse{$poss} = $val; + last; + } + } + } + } + } + } elsif (ref($current{'defaults'}) eq 'HASH') { + foreach my $key (keys(%{$current{'defaults'}})) { + my $currval = $current{'defaults'}{$key}; + if (grep(/^\Q$key\E$/,@{$defaults{$provider}})) { + $inuse{$key} = 1; + } else { + my $match; + foreach my $poss (keys(%{$extended{$provider}})) { + if (ref($extended{$provider}{$poss}) eq 'ARRAY') { + if (grep(/^\Q$key\E$/,@{$extended{$provider}{$poss}})) { + $inuse{$poss} = $key; + last; + } + } elsif (ref($extended{$provider}{$poss}) eq 'HASH') { + foreach my $inner (sort(keys(%{$extended{$provider}{$poss}}))) { + if (ref($extended{$provider}{$poss}{$inner}) eq 'ARRAY') { + if (grep(/^\Q$currval\E$/,@{$extended{$provider}{$poss}{$inner}})) { + $currentdef{$inner} = $currval; + $match = 1; + last; + } + } elsif ($inner eq $key) { + $currentdef{$key} = $currval; + $match = 1; + last; + } + } + } + last if ($match); + } + } + } + } + if (ref($current{'crsconf'}) eq 'ARRAY') { + map { $crsconfig{$_} = 1; } @{$current{'crsconf'}}; + } + } + } + my %lt = &proctoring_titles($provider); + my %fieldtitles = &proctoring_fieldtitles($provider); + my $onclickavailable = ' onclick="toggleProctoring(this.form,'."'$provider'".');"'; + my %checkedavailable = ( + yes => '', + no => ' checked="checked"', + ); + if ($available) { + $checkedavailable{'yes'} = $checkedavailable{'no'}; + $checkedavailable{'no'} = ''; + } + my $chgstr = ' onchange="javascript:reorderProctoring(this.form,'."'proctoring_pos_".$provider."'".');"'; + $datatable .= '
'. + ''."\n"; + } + $datatable .= ''; + } + $itemcount ++; + } + } + return $datatable; +} + +sub proctoring_data { + my $requserfields = { + proctorio => ['user'], + examity => ['roles','user'], + }; + my $optuserfields = { + proctorio => ['fullname'], + examity => ['fullname','firstname','lastname','email'], + }; + my $defaults = { + proctorio => ['recordvideo','recordaudio','recordscreen','recordwebtraffic', + 'recordroomstart','verifyvideo','verifyaudio','verifydesktop', + 'verifyid','verifysignature','fullscreen','clipboard','tabslinks', + 'closetabs','onescreen','print','downloads','cache','rightclick', + 'reentry','calculator','whiteboard'], + examity => ['display'], + }; + my $extended = { + proctorio => { + verifyid => ['verifyidauto','verifyidlive'], + fullscreen => ['fullscreenlenient','fullscreenmoderate','fullscreensever'], + tabslinks => ['notabs','linksonly'], + reentry => ['noreentry','agentreentry'], + calculator => ['calculatorbasic','calculatorsci'], + }, + examity => { + display => { + target => ['iframe','tab','window'], + width => '', + height => '', + linktext => '', + explanation => '', + }, + }, + }; + my $crsconf = { + proctorio => ['recordvideo','recordaudio','recordscreen','recordwebtraffic', + 'recordroomstart','verifyvideo','verifyaudio','verifydesktop', + 'verifyid','verifysignature','fullscreen','clipboard','tabslinks', + 'closetabs','onescreen','print','downloads','cache','rightclick', + 'reentry','calculator','whiteboard'], + examity => ['label','title','target','linktext','explanation','append'], + }; + my $courseroles = ['cc','in','ta','ep','st']; + my $ltiroles = ['Instructor','ContentDeveloper','TeachingAssistant','Learner']; + return ($requserfields,$optuserfields,$defaults,$extended,$crsconf,$courseroles,$ltiroles); +} + +sub proctoring_titles { + my ($item) = @_; + my (%common_lt,%custom_lt); + %common_lt = &Apache::lonlocal::texthash ( + 'avai' => 'Available?', + 'base' => 'Basic Settings', + 'requ' => 'User data required to be sent on launch', + 'optu' => 'User data optionally sent on launch', + 'udsl' => 'User data sent on launch', + 'defa' => 'Defaults for items configurable in course', + 'sigmethod' => 'Signature Method', + 'key' => 'Key', + 'lifetime' => 'Nonce lifetime (s)', + 'secret' => 'Secret', + 'icon' => 'Icon', + 'fullname' => 'Full Name', + 'visible' => 'Visible input', + 'username' => 'username', + 'user' => 'User', + ); + if ($item eq 'proctorio') { + %custom_lt = &Apache::lonlocal::texthash ( + 'version' => 'OAuth version', + 'url' => 'API URL', + 'uname:dom' => 'username-domain', + ); + } elsif ($item eq 'examity') { + %custom_lt = &Apache::lonlocal::texthash ( + 'version' => 'LTI Version', + 'url' => 'URL', + 'uname:dom' => 'username:domain', + 'msgtype' => 'Message Type', + 'firstname' => 'First Name', + 'lastname' => 'Last Name', + 'email' => 'E-mail', + 'roles' => 'Role', + 'crstarget' => 'Display target', + 'crslabel' => 'Course label', + 'crstitle' => 'Course title', + 'crslinktext' => 'Link Text', + 'crsexplanation' => 'Explanation', + 'crsappend' => 'Provider URL', + ); + } + my %lt = (%common_lt,%custom_lt); + return %lt; +} + +sub proctoring_fieldtitles { + my ($item) = @_; + if ($item eq 'proctorio') { + return &Apache::lonlocal::texthash ( + 'recordvideo' => 'Record video', + 'recordaudio' => 'Record audio', + 'recordscreen' => 'Record screen', + 'recordwebtraffic' => 'Record web traffic', + 'recordroomstart' => 'Record room scan', + 'verifyvideo' => 'Verify webcam', + 'verifyaudio' => 'Verify microphone', + 'verifydesktop' => 'Verify desktop recording', + 'verifyid' => 'Photo ID verification', + 'verifysignature' => 'Require signature', + 'fullscreen' => 'Fullscreen', + 'clipboard' => 'Disable copy/paste', + 'tabslinks' => 'New tabs/windows', + 'closetabs' => 'Close other tabs', + 'onescreen' => 'Limit to single screen', + 'print' => 'Disable Printing', + 'downloads' => 'Disable Downloads', + 'cache' => 'Empty cache after exam', + 'rightclick' => 'Disable right click', + 'reentry' => 'Re-entry to exam', + 'calculator' => 'Onscreen calculator', + 'whiteboard' => 'Onscreen whiteboard', + 'verifyidauto' => 'Automated verification', + 'verifyidlive' => 'Live agent verification', + 'fullscreenlenient' => 'Forced, but can navigate away for up to 30s', + 'fullscreenmoderate' => 'Forced, but can navigate away for up to 15s', + 'fullscreensever' => 'Forced, navigation away ends exam', + 'notabs' => 'Disaallowed', + 'linksonly' => 'Allowed from links in exam', + 'noreentry' => 'Disallowed', + 'agentreentry' => 'Agent required for re-entry', + 'calculatorbasic' => 'Basic', + 'calculatorsci' => 'Scientific', + ); + } elsif ($item eq 'examity') { + return &Apache::lonlocal::texthash ( + 'target' => 'Display target', + 'window' => 'Window', + 'tab' => 'Tab', + 'iframe' => 'iFrame', + 'height' => 'Height (pixels)', + 'width' => 'Width (pixels)', + 'linktext' => 'Default Link Text', + 'explanation' => 'Default Explanation', + 'append' => 'Provider URL', + ); + } +} + +sub proctoring_providernames { + return ( + proctorio => 'Proctorio', + examity => 'Examity', + ); +} + sub print_lti { my ($dom,$settings,$rowtotal) = @_; my $itemcount = 1; @@ -5137,7 +5894,7 @@ sub lti_options { $output .= ''. '
'. '
'. + 'value="'.$userfield.'" />'. '
'.&mt('Mapping course roles').'
' + .''.(' 'x2).''.$providernames{$provider}.'
'. + ''.$lt{'avai'}.' '. + ' '."\n". + ''."\n". + '
'. + '
'.$lt{'base'}.''. + ''.$lt{'version'}.': '."\n". + (' 'x2). + ''.$lt{'sigmethod'}.':'. + (' 'x2). + ''.$lt{'lifetime'}.': '."\n". + '
'. + ''.$lt{'url'}.': '."\n". + '
'. + ''.$lt{'key'}.': '."\n". + (' 'x2). + ''.$lt{'secret'}.':'. + '
'."\n"; + $datatable .= ''.$lt{'icon'}.': '; + if ($imgsrc) { + $datatable .= $imgsrc. + ' '. + ' '.&mt('Replace:'); + } + $datatable .= ' '; + if ($switchserver) { + $datatable .= &mt('Upload to library server: [_1]',$switchserver); + } else { + $datatable .= ''; + } + unless ($imgsrc) { + $datatable .= '
('.&mt('if larger than 21x21 pixels, image will be scaled').')'; + } + $datatable .= '
'."\n"; + if (ref($requserfields{$provider}) eq 'ARRAY') { + if (@{$requserfields{$provider}} > 0) { + $datatable .= '
'.$lt{'requ'}.''; + foreach my $field (@{$requserfields{$provider}}) { + $datatable .= ''. + ''; + if ($field eq 'user') { + my $seluserdom = ''; + my $unseluserdom = ' selected="selected"'; + if ($userincdom) { + $seluserdom = $unseluserdom; + $unseluserdom = ''; + } + $datatable .= ': '. + ' '; + } else { + $datatable .= ' '; + if ($field eq 'roles') { + $showroles = 1; + } + } + $datatable .= ' '; + } + } + $datatable .= '
'."\n"; + } + if (ref($optuserfields{$provider}) eq 'ARRAY') { + if (@{$optuserfields{$provider}} > 0) { + $datatable .= '
'.$lt{'optu'}.''; + foreach my $field (@{$optuserfields{$provider}}) { + my $checked; + if ($checkedfields{$field}) { + $checked = ' checked="checked"'; + } + $datatable .= ''. + '  '; + } + $datatable .= '
'."\n"; + } + } + if (ref($defaults{$provider}) eq 'ARRAY') { + if (@{$defaults{$provider}}) { + my (%options,@selectboxes); + if (ref($extended{$provider}) eq 'HASH') { + %options = %{$extended{$provider}}; + } + $datatable .= '
'.$lt{'defa'}.''; + my ($rem,$numinrow,$dropdowns); + if ($provider eq 'proctorio') { + $datatable .= ''; + $numinrow = 4; + } + my $i = 0; + foreach my $field (@{$defaults{$provider}}) { + my $checked; + if ($inuse{$field}) { + $checked = ' checked="checked"'; + } + if ($provider eq 'examity') { + if ($field eq 'display') { + $datatable .= ''.&mt('Display target:'); + foreach my $option ('iframe','tab','window') { + my $checkdisp; + if ($currentdef{'target'} eq $option) { + $checkdisp = ' checked="checked"'; + } + $datatable .= ''.(' 'x2); + } + $datatable .= (' 'x4); + foreach my $dimen ('width','height') { + $datatable .= ''. + (' 'x2); + } + $datatable .= '
'. + '
'.$fieldtitles{'linktext'}.'
'. + '
'. + '
'.$fieldtitles{'explanation'}.'
'. + '

'; + } + } else { + if ((exists($options{$field})) && (ref($options{$field}) eq 'ARRAY')) { + my ($output,$selnone); + unless ($checked) { + $selnone = ' selected="selected"'; + } + $output .= ''.$fieldtitles{$field}.': '. + ''; + push(@selectboxes,$output); + } else { + $rem = $i%($numinrow); + if ($rem == 0) { + if ($i > 0) { + $datatable .= ''; + } + $datatable .= ''; + } + $datatable .= ''; + $i++; + } + } + } + if ($provider eq 'proctorio') { + if ($numinrow) { + $rem = $i%$numinrow; + } + my $colsleft = $numinrow - $rem; + if ($colsleft > 1) { + $datatable .= '
'. + ''. + ''; + } else { + $datatable .= ''; + } + $datatable .= ' '. + '
'; + if (@selectboxes) { + $datatable .= '
'; + $numinrow = 2; + for (my $i=0; $i<@selectboxes; $i++) { + $rem = $i%($numinrow); + if ($rem == 0) { + if ($i > 0) { + $datatable .= ''; + } + $datatable .= ''; + } + $datatable .= ''; + } + if ($numinrow) { + $rem = $i%$numinrow; + } + $colsleft = $numinrow - $rem; + if ($colsleft > 1) { + $datatable .= '
'. + $selectboxes[$i].''; + } else { + $datatable .= ''; + } + $datatable .= ' '. + '
'; + } + } + $datatable .= '
'; + } + if (ref($crsconf{$provider}) eq 'ARRAY') { + $datatable .= '
'. + ''.&mt('Configurable in course').''; + my ($rem,$numinrow); + if ($provider eq 'proctorio') { + $datatable .= ''; + $numinrow = 4; + } + my $i = 0; + foreach my $item (@{$crsconf{$provider}}) { + my $name; + if ($provider eq 'examity') { + $name = $lt{'crs'.$item}; + } elsif ($provider eq 'proctorio') { + $name = $fieldtitles{$item}; + $rem = $i%($numinrow); + if ($rem == 0) { + if ($i > 0) { + $datatable .= ''; + } + $datatable .= ''; + } + $datatable .= '
'. + $name.''; + if ($provider eq 'examity') { + $datatable .= '  '; + } + $datatable .= "\n"; + $i++; + } + if ($provider eq 'proctorio') { + if ($numinrow) { + $rem = $i%$numinrow; + } + my $colsleft = $numinrow - $rem; + if ($colsleft > 1) { + $datatable .= ''; + } else { + $datatable .= ''; + } + $datatable .= ' '. + '
'; + } + $datatable .= '
'; + } + if ($showroles) { + $datatable .= '
'. + ''.&mt('Role mapping').''; + foreach my $role (@courseroles) { + my ($selected,$selectnone); + if (!$rolemaps{$role}) { + $selectnone = ' selected="selected"'; + } + $datatable .= ''; + } + $datatable .= '
'. + &Apache::lonnet::plaintext($role,'Course').'
'. + '
'. + '
'. + ''.&mt('Custom items sent on launch').''. + ''. + ''. + ''; + if ((ref($settings) eq 'HASH') && (ref($settings->{$provider}) eq 'HASH') && + (ref($settings->{$provider}->{'custom'}) eq 'HASH')) { + my %custom = %{$settings->{$provider}->{'custom'}}; + if (keys(%custom) > 0) { + foreach my $key (sort(keys(%custom))) { + next if ($key eq 'lms'); + $datatable .= ''. + ''; + } + } + } + $datatable .= ''. + '
'.&mt('Action').''.&mt('Name').''.&mt('Value').'
lms
'. + ''.$key.'
'. + ''. + '
'; foreach my $ltirole (@lticourseroles) { my ($selected,$selectnone); @@ -12427,7 +13184,7 @@ sub modify_ltitools { } } if (!$numconfig) { - $resulttext .= &mt('None'); + $resulttext .= ' '.&mt('None'); } $resulttext .= ''; foreach my $item ('passback','roster') { @@ -12606,6 +13363,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); + 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:').''; + } else { + $resulttext = &mt('No changes made.'); + } + } else { + $errors .= '
  • '.&mt('Failed to save changes').'
  • '; + } + if ($errors) { + $resulttext .= &mt('The following errors occurred: ').''; + } + 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); @@ -19589,6 +20876,7 @@ function balancerChange(balnum,baltotal, END } + sub new_spares_js { my @sparestypes = ('primary','default'); my $types = join("','",@sparestypes); @@ -19662,7 +20950,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') { @@ -19796,7 +21084,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;