--- loncom/lonnet/perl/lonnet.pm	2018/03/28 10:22:10	1.1172.2.93.4.8
+++ loncom/lonnet/perl/lonnet.pm	2020/05/04 15:07:10	1.1172.2.123
@@ -1,7 +1,7 @@
 # The LearningOnline Network
 # TCP networking package
 #
-# $Id: lonnet.pm,v 1.1172.2.93.4.8 2018/03/28 10:22:10 raeburn Exp $
+# $Id: lonnet.pm,v 1.1172.2.123 2020/05/04 15:07:10 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -74,10 +74,11 @@ use strict;
 use LWP::UserAgent();
 use HTTP::Date;
 use Image::Magick;
+use CGI::Cookie;
 
-use vars qw(%perlvar %spareid %pr %prp $memcache %packagetab $tmpdir
+use vars qw(%perlvar %spareid %pr %prp $memcache %packagetab $tmpdir $deftex
             $_64bit %env %protocol %loncaparevs %serverhomeIDs %needsrelease
-            %managerstab);
+            %managerstab $passwdmin);
 
 my (%badServerCache, $memcache, %courselogs, %accesshash, %domainrolehash,
     %userrolehash, $processmarker, $dumpcount, %coursedombuf,
@@ -98,6 +99,7 @@ use LONCAPA qw(:DEFAULT :match);
 use LONCAPA::Configuration;
 use LONCAPA::lonmetadata;
 use LONCAPA::Lond;
+use LONCAPA::transliterate;
 
 use File::Copy;
 
@@ -261,9 +263,10 @@ sub get_server_loncaparev {
                 if ($caller eq 'loncron') {
                     my $ua=new LWP::UserAgent;
                     $ua->timeout(4);
+                    my $hostname = &hostname($lonhost);
                     my $protocol = $protocol{$lonhost};
                     $protocol = 'http' if ($protocol ne 'https');
-                    my $url = $protocol.'://'.&hostname($lonhost).'/adm/about.html';
+                    my $url = $protocol.'://'.$hostname.'/adm/about.html';
                     my $request=new HTTP::Request('GET',$url);
                     my $response=$ua->request($request);
                     unless ($response->is_error()) {
@@ -407,8 +410,26 @@ sub reply {
     unless (defined(&hostname($server))) { return 'no_such_host'; }
     my $answer=subreply($cmd,$server);
     if (($answer=~/^refused/) || ($answer=~/^rejected/)) {
-       &logthis("<font color=\"blue\">WARNING:".
-                " $cmd to $server returned $answer</font>");
+        my $logged = $cmd;
+        if ($cmd =~ /^encrypt:([^:]+):/) {
+            my $subcmd = $1;
+            if (($subcmd eq 'auth') || ($subcmd eq 'passwd') ||
+                ($subcmd eq 'changeuserauth') || ($subcmd eq 'makeuser') ||
+                ($subcmd eq 'putdom') || ($subcmd eq 'autoexportgrades')) {
+                (undef,undef,my @rest) = split(/:/,$cmd);
+                if (($subcmd eq 'auth') || ($subcmd eq 'putdom')) {
+                    splice(@rest,2,1,'Hidden');
+                } elsif ($subcmd eq 'passwd') {
+                    splice(@rest,2,2,('Hidden','Hidden'));
+                } elsif (($subcmd eq 'changeuserauth') || ($subcmd eq 'makeuser') ||
+                         ($subcmd eq 'autoexportgrades')) {
+                    splice(@rest,3,1,'Hidden');
+                }
+                $logged = join(':',('encrypt:'.$subcmd,@rest));
+            }
+        }
+        &logthis("<font color=\"blue\">WARNING:".
+                 " $logged to $server returned $answer</font>");
     }
     return $answer;
 }
@@ -603,18 +624,39 @@ sub transfer_profile_to_env {
 sub check_for_valid_session {
     my ($r,$name,$userhashref,$domref) = @_;
     my %cookies=CGI::Cookie->parse($r->header_in('Cookie'));
-    if ($name eq '') {
-        $name = 'lonID';
-    }
-    my $lonid=$cookies{$name};
-    return undef if (!$lonid);
-
-    my $handle=&LONCAPA::clean_handle($lonid->value);
-    my $lonidsdir;
+    my ($lonidsdir,$linkname,$pubname,$secure,$lonid);
     if ($name eq 'lonDAV') {
         $lonidsdir=$r->dir_config('lonDAVsessDir');
     } else {
         $lonidsdir=$r->dir_config('lonIDsDir');
+        if ($name eq '') {
+            $name = 'lonID';
+        }
+    }
+    if ($name eq 'lonID') {
+        $secure = 'lonSID';
+        $linkname = 'lonLinkID';
+        $pubname = 'lonPubID';
+        if (exists($cookies{$secure})) {
+            $lonid=$cookies{$secure};
+        } elsif (exists($cookies{$name})) {
+            $lonid=$cookies{$name};
+        } elsif ((exists($cookies{$linkname})) && ($ENV{'SERVER_PORT'} != 443)) {
+            $lonid=$cookies{$linkname};
+        } elsif (exists($cookies{$pubname})) {
+            $lonid=$cookies{$pubname};
+        }
+    } else {
+        $lonid=$cookies{$name};
+    }
+    return undef if (!$lonid);
+
+    my $handle=&LONCAPA::clean_handle($lonid->value);
+    if (-l "$lonidsdir/$handle.id") {
+        my $link = readlink("$lonidsdir/$handle.id");
+        if ((-e $link) && ($link =~ m{^\Q$lonidsdir\E/(.+)\.id$})) {
+            $handle = $1;
+        }
     }
     if (!-e "$lonidsdir/$handle.id") {
         if ((ref($domref)) && ($name eq 'lonID') &&
@@ -639,6 +681,7 @@ sub check_for_valid_session {
 
     if (!defined($disk_env{'user.name'})
 	|| !defined($disk_env{'user.domain'})) {
+        untie(%disk_env);
 	return undef;
     }
 
@@ -646,6 +689,7 @@ sub check_for_valid_session {
         $userhashref->{'name'} = $disk_env{'user.name'};
         $userhashref->{'domain'} = $disk_env{'user.domain'};
     }
+    untie(%disk_env);
 
     return $handle;
 }
@@ -670,6 +714,37 @@ sub timed_flock {
     }
 }
 
+sub get_sessionfile_vars {
+    my ($handle,$lonidsdir,$storearr) = @_;
+    my %returnhash;
+    unless (ref($storearr) eq 'ARRAY') {
+        return %returnhash;
+    }
+    if (-l "$lonidsdir/$handle.id") {
+        my $link = readlink("$lonidsdir/$handle.id");
+        if ((-e $link) && ($link =~ m{^\Q$lonidsdir\E/(.+)\.id$})) {
+            $handle = $1;
+        }
+    }
+    if ((-e "$lonidsdir/$handle.id") &&
+        ($handle =~ /^($match_username)\_\d+\_($match_domain)\_(.+)$/)) {
+        my ($possuname,$possudom,$possuhome) = ($1,$2,$3);
+        if ((&domain($possudom) ne '') && (&homeserver($possuname,$possudom) eq $possuhome)) {
+            if (open(my $idf,'+<',"$lonidsdir/$handle.id")) {
+                flock($idf,LOCK_SH);
+                if (tie(my %disk_env,'GDBM_File',"$lonidsdir/$handle.id",
+                        &GDBM_READER(),0640)) {
+                    foreach my $item (@{$storearr}) {
+                        $returnhash{$item} = $disk_env{$item};
+                    }
+                    untie(%disk_env);
+                }
+            }
+        }
+    }
+    return %returnhash;
+}
+
 # ---------------------------------------------------------- Append Environment
 
 sub appenv {
@@ -695,16 +770,19 @@ sub appenv {
                 $env{$key}=$newenv->{$key};
             }
         }
-        my $opened = open(my $env_file,'+<',$env{'user.environment'});
-        if ($opened
-	    && &timed_flock($env_file,LOCK_EX)
-	    &&
-	    tie(my %disk_env,'GDBM_File',$env{'user.environment'},
-	        (&GDBM_WRITER()|&GDBM_NOLOCK()),0640)) {
-	    while (my ($key,$value) = each(%{$newenv})) {
-	        $disk_env{$key} = $value;
-	    }
-	    untie(%disk_env);
+        my $lonids = $perlvar{'lonIDsDir'};
+        if ($env{'user.environment'} =~ m{^\Q$lonids/\E$match_username\_\d+\_$match_domain\_[\w\-.]+\.id$}) {
+            my $opened = open(my $env_file,'+<',$env{'user.environment'});
+            if ($opened
+	        && &timed_flock($env_file,LOCK_EX)
+	        &&
+	        tie(my %disk_env,'GDBM_File',$env{'user.environment'},
+	            (&GDBM_WRITER()|&GDBM_NOLOCK()),0640)) {
+	        while (my ($key,$value) = each(%{$newenv})) {
+	            $disk_env{$key} = $value;
+	        }
+	        untie(%disk_env);
+            }
         }
     }
     return 'ok';
@@ -820,6 +898,7 @@ sub userload {
 	while ($filename=readdir(LONIDS)) {
 	    next if ($filename eq '.' || $filename eq '..');
 	    next if ($filename =~ /publicuser_\d+\.id/);
+            next if ($filename =~ /^[a-f0-9]+_linked\.id$/);
 	    my ($mtime)=(stat($perlvar{'lonIDsDir'}.'/'.$filename))[9];
 	    if ($curtime-$mtime < 1800) { $numusers++; }
 	}
@@ -875,13 +954,13 @@ sub spareserver {
     }
 
     if (!$want_server_name) {
-        my $protocol = 'http';
-        if ($protocol{$spare_server} eq 'https') {
-            $protocol = $protocol{$spare_server};
-        }
         if (defined($spare_server)) {
             my $hostname = &hostname($spare_server);
             if (defined($hostname)) {
+                my $protocol = 'http';
+                if ($protocol{$spare_server} eq 'https') {
+                    $protocol = $protocol{$spare_server};
+                }
 	        $spare_server = $protocol.'://'.$hostname;
             }
         }
@@ -948,6 +1027,75 @@ sub find_existing_session {
     return;
 }
 
+# check if user's browser sent load balancer cookie and server still has session
+# and is not overloaded.
+sub check_for_balancer_cookie {
+    my ($r,$update_mtime) = @_;
+    my ($otherserver,$cookie);
+    my %cookies=CGI::Cookie->parse($r->header_in('Cookie'));
+    if (exists($cookies{'balanceID'})) {
+        my $balid = $cookies{'balanceID'};
+        $cookie=&LONCAPA::clean_handle($balid->value);
+        my $balancedir=$r->dir_config('lonBalanceDir');
+        if ((-d $balancedir) && (-e "$balancedir/$cookie.id")) {
+            if ($cookie =~ /^($match_domain)_($match_username)_[a-f0-9]+$/) {
+                my ($possudom,$possuname) = ($1,$2);
+                my $has_session = 0;
+                if ((&domain($possudom) ne '') &&
+                    (&homeserver($possuname,$possudom) ne 'no_host')) {
+                    my $try_server;
+                    my $opened = open(my $idf,'+<',"$balancedir/$cookie.id");
+                    if ($opened) {
+                        flock($idf,LOCK_SH);
+                        while (my $line = <$idf>) {
+                            chomp($line);
+                            if (&hostname($line) ne '') {
+                                $try_server = $line;
+                                last;
+                            }
+                        }
+                        close($idf);
+                        if (($try_server) &&
+                            (&has_user_session($try_server,$possudom,$possuname))) {
+                            my $lowest_load = 30000;
+                            ($otherserver,$lowest_load) =
+                                &compare_server_load($try_server,undef,$lowest_load);
+                            if ($otherserver ne '' && $lowest_load < 100) {
+                                $has_session = 1;
+                            } else {
+                                undef($otherserver);
+                            }
+                        }
+                    }
+                }
+                if ($has_session) {
+                    if ($update_mtime) {
+                        my $atime = my $mtime = time;
+                        utime($atime,$mtime,"$balancedir/$cookie.id");
+                    }
+                } else {
+                    unlink("$balancedir/$cookie.id");
+                }
+            }
+        }
+    }
+    return ($otherserver,$cookie);
+}
+
+sub delbalcookie {
+    my ($cookie,$balancer) =@_;
+    if ($cookie =~ /^($match_domain)\_($match_username)\_[a-f0-9]{32}$/) {
+        my ($udom,$uname) = ($1,$2);
+        my $uprimary_id = &domain($udom,'primary');
+        my $uintdom = &internet_dom($uprimary_id);
+        my $intdom = &internet_dom($balancer);
+        my $serverhomedom = &host_domain($balancer);
+        if (($uintdom ne '') && ($uintdom eq $intdom)) {
+            return &reply("delbalcookie:$cookie",$balancer);
+        }
+    }
+}
+
 # -------------------------------- ask if server already has a session for user
 sub has_user_session {
     my ($lonid,$udom,$uname) = @_;
@@ -1014,6 +1162,28 @@ sub choose_server {
     return ($login_host,$hostname,$portal_path,$isredirect,$lowest_load);
 }
 
+sub get_course_sessions {
+    my ($cnum,$cdom,$lastactivity) = @_;
+    my %servers = &internet_dom_servers($cdom);
+    my %returnhash;
+    foreach my $server (sort(keys(%servers))) {
+        my $rep = &reply("coursesessions:$cdom:$cnum:$lastactivity",$server);
+        my @pairs=split(/\&/,$rep);
+        unless (($rep eq 'unknown_cmd') || ($rep =~ /^error/)) {
+            foreach my $item (@pairs) {
+                my ($key,$value)=split(/=/,$item,2);
+                $key = &unescape($key);
+                next if ($key =~ /^error: 2 /);
+                if (exists($returnhash{$key})) {
+                    next if ($value < $returnhash{$key});
+                }
+                $returnhash{$key}=$value;
+            }
+        }
+    }
+    return %returnhash;
+}
+
 # --------------------------------------------- Try to change a user's password
 
 sub changepass {
@@ -1049,6 +1219,9 @@ sub changepass {
     } elsif ($answer =~ "invalid_client") {
         &logthis("$server refused to change $uname in $udom password because ".
                  "it was a reset by e-mail originating from an invalid server.");
+    } elsif ($answer =~ "^prioruse") {
+       &logthis("$server refused to change $uname in $udom password because ".
+                "the password had been used before");
     }
     return $answer;
 }
@@ -1285,7 +1458,7 @@ sub get_lonbalancer_config {
 sub check_loadbalancing {
     my ($uname,$udom,$caller) = @_;
     my ($is_balancer,$currtargets,$currrules,$dom_in_use,$homeintdom,
-        $rule_in_effect,$offloadto,$otherserver);
+        $rule_in_effect,$offloadto,$otherserver,$setcookie);
     my $lonhost = $perlvar{'lonHostID'};
     my @hosts = &current_machine_ids();
     my $uprimary_id = &Apache::lonnet::domain($udom,'primary');
@@ -1312,7 +1485,7 @@ sub check_loadbalancing {
         }
     }
     if (ref($result) eq 'HASH') {
-        ($is_balancer,$currtargets,$currrules) =
+        ($is_balancer,$currtargets,$currrules,$setcookie) =
             &check_balancer_result($result,@hosts);
         if ($is_balancer) {
             if (ref($currrules) eq 'HASH') {
@@ -1373,7 +1546,7 @@ sub check_loadbalancing {
             }
         }
         if (ref($result) eq 'HASH') {
-            ($is_balancer,$currtargets,$currrules) =
+            ($is_balancer,$currtargets,$currrules,$setcookie) =
                 &check_balancer_result($result,@hosts);
             if ($is_balancer) {
                 if (ref($currrules) eq 'HASH') {
@@ -1439,20 +1612,22 @@ sub check_loadbalancing {
                 $is_balancer = 0;
                 if ($uname ne '' && $udom ne '') {
                     if (($env{'user.name'} eq $uname) && ($env{'user.domain'} eq $udom)) {
-
                         &appenv({'user.loadbalexempt'     => $lonhost,
                                  'user.loadbalcheck.time' => time});
                     }
                 }
             }
         }
+        unless ($homeintdom) {
+            undef($setcookie);
+        }
     }
-    return ($is_balancer,$otherserver);
+    return ($is_balancer,$otherserver,$setcookie);
 }
 
 sub check_balancer_result {
     my ($result,@hosts) = @_;
-    my ($is_balancer,$currtargets,$currrules);
+    my ($is_balancer,$currtargets,$currrules,$setcookie);
     if (ref($result) eq 'HASH') {
         if ($result->{'lonhost'} ne '') {
             my $currbalancer = $result->{'lonhost'};
@@ -1468,12 +1643,13 @@ sub check_balancer_result {
                     $is_balancer = 1;
                     $currrules = $result->{$key}{'rules'};
                     $currtargets = $result->{$key}{'targets'};
+                    $setcookie = $result->{$key}{'cookie'};
                     last;
                 }
             }
         }
     }
-    return ($is_balancer,$currtargets,$currrules);
+    return ($is_balancer,$currtargets,$currrules,$setcookie);
 }
 
 sub get_loadbalancer_targets {
@@ -1697,12 +1873,7 @@ sub get_dom {
         }
     }
     if ($udom && $uhome && ($uhome ne 'no_host')) {
-        my $rep;
-        if ($namespace =~ /^enc/) {
-            $rep=&reply("encrypt:egetdom:$udom:$namespace:$items",$uhome);
-        } else {
-            $rep=&reply("getdom:$udom:$namespace:$items",$uhome);
-        }
+        my $rep=&reply("getdom:$udom:$namespace:$items",$uhome);
         my %returnhash;
         if ($rep eq '' || $rep =~ /^error: 2 /) {
             return %returnhash;
@@ -1746,11 +1917,7 @@ sub put_dom {
             $items.=&escape($item).'='.&freeze_escape($$storehash{$item}).'&';
         }
         $items=~s/\&$//;
-        if ($namespace =~ /^enc/) {
-            return &reply("encrypt:putdom:$udom:$namespace:$items",$uhome);
-        } else {
-            return &reply("putdom:$udom:$namespace:$items",$uhome);
-        }
+        return &reply("putdom:$udom:$namespace:$items",$uhome);
     } else {
         &logthis("put_dom failed - no homeserver and/or domain");
     }
@@ -1843,11 +2010,11 @@ sub inst_directory_query {
     if ($homeserver ne '') {
         unless ($homeserver eq $perlvar{'lonHostID'}) {
             if ($srch->{'srchby'} eq 'email') {
-                my $lcrev = &get_server_loncaparev(undef,$homeserver);
-                my ($major,$minor,$subver) = ($lcrev =~ /^\'?(\d+)\.(\d+)\.([\w.\-]+)\'?$/);
+                my $lcrev = &get_server_loncaparev($udom,$homeserver);
+                my ($major,$minor,$subver) = ($lcrev =~ /^\'?(\d+)\.(\d+)\.(\d+)[\w.\-]+\'?$/);
                 if (($major eq '' && $minor eq '') || ($major < 2) ||
-                    (($major == 2) && ($minor < 11)) || 
-                    (($major == 2) && ($minor == 11) && ($subver !~ /^2\.B/))) {
+                    (($major == 2) && ($minor < 11)) ||
+                    (($major == 2) && ($minor == 11) && ($subver < 3))) {
                     return;
                 }
             }
@@ -1895,11 +2062,11 @@ sub usersearch {
         if (&host_domain($tryserver) eq $dom) {
             unless ($tryserver eq $perlvar{'lonHostID'}) {
                 if ($srch->{'srchby'} eq 'email') {
-                    my $lcrev = &get_server_loncaparev(undef,$tryserver);
-                    my ($major,$minor,$subver) = ($lcrev =~ /^\'?(\d+)\.(\d+)\.([\w.\-]+)\'?$/);
+                    my $lcrev = &get_server_loncaparev($dom,$tryserver);
+                    my ($major,$minor,$subver) = ($lcrev =~ /^\'?(\d+)\.(\d+)\.(\d+)[\w.\-]+\'?$/);
                     next if (($major eq '' && $minor eq '') || ($major < 2) ||
-                             (($major == 2) && ($minor < 11)) || 
-                             (($major == 2) && ($minor == 11) && ($subver !~ /^2\.B/)));
+                             (($major == 2) && ($minor < 11)) ||
+                             (($major == 2) && ($minor == 11) && ($subver < 3)));
                 }
             }
             my $host=&hostname($tryserver);
@@ -2219,6 +2386,9 @@ sub get_domain_defaults {
         } elsif ($domconfig{'coursedefaults'}{'canclone'}) {
             $domdefaults{'canclone'}=$domconfig{'coursedefaults'}{'canclone'};
         }
+        if ($domconfig{'coursedefaults'}{'texengine'}) {
+            $domdefaults{'texengine'} = $domconfig{'coursedefaults'}{'texengine'};
+        }
     }
     if (ref($domconfig{'usersessions'}) eq 'HASH') {
         if (ref($domconfig{'usersessions'}{'remote'}) eq 'HASH') {
@@ -2282,20 +2452,86 @@ sub get_domain_defaults {
     return %domdefaults;
 }
 
-sub course_portal_url {
-    my ($cnum,$cdom) = @_;
-    my $chome = &homeserver($cnum,$cdom);
-    my $hostname = &hostname($chome);
-    my $protocol = $protocol{$chome};
-    $protocol = 'http' if ($protocol ne 'https');
-    my %domdefaults = &get_domain_defaults($cdom);
-    my $firsturl;
-    if ($domdefaults{'portal_def'}) {
-        $firsturl = $domdefaults{'portal_def'};
-    } else {
-        $firsturl = $protocol.'://'.$hostname;
+sub get_dom_cats {
+    my ($dom) = @_;
+    return unless (&domain($dom));
+    my ($cats,$cached)=&is_cached_new('cats',$dom);
+    unless (defined($cached)) {
+        my %domconfig = &get_dom('configuration',['coursecategories'],$dom);
+        if (ref($domconfig{'coursecategories'}) eq 'HASH') {
+            if (ref($domconfig{'coursecategories'}{'cats'}) eq 'HASH') {
+                %{$cats} = %{$domconfig{'coursecategories'}{'cats'}};
+            } else {
+                $cats = {};
+            }
+        } else {
+            $cats = {};
+        }
+        &Apache::lonnet::do_cache_new('cats',$dom,$cats,3600);
+    }
+    return $cats;
+}
+
+sub get_dom_instcats {
+    my ($dom) = @_;
+    return unless (&domain($dom));
+    my ($instcats,$cached)=&is_cached_new('instcats',$dom);
+    unless (defined($cached)) {
+        my (%coursecodes,%codes,@codetitles,%cat_titles,%cat_order);
+        my $totcodes = &retrieve_instcodes(\%coursecodes,$dom);
+        if ($totcodes > 0) {
+            my $caller = 'global';
+            if (&auto_instcode_format($caller,$dom,\%coursecodes,\%codes,
+                                      \@codetitles,\%cat_titles,\%cat_order) eq 'ok') {
+                $instcats = {
+                                codes => \%codes,
+                                codetitles => \@codetitles,
+                                cat_titles => \%cat_titles,
+                                cat_order => \%cat_order,
+                            };
+                &do_cache_new('instcats',$dom,$instcats,3600);
+            }
+        }
+    }
+    return $instcats;
+}
+
+sub retrieve_instcodes {
+    my ($coursecodes,$dom) = @_;
+    my $totcodes;
+    my %courses = &courseiddump($dom,'.',1,'.','.','.',undef,undef,'Course');
+    foreach my $course (keys(%courses)) {
+        if (ref($courses{$course}) eq 'HASH') {
+            if ($courses{$course}{'inst_code'} ne '') {
+                $$coursecodes{$course} = $courses{$course}{'inst_code'};
+                $totcodes ++;
+            }
+        }
+    }
+    return $totcodes;
+}
+
+# --------------------------------------------- Get domain config for passwords
+
+sub get_passwdconf {
+    my ($dom) = @_;
+    my (%passwdconf,$gotconf,$lookup);
+    my ($result,$cached)=&is_cached_new('passwdconf',$dom);
+    if (defined($cached)) {
+        if (ref($result) eq 'HASH') {
+            %passwdconf = %{$result};
+            $gotconf = 1;
+        }
+    }
+    unless ($gotconf) {
+        my %domconfig = &get_dom('configuration',['passwords'],$dom);
+        if (ref($domconfig{'passwords'}) eq 'HASH') {
+            %passwdconf = %{$domconfig{'passwords'}};
+        }
+        my $cachetime = 24*60*60;
+        &do_cache_new('passwdconf',$dom,\%passwdconf,$cachetime);
     }
-    return $firsturl;
+    return %passwdconf;
 }
 
 # --------------------------------------------------- Assign a key to a student
@@ -2882,8 +3118,7 @@ sub absolute_url {
 sub ssi {
 
     my ($fn,%form)=@_;
-    my $ua=new LWP::UserAgent;
-    my $request;
+    my ($request,$response);
 
     $form{'no_update_last_known'}=1;
     &Apache::lonenc::check_encrypt(\$fn);
@@ -2900,7 +3135,30 @@ sub ssi {
     }
 
     $request->header(Cookie => $ENV{'HTTP_COOKIE'});
-    my $response= $ua->request($request);
+
+    if (($env{'request.course.id'}) &&
+        ($form{'grade_courseid'} eq $env{'request.course.id'}) &&
+        ($form{'grade_username'} ne '') && ($form{'grade_domain'} ne '') &&
+        ($form{'grade_symb'} ne '') &&
+        (&Apache::lonnet::allowed('mgr',$env{'request.course.id'}.
+                                 ($env{'request.course.sec'}?'/'.$env{'request.course.sec'}:'')))) {
+        if (LWP::UserAgent->VERSION >= 5.834) {
+            my $ua=new LWP::UserAgent;
+            $ua->local_address('127.0.0.1');
+            $response = $ua->request($request);
+        } else {
+            {
+                require LWP::Protocol::http;
+                local @LWP::Protocol::http::EXTRA_SOCK_OPTS = (LocalAddr => '127.0.0.1');
+                my $ua=new LWP::UserAgent;
+                $response = $ua->request($request);
+                @LWP::Protocol::http::EXTRA_SOCK_OPTS = ();
+            }
+        }
+    } else {
+        my $ua=new LWP::UserAgent;
+        $response = $ua->request($request);
+    }
     if (wantarray) {
 	return ($response->content, $response);
     } else {
@@ -2920,6 +3178,72 @@ sub externalssi {
     }
 }
 
+# If the local copy of a replicated resource is outdated, trigger a
+# connection from the homeserver to flush the delayed queue. If no update
+# happens, remove local copies of outdated resource (and corresponding
+# metadata file).
+
+sub remove_stale_resfile {
+    my ($url) = @_;
+    my $removed;
+    if ($url=~m{^/res/($match_domain)/($match_username)/}) {
+        my $audom = $1;
+        my $auname = $2;
+        unless (($url =~ /\.\d+\.\w+$/) || ($url =~ m{^/res/lib/templates/})) {
+            my $homeserver = &homeserver($auname,$audom);
+            unless (($homeserver eq 'no_host') ||
+                    (grep { $_ eq $homeserver } &current_machine_ids())) {
+                my $fname = &filelocation('',$url);
+                if (-e $fname) {
+                    my $hostname = &hostname($homeserver);
+                    if ($hostname) {
+                        my $protocol = $protocol{$homeserver};
+                        $protocol = 'http' if ($protocol ne 'https');
+                        my $uri = $protocol.'://'.$hostname.'/raw/'.&declutter($url);
+                        my $ua=new LWP::UserAgent;
+                        $ua->timeout(5);
+                        my $request=new HTTP::Request('HEAD',$uri);
+                        my $response=$ua->request($request);
+                        if ($response->is_success()) {
+                            my $remmodtime = &HTTP::Date::str2time( $response->header('Last-modified') );
+                            my $locmodtime = (stat($fname))[9];
+                            if ($locmodtime < $remmodtime) {
+                                my $stale;
+                                my $answer = &reply('pong',$homeserver);
+                                if ($answer eq $homeserver.':'.$perlvar{'lonHostID'}) {
+                                    sleep(0.2);
+                                    $locmodtime = (stat($fname))[9];
+                                    if ($locmodtime < $remmodtime) {
+                                        my $posstransfer = $fname.'.in.transfer';
+                                        if ((-e $posstransfer) && ($remmodtime < (stat($posstransfer))[9])) {
+                                            $removed = 1;
+                                        } else {
+                                            $stale = 1;
+                                        }
+                                    } else {
+                                        $removed = 1;
+                                    }
+                                } else {
+                                    $stale = 1;
+                                }
+                                if ($stale) {
+                                    unlink($fname);
+                                    if ($uri!~/\.meta$/) {
+                                        unlink($fname.'.meta');
+                                    }
+                                    &reply("unsub:$fname",$homeserver);
+                                    $removed = 1;
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+    return $removed;
+}
+
 # -------------------------------- Allow a /uploaded/ URI to be vouched for
 
 sub allowuploaded {
@@ -3058,14 +3382,18 @@ sub can_edit_resource {
                         $forceedit = 1;
                     }
                     $cfile = $resurl;
-                } elsif ($resurl =~ m{^/adm/wrapper/adm/$cdom/$cnum/\d+/ext\.tool$}) {
-                    $incourse = 1;
-                    if ($env{'form.forceedit'}) {
-                        $forceview = 1;
-                    } else {
-                        $forceedit = 1;
+                } elsif (($resurl =~ m{^/ext/}) && ($symb ne '')) {
+                    my ($map,$id,$res) = &decode_symb($symb);
+                    if ($map =~ /\.page$/) {
+                        $incourse = 1;
+                        if ($env{'form.forceedit'}) {
+                            $forceview = 1;
+                            $cfile = $map;
+                        } else {
+                            $forceedit = 1;
+                            $cfile =  '/adm/wrapper'.$resurl;
+                        }
                     }
-                    $cfile = $resurl;
                 } elsif ($resurl =~ m{^/?adm/viewclasslist$}) {
                     $incourse = 1;
                     if ($env{'form.forceedit'}) {
@@ -3083,14 +3411,6 @@ sub can_edit_resource {
                     $cfile = $template;
                 }
             } elsif (($resurl =~ m{^/adm/wrapper/ext/}) && ($env{'form.folderpath'} =~ /^supplemental/)) {
-                    $incourse = 1;
-                    if ($env{'form.forceedit'}) {
-                        $forceview = 1;
-                    } else {
-                        $forceedit = 1;
-                    }
-                    $cfile = $resurl;
-            } elsif (($resurl =~ m{^/adm/wrapper/adm/$cdom/$cnum/\d+/ext\.tool$}) && ($env{'form.folderpath'} =~ /^supplemental/)) {
                 $incourse = 1;
                 if ($env{'form.forceedit'}) {
                     $forceview = 1;
@@ -3107,13 +3427,8 @@ sub can_edit_resource {
                     $cfile = &clutter($res);
                 } else {
                     $cfile = $env{'form.suppurl'};
-                    my $escfile = &unescape($cfile);
-                    if ($escfile =~ m{^/adm/$cdom/$cnum/\d+/ext\.tool$}) {
-                        $cfile = '/adm/wrapper'.$escfile;
-                    } else {
-                        $escfile =~ s{^http://}{};
-                        $cfile = &escape("/adm/wrapper/ext/$escfile");
-                    }
+                    $cfile =~ s{^http://}{};
+                    $cfile = '/adm/wrapper/ext/'.$cfile;
                 }
             } elsif ($resurl =~ m{^/?adm/viewclasslist$}) {
                 if ($env{'form.forceedit'}) {
@@ -3352,6 +3667,9 @@ sub clean_filename {
     }
 # Replace spaces by underscores
     $fname=~s/\s+/\_/g;
+# Transliterate non-ascii text to ascii
+    my $lang = &Apache::lonlocal::current_language();
+    $fname = &LONCAPA::transliterate::fname_to_ascii($fname,$lang);
 # Replace all other weird characters by nothing
     $fname=~s{[^/\w\.\-]}{}g;
 # Replace all .\d. sequences with _\d. so they no longer look like version
@@ -3359,6 +3677,7 @@ sub clean_filename {
     $fname=~s/\.(\d+)(?=\.)/_$1/g;
     return $fname;
 }
+
 # This Function checks if an Image's dimensions exceed either $resizewidth (width) 
 # or $resizeheight (height) - both pixels. If so, the image is scaled to produce an 
 # image with the same aspect ratio as the original, but with dimensions which do 
@@ -3401,13 +3720,16 @@ sub resizeImage {
 # input: $formname - the contents of the file are in $env{"form.$formname"}
 #                    the desired filename is in $env{"form.$formname.filename"}
 #        $context - possible values: coursedoc, existingfile, overwrite, 
-#                                    canceloverwrite, or ''. 
+#                                    canceloverwrite, scantron or ''. 
 #                   if 'coursedoc': upload to the current course
 #                   if 'existingfile': write file to tmp/overwrites directory 
 #                   if 'canceloverwrite': delete file written to tmp/overwrites directory
 #                   $context is passed as argument to &finishuserfileupload
 #        $subdir - directory in userfile to store the file into
-#        $parser - instruction to parse file for objects ($parser = parse)    
+#        $parser - instruction to parse file for objects ($parser = parse) or
+#                  if context is 'scantron', $parser is hashref of csv column mapping
+#                  (e.g.,{ PaperID => 0, LastName => 1, FirstName => 2, ID => 3,
+#                          Section => 4, CODE => 5, FirstQuestion => 9 }).
 #        $allfiles - reference to hash for embedded objects
 #        $codebase - reference to hash for codebase of java objects
 #        $desuname - username for permanent storage of uploaded file
@@ -3430,6 +3752,14 @@ sub userfileupload {
     $fname=&clean_filename($fname);
     # See if there is anything left
     unless ($fname) { return 'error: no uploaded file'; }
+    # If filename now begins with a . prepend unix timestamp _ milliseconds
+    if ($fname =~ /^\./) {
+        my ($s,$usec) = &gettimeofday();
+        while (length($usec) < 6) {
+            $usec = '0'.$usec;
+        }
+        $fname = $s.'_'.substr($usec,0,3).$fname;
+    }
     # Files uploaded to help request form, or uploaded to "create course" page are handled differently
     if ((($formname eq 'screenshot') && ($subdir eq 'helprequests')) ||
         (($formname eq 'coursecreatorxml') && ($subdir eq 'batchupload')) ||
@@ -3448,7 +3778,7 @@ sub userfileupload {
             } else {
                 $docudom = $env{'user.domain'};
             }
-            if ($destuname =~ /^$match_username$/) {
+            if ($destuname =~ /^$match_username$/) { 
                 $docuname = $destuname;
             } else {
                 $docuname = $env{'user.name'};
@@ -3597,7 +3927,7 @@ sub finishuserfileupload {
             }
         }
     }
-    if ($parser eq 'parse') {
+    if (($context ne 'scantron') && ($parser eq 'parse')) {
         if ((ref($mimetype)) && ($$mimetype eq 'text/html')) {
             my $parse_result = &extract_embedded_items($filepath.'/'.$file,
                                                        $allfiles,$codebase);
@@ -3606,6 +3936,9 @@ sub finishuserfileupload {
 	   	         ' for embedded media: '.$parse_result); 
             }
         }
+    } elsif (($context eq 'scantron') && (ref($parser) eq 'HASH')) {
+        my $format = $env{'form.scantron_format'};
+        &bubblesheet_converter($docudom,$filepath.'/'.$file,$parser,$format);
     }
     if (($thumbwidth =~ /^\d+$/) && ($thumbheight =~ /^\d+$/)) {
         my $input = $filepath.'/'.$file;
@@ -3846,6 +4179,246 @@ sub embedded_dependency {
     return;
 }
 
+sub bubblesheet_converter {
+    my ($cdom,$fullpath,$config,$format) = @_;
+    if ((&domain($cdom) ne '') &&
+        ($fullpath =~ m{^\Q$perlvar{'lonDocRoot'}/userfiles/$cdom/\E$match_courseid/scantron_orig}) &&
+        (-e $fullpath) && (ref($config) eq 'HASH') && ($format ne '')) {
+        my (%csvcols,%csvoptions);
+        if (ref($config->{'fields'}) eq 'HASH') {
+            %csvcols = %{$config->{'fields'}};
+        }
+        if (ref($config->{'options'}) eq 'HASH') {
+            %csvoptions = %{$config->{'options'}};
+        }
+        my %csvbynum = reverse(%csvcols);
+        my %scantronconf = &get_scantron_config($format,$cdom);
+        if (keys(%scantronconf)) {
+            my %bynum = (
+                          $scantronconf{CODEstart} => 'CODEstart',
+                          $scantronconf{IDstart}   => 'IDstart',
+                          $scantronconf{PaperID}   => 'PaperID',
+                          $scantronconf{FirstName} => 'FirstName',
+                          $scantronconf{LastName}  => 'LastName',
+                          $scantronconf{Qstart}    => 'Qstart',
+                        );
+            my @ordered;
+            foreach my $item (sort { $a <=> $b } keys(%bynum)) {
+                push(@ordered,$bynum{$item});
+            }
+            my %mapstart = (
+                              CODEstart => 'CODE',
+                              IDstart   => 'ID',
+                              PaperID   => 'PaperID',
+                              FirstName => 'FirstName',
+                              LastName  => 'LastName',
+                              Qstart    => 'FirstQuestion',
+                           );
+            my %maplength = (
+                              CODEstart => 'CODElength',
+                              IDstart   => 'IDlength',
+                              PaperID   => 'PaperIDlength',
+                              FirstName => 'FirstNamelength',
+                              LastName  => 'LastNamelength',
+            );
+            if (open(my $fh,'<',$fullpath)) {
+                my $output;
+                my %lettdig = &letter_to_digits();
+                my %diglett = reverse(%lettdig);
+                my $numletts = scalar(keys(%lettdig));
+                my $num = 0;
+                while (my $line=<$fh>) {
+                    $num ++;
+                    next if (($num == 1) && ($csvoptions{'hdr'} == 1));
+                    $line =~ s{[\r\n]+$}{};
+                    my %found;
+                    my @values = split(/,/,$line);
+                    my ($qstart,$record);
+                    for (my $i=0; $i<@values; $i++) {
+                        if ((($qstart ne '') && ($i > $qstart)) ||
+                            ($csvbynum{$i} eq 'FirstQuestion')) {
+                            if ($values[$i] eq '') {
+                                $values[$i] = $scantronconf{'Qoff'};
+                            } elsif ($scantronconf{'Qon'} eq 'number') {
+                                if ($values[$i] =~ /^[A-Ja-j]$/) {
+                                    $values[$i] = $lettdig{uc($values[$i])};
+                                }
+                            } elsif ($scantronconf{'Qon'} eq 'letter') {
+                                if ($values[$i] =~ /^[0-9]$/) {
+                                    $values[$i] = $diglett{$values[$i]};
+                                }
+                            } else {
+                                if ($values[$i] =~ /^[0-9A-Ja-j]$/) {
+                                    my $digit;
+                                    if ($values[$i] =~ /^[A-Ja-j]$/) {
+                                        $digit = $lettdig{uc($values[$i])}-1;
+                                        if ($values[$i] eq 'J') {
+                                            $digit += $numletts;
+                                        }
+                                    } elsif ($values[$i] =~ /^[0-9]$/) {
+                                        $digit = $values[$i]-1;
+                                        if ($values[$i] eq '0') {
+                                            $digit += $numletts;
+                                        }
+                                    }
+                                    my $qval='';
+                                    for (my $j=0; $j<$scantronconf{'Qlength'}; $j++) {
+                                        if ($j == $digit) {
+                                            $qval .= $scantronconf{'Qon'};
+                                        } else {
+                                            $qval .= $scantronconf{'Qoff'};
+                                        }
+                                    }
+                                    $values[$i] = $qval;
+                                }
+                            }
+                            if (length($values[$i]) > $scantronconf{'Qlength'}) {
+                                $values[$i] = substr($values[$i],0,$scantronconf{'Qlength'});
+                            }
+                            my $numblank = $scantronconf{'Qlength'} - length($values[$i]);
+                            if ($numblank > 0) {
+                                 $values[$i] .= ($scantronconf{'Qoff'} x $numblank);
+                            }
+                            if ($csvbynum{$i} eq 'FirstQuestion') {
+                                $qstart = $i;
+                                $found{$csvbynum{$i}} = $values[$i];
+                            } else {
+                                $found{'FirstQuestion'} .= $values[$i];
+                            }
+                        } elsif (exists($csvbynum{$i})) {
+                            if ($csvoptions{'rem'}) {
+                                $values[$i] =~ s/^\s+//;
+                            }
+                            if (($csvbynum{$i} eq 'PaperID') && ($csvoptions{'pad'})) {
+                                while (length($values[$i]) < $scantronconf{$maplength{$csvbynum{$i}}}) {
+                                    $values[$i] = '0'.$values[$i];
+                                }
+                            }
+                            $found{$csvbynum{$i}} = $values[$i];
+                        }
+                    }
+                    foreach my $item (@ordered) {
+                        my $currlength = 1+length($record);
+                        my $numspaces = $scantronconf{$item} - $currlength;
+                        if ($numspaces > 0) {
+                            $record .= (' ' x $numspaces);
+                        }
+                        if (($mapstart{$item} ne '') && (exists($found{$mapstart{$item}}))) {
+                            unless ($item eq 'Qstart') {
+                                if (length($found{$mapstart{$item}}) > $scantronconf{$maplength{$item}}) {
+                                    $found{$mapstart{$item}} = substr($found{$mapstart{$item}},0,$scantronconf{$maplength{$item}});
+                                }
+                            }
+                            $record .= $found{$mapstart{$item}};
+                        }
+                    }
+                    $output .= "$record\n";
+                }
+                close($fh);
+                if ($output) {
+                    if (open(my $fh,'>',$fullpath)) {
+                        print $fh $output;
+                        close($fh);
+                    }
+                }
+            }
+        }
+        return;
+    }
+}
+
+sub letter_to_digits {
+    my %lettdig = (
+                    A => 1,
+                    B => 2,
+                    C => 3,
+                    D => 4,
+                    E => 5,
+                    F => 6,
+                    G => 7,
+                    H => 8,
+                    I => 9,
+                    J => 0,
+                  );
+    return %lettdig;
+}
+
+sub get_scantron_config {
+    my ($which,$cdom) = @_;
+    my @lines = &get_scantronformat_file($cdom);
+    my %config;
+    #FIXME probably should move to XML it has already gotten a bit much now
+    foreach my $line (@lines) {
+        my ($name,$descrip)=split(/:/,$line);
+        if ($name ne $which ) { next; }
+        chomp($line);
+        my @config=split(/:/,$line);
+        $config{'name'}=$config[0];
+        $config{'description'}=$config[1];
+        $config{'CODElocation'}=$config[2];
+        $config{'CODEstart'}=$config[3];
+        $config{'CODElength'}=$config[4];
+        $config{'IDstart'}=$config[5];
+        $config{'IDlength'}=$config[6];
+        $config{'Qstart'}=$config[7];
+        $config{'Qlength'}=$config[8];
+        $config{'Qoff'}=$config[9];
+        $config{'Qon'}=$config[10];
+        $config{'PaperID'}=$config[11];
+        $config{'PaperIDlength'}=$config[12];
+        $config{'FirstName'}=$config[13];
+        $config{'FirstNamelength'}=$config[14];
+        $config{'LastName'}=$config[15];
+        $config{'LastNamelength'}=$config[16];
+        $config{'BubblesPerRow'}=$config[17];
+        last;
+    }
+    return %config;
+}
+
+sub get_scantronformat_file {
+    my ($cdom) = @_;
+    if ($cdom eq '') {
+        $cdom= $env{'course.'.$env{'request.course.id'}.'.domain'};
+    }
+    my %domconfig = &get_dom('configuration',['scantron'],$cdom);
+    my $gottab = 0;
+    my @lines;
+    if (ref($domconfig{'scantron'}) eq 'HASH') {
+        if ($domconfig{'scantron'}{'scantronformat'} ne '') {
+            my $formatfile = &getfile($perlvar{'lonDocRoot'}.$domconfig{'scantron'}{'scantronformat'});
+            if ($formatfile ne '-1') {
+                @lines = split("\n",$formatfile,-1);
+                $gottab = 1;
+            }
+        }
+    }
+    if (!$gottab) {
+        my $confname = $cdom.'-domainconfig';
+        my $default = $perlvar{'lonDocRoot'}.'/res/'.$cdom.'/'.$confname.'/default.tab';
+        my $formatfile = &getfile($default);
+        if ($formatfile ne '-1') {
+            @lines = split("\n",$formatfile,-1);
+            $gottab = 1;
+        }
+    }
+    if (!$gottab) {
+        my @domains = &current_machine_domains();
+        if (grep(/^\Q$cdom\E$/,@domains)) {
+            if (open(my $fh,'<',$perlvar{'lonTabDir'}.'/scantronformat.tab')) {
+                @lines = <$fh>;
+                close($fh);
+            }
+        } else {
+            if (open(my $fh,'<',$perlvar{'lonTabDir'}.'/default_scantronformat.tab')) {
+                @lines = <$fh>;
+                close($fh);
+            }
+        }
+    }
+    return @lines;
+}
+
 sub removeuploadedurl {
     my ($url)=@_;	
     my (undef,undef,$udom,$uname,$fname)=split('/',$url,5);    
@@ -4828,10 +5401,9 @@ my %cachedtimes=();
 my $cachedtime='';
 
 sub load_all_first_access {
-    my ($uname,$udom,$ignorecache)=@_;
+    my ($uname,$udom)=@_;
     if (($cachedkey eq $uname.':'.$udom) &&
-        (abs($cachedtime-time)<5) && (!$env{'form.markaccess'}) &&
-        (!$ignorecache)) {
+        (abs($cachedtime-time)<5) && (!$env{'form.markaccess'})) {
         return;
     }
     $cachedtime=time;
@@ -4840,7 +5412,7 @@ sub load_all_first_access {
 }
 
 sub get_first_access {
-    my ($type,$argsymb,$argmap,$ignorecache)=@_;
+    my ($type,$argsymb,$argmap)=@_;
     my ($symb,$courseid,$udom,$uname)=&whichuser();
     if ($argsymb) { $symb=$argsymb; }
     my ($map,$id,$res)=&decode_symb($symb);
@@ -4852,7 +5424,7 @@ sub get_first_access {
     } else {
 	$res=$symb;
     }
-    &load_all_first_access($uname,$udom,$ignorecache);
+    &load_all_first_access($uname,$udom);
     return $cachedtimes{"$courseid\0$res"};
 }
 
@@ -4869,7 +5441,12 @@ sub set_first_access {
     }
     $cachedkey='';
     my $firstaccess=&get_first_access($type,$symb,$map);
-    if (!$firstaccess) {
+    if ($firstaccess) {
+        &logthis("First access time already set ($firstaccess) when attempting ".
+                 "to set new value (type: $type, extent: $res) for $uname:$udom ".
+                 "in $courseid");
+        return 'already_set';
+    } else {
         my $start = time;
 	my $putres = &put('firstaccesstimes',{"$courseid\0$res"=>$start},
                           $udom,$uname);
@@ -4882,6 +5459,12 @@ sub set_first_access {
                         'course.'.$courseid.'.timerinterval.'.$res => $interval,
                      }
                   );
+            if (($cachedtime) && (abs($start-$cachedtime) < 5)) {
+                $cachedtimes{"$courseid\0$res"} = $start;
+            }
+        } elsif ($putres ne 'refused') {
+            &logthis("Result: $putres when attempting to set first access time ".
+                     "(type: $type, extent: $res) for $uname:$udom in $courseid");
         }
         return $putres;
     }
@@ -6258,7 +6841,7 @@ sub currentdump {
    #
    my %returnhash=();
    #
-   if ($rep eq 'unknown_cmd') { 
+   if ($rep eq "unknown_cmd") { 
        # an old lond will not know currentdump
        # Do a dump and make it look like a currentdump
        my @tmp = &dumpstore($courseid,$sdom,$sname,'.');
@@ -7191,7 +7774,7 @@ sub allowed {
 
     if (defined($env{'allowed.'.$priv})) { return $env{'allowed.'.$priv}; }
 # Free bre access to adm and meta resources
-    if (((($uri=~/^adm\//) && ($uri !~ m{/(?:smppg|bulletinboard|ext\.tool)$})) 
+    if (((($uri=~/^adm\//) && ($uri !~ m{/(?:smppg|bulletinboard)$})) 
 	 || (($uri=~/\.meta$/) && ($uri!~m|^uploaded/|) )) 
 	&& ($priv eq 'bre')) {
 	return 'F';
@@ -7356,8 +7939,34 @@ sub allowed {
 
     if ($env{'user.priv.'.$env{'request.role'}.'.'.$courseuri}
        =~/\Q$priv\E\&([^\:]*)/) {
-        unless (($priv eq 'bro') && (!$ownaccess)) {
-            $thisallowed.=$1;
+        if ($priv eq 'mip') {
+            my $rem = $1;
+            if (($uri ne '') && ($env{'request.course.id'} eq $uri) &&
+                ($env{'course.'.$env{'request.course.id'}.'.internal.courseowner'} eq $env{'user.name'}.':'.$env{'user.domain'})) {
+                my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'};
+                if ($cdom ne '') {
+                    my %passwdconf = &get_passwdconf($cdom);
+                    if (ref($passwdconf{'crsownerchg'}) eq 'HASH') {
+                        if (ref($passwdconf{'crsownerchg'}{'by'}) eq 'ARRAY') {
+                            if (@{$passwdconf{'crsownerchg'}{'by'}}) {
+                                my @inststatuses = split(':',$env{'environment.inststatus'});
+                                unless (@inststatuses) {
+                                    @inststatuses = ('default');
+                                }
+                                foreach my $status (@inststatuses) {
+                                    if (grep(/^\Q$status\E$/,@{$passwdconf{'crsownerchg'}{'by'}})) {
+                                        $thisallowed.=$rem;
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        } else {
+            unless (($priv eq 'bro') && (!$ownaccess)) {
+                $thisallowed.=$1;
+            }
         }
     }
 
@@ -7440,6 +8049,16 @@ sub allowed {
 
     if ($env{'request.course.id'}) {
 
+# If this is modifying password (internal auth) domains must match for user and user's role.
+
+        if ($priv eq 'mip') {
+            if ($env{'user.domain'} eq $env{'request.role.domain'}) {
+                return $thisallowed;
+            } else {
+                return '';
+            }
+        }
+
        $courseprivid=$env{'request.course.id'};
        if ($env{'request.course.sec'}) {
           $courseprivid.='/'.$env{'request.course.sec'};
@@ -7852,8 +8471,7 @@ sub get_commblock_resources {
                             }
                         }
                     }
-                    if ($interval[0] =~ /^(\d+)/) {
-                        my $timelimit = $1;
+                    if ($interval[0] =~ /^\d+$/) {
                         my $first_access;
                         if ($type eq 'resource') {
                             $first_access=&get_first_access($interval[1],$item);
@@ -7863,7 +8481,7 @@ sub get_commblock_resources {
                             $first_access=&get_first_access($interval[1]);
                         }
                         if ($first_access) {
-                            my $timesup = $first_access+$timelimit;
+                            my $timesup = $first_access+$interval[0];
                             if ($timesup > $now) {
                                 my $activeblock;
                                 foreach my $res (@to_test) {
@@ -8612,6 +9230,33 @@ sub auto_validate_class_sec {
     return $response;
 }
 
+sub auto_validate_instclasses {
+    my ($cdom,$cnum,$owners,$classesref) = @_;
+    my ($homeserver,%validations);
+    $homeserver = &homeserver($cnum,$cdom);
+    unless ($homeserver eq 'no_host') {
+        my $ownerlist;
+        if (ref($owners) eq 'ARRAY') {
+            $ownerlist = join(',',@{$owners});
+        } else {
+            $ownerlist = $owners;
+        }
+        if (ref($classesref) eq 'HASH') {
+            my $classes = &freeze_escape($classesref);
+            my $response=&reply('autovalidateinstclasses:'.&escape($ownerlist).
+                                ':'.$cdom.':'.$classes,$homeserver);
+            unless ($response =~ /(con_lost|error|no_such_host|refused)/) {
+                my @items = split(/&/,$response);
+                foreach my $item (@items) {
+                    my ($key,$value) = split('=',$item);
+                    $validations{&unescape($key)} = &thaw_unescape($value);
+                }
+            }
+        }
+    }
+    return %validations;
+}
+
 sub auto_crsreq_update {
     my ($cdom,$cnum,$crstype,$action,$ownername,$ownerdomain,$fullname,$title,
         $code,$accessstart,$accessend,$inbound) = @_;
@@ -9210,7 +9855,22 @@ sub store_coowners {
 sub modifyuserauth {
     my ($udom,$uname,$umode,$upass)=@_;
     my $uhome=&homeserver($uname,$udom);
-    unless (&allowed('mau',$udom)) { return 'refused'; }
+    my $allowed;
+    if (&allowed('mau',$udom)) {
+        $allowed = 1;
+    } elsif (($umode eq 'internal') && ($udom eq $env{'user.domain'}) &&
+             ($env{'request.course.id'}) && (&allowed('mip',$env{'request.course.id'})) &&
+             (!$env{'course.'.$env{'request.course.id'}.'.internal.nopasswdchg'})) {
+        my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'};
+        my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'};
+        if (($cdom ne '') && ($cnum ne '')) {
+            my $is_owner = &is_course_owner($cdom,$cnum);
+            if ($is_owner) {
+                $allowed = 1;
+            }
+        }
+    }
+    unless ($allowed) { return 'refused'; }
     &logthis('Call to modify user authentication '.$udom.', '.$uname.', '.
              $umode.' by '.$env{'user.name'}.' at '.$env{'user.domain'}.
              ' in domain '.$env{'request.role.domain'});  
@@ -9703,13 +10363,25 @@ sub generate_coursenum {
 sub is_course {
     my ($cdom, $cnum) = scalar(@_) == 1 ? 
          ($_[0] =~ /^($match_domain)_($match_courseid)$/)  :  @_;
-
-    return unless $cdom and $cnum;
-
-    my %courses = &courseiddump($cdom, '.', 1, '.', '.', $cnum, undef, undef,
-        '.');
-
-    return unless(exists($courses{$cdom.'_'.$cnum}));
+    return unless (($cdom =~ /^$match_domain$/) && ($cnum =~ /^$match_courseid$/));
+    my $uhome=&homeserver($cnum,$cdom);
+    my $iscourse;
+    if (grep { $_ eq $uhome } current_machine_ids()) {
+        $iscourse = &LONCAPA::Lond::is_course($cdom,$cnum);
+    } else {
+        my $hashid = $cdom.':'.$cnum;
+        ($iscourse,my $cached) = &is_cached_new('iscourse',$hashid);
+        unless (defined($cached)) {
+            my %courses = &courseiddump($cdom, '.', 1, '.', '.',
+                                        $cnum,undef,undef,'.');
+            $iscourse = 0;
+            if (exists($courses{$cdom.'_'.$cnum})) {
+                $iscourse = 1;
+            }
+            &do_cache_new('iscourse',$hashid,$iscourse,3600);
+        }
+    }
+    return unless($iscourse);
     return wantarray ? ($cdom, $cnum) : $cdom.'_'.$cnum;
 }
 
@@ -9891,7 +10563,7 @@ sub files_not_in_path {
     my $filename = $user."savedfiles";
     my @return_files;
     my $path_part;
-    open(IN,'<',LONCAPA::tempdir().$filename);
+    open(IN, '<',LONCAPA::tempdir().$filename);
     while (my $line = <IN>) {
         #ok, I know it's clunky, but I want it to work
         my @paths_and_file = split(m|/|, $line);
@@ -10551,7 +11223,7 @@ sub get_userresdata {
 #  Parameters:
 #     $name      - Course/user name.
 #     $domain    - Name of the domain the user/course is registered on.
-#     $type      - Type of thing $name is (must be 'course' or 'user')
+#     $type      - Type of thing $name is (must be 'course' or 'user'
 #     @which     - Array of names of resources desired.
 #  Returns:
 #     The value of the first reasource in @which that is found in the
@@ -10570,44 +11242,13 @@ sub resdata {
     }
     if (!ref($result)) { return $result; }    
     foreach my $item (@which) {
-        if (ref($item) eq 'ARRAY') {
-	    if (defined($result->{$item->[0]})) {
-	        return [$result->{$item->[0]},$item->[1]];
-	    }
-        }
+	if (defined($result->{$item->[0]})) {
+	    return [$result->{$item->[0]},$item->[1]];
+	}
     }
     return undef;
 }
 
-sub get_domain_ltitools {
-    my ($cdom) = @_;
-    my %ltitools;
-    my ($result,$cached)=&is_cached_new('ltitools',$cdom);
-    if (defined($cached)) {
-        if (ref($result) eq 'HASH') {
-            %ltitools = %{$result};
-        }
-    } else {
-        my %domconfig = &get_dom('configuration',['ltitools'],$cdom);
-        if (ref($domconfig{'ltitools'}) eq 'HASH') {
-            %ltitools = %{$domconfig{'ltitools'}};
-            my %encdomconfig = &get_dom('encconfig',['ltitools'],$cdom);
-            if (ref($encdomconfig{'ltitools'}) eq 'HASH') {
-                foreach my $id (keys(%ltitools)) {
-                    if (ref($encdomconfig{'ltitools'}{$id}) eq 'HASH') {
-                        foreach my $item ('key','secret') {
-                            $ltitools{$id}{$item} = $encdomconfig{'ltitools'}{$id}{$item};
-                        }
-                    }
-                }
-            }
-        }
-        my $cachetime = 24*60*60;
-        &do_cache_new('ltitools',$cdom,\%ltitools,$cachetime);
-    }
-    return %ltitools;
-}
-
 sub get_numsuppfiles {
     my ($cnum,$cdom,$ignorecache)=@_;
     my $hashid=$cnum.':'.$cdom;
@@ -11063,7 +11704,7 @@ sub metadata {
     # if it is a non metadata possible uri return quickly
     if (($uri eq '') || 
 	(($uri =~ m|^/*adm/|) && 
-	     ($uri !~ m|^adm/includes|) && ($uri !~ m{/(smppg|bulletinboard|ext\.tool)$})) ||
+	     ($uri !~ m|^adm/includes|) && ($uri !~ m{/(smppg|bulletinboard)$})) ||
         ($uri =~ m|/$|) || ($uri =~ m|/.meta$|) || ($uri =~ m{^/*uploaded/.+\.sequence$})) {
 	return undef;
     }
@@ -11190,6 +11831,7 @@ sub metadata {
 # Check metadata for imported file to
 # see if it contained response items
 #
+                        my %currmetaentry = %metaentry;
                         my $libresponseorder = &metadata($location,'responseorder');
                         my $origfile;
                         if ($libresponseorder ne '') {
@@ -11209,6 +11851,10 @@ sub metadata {
                                 }
                             }
                         }
+# Do not overwrite contents of %metaentry hash for resource itself with 
+# hash populated for imported library file
+                        %metaentry = %currmetaentry;
+                        undef(%currmetaentry);
                         if ($importmode eq 'problem') {
 # Import as problem/response
                            $unikey=&add_prefix_and_part($prefix,$token->[2]->{'part'});
@@ -11218,7 +11864,7 @@ sub metadata {
 # We need to get the original file and the imported file to get the part order correct
 # Good news: we do not need to worry about nested libraries, since parts cannot be nested
 # Load and inspect original file if we didn't do that already
-                           if ($#origfileimportpartids<0) {
+                           if ($#origfiletagids<0) {
                                undef(%importedrespids);
                                undef(%importedpartids);
                                if ($origfile eq '') {
@@ -11645,18 +12291,16 @@ sub symbverify {
 
     if (tie(%bighash,'GDBM_File',$env{'request.course.fn'}.'.db',
                             &GDBM_READER(),0640)) {
-        my $noclutter;
         if (($thisurl =~ m{^/adm/wrapper/ext/}) || ($thisurl =~ m{^ext/})) {
             $thisurl =~ s/\?.+$//;
             if ($map =~ m{^uploaded/.+\.page$}) {
                 $thisurl =~ s{^(/adm/wrapper|)/ext/}{http://};
                 $thisurl =~ s{^\Qhttp://https://\E}{https://};
-                $noclutter = 1;
             }
         }
         my $ids;
-        if ($noclutter) {
-            $ids=$bighash{'ids_'.$thisurl};
+        if ($map =~ m{^uploaded/.+\.page$}) {
+            $ids=$bighash{'ids_'.&clutter_with_no_wrapper($thisurl)};
         } else {
             $ids=$bighash{'ids_'.&clutter($thisurl)};
         }
@@ -12402,9 +13046,10 @@ sub repcopy_userfile {
     my $request;
     $uri=~s/^\///;
     my $homeserver = &homeserver($cnum,$cdom);
+    my $hostname = &hostname($homeserver);
     my $protocol = $protocol{$homeserver};
     $protocol = 'http' if ($protocol ne 'https');
-    $request=new HTTP::Request('GET',$protocol.'://'.&hostname($homeserver).'/raw/'.$uri);
+    $request=new HTTP::Request('GET',$protocol.'://'.$hostname.'/raw/'.$uri);
     my $response=$ua->request($request,$transferfile);
 # did it work?
     if ($response->is_error()) {
@@ -12428,9 +13073,10 @@ sub tokenwrapper {
 	$file=~s|(\?\.*)*$||;
         &appenv({"userfile.$udom/$uname/$file" => $env{'request.course.id'}});
         my $homeserver = &homeserver($uname,$udom);
+        my $hostname = &hostname($homeserver);
         my $protocol = $protocol{$homeserver};
         $protocol = 'http' if ($protocol ne 'https');
-        return $protocol.'://'.&hostname($homeserver).'/'.$uri.
+        return $protocol.'://'.$hostname.'/'.$uri.
                (($uri=~/\?/)?'&':'?').'token='.$token.
                                '&tokenissued='.$perlvar{'lonHostID'};
     } else {
@@ -12446,9 +13092,10 @@ sub getuploaded {
     my ($reqtype,$uri,$cdom,$cnum,$info,$rtncode) = @_;
     $uri=~s/^\///;
     my $homeserver = &homeserver($cnum,$cdom);
+    my $hostname = &hostname($homeserver);
     my $protocol = $protocol{$homeserver};
     $protocol = 'http' if ($protocol ne 'https');
-    $uri = $protocol.'://'.&hostname($homeserver).'/raw/'.$uri;
+    $uri = $protocol.'://'.$hostname.'/raw/'.$uri;
     my $ua=new LWP::UserAgent;
     my $request=new HTTP::Request($reqtype,$uri);
     my $response=$ua->request($request);
@@ -12602,6 +13249,66 @@ sub default_login_domain {
     return $domain;
 }
 
+sub shared_institution {
+    my ($dom) = @_;
+    my $same_intdom;
+    my $hostintdom = &internet_dom($perlvar{'lonHostID'});
+    if ($hostintdom ne '') {
+        my %iphost = &get_iphost();
+        my $primary_id = &domain($dom,'primary');
+        my $primary_ip = &get_host_ip($primary_id);
+        if (ref($iphost{$primary_ip}) eq 'ARRAY') {
+            foreach my $id (@{$iphost{$primary_ip}}) {
+                my $intdom = &internet_dom($id);
+                if ($intdom eq $hostintdom) {
+                    $same_intdom = 1;
+                    last;
+                }
+            }
+        }
+    }
+    return $same_intdom;
+}
+
+sub uses_sts {
+    my ($ignore_cache) = @_;
+    my $lonhost = $perlvar{'lonHostID'};
+    my $hostname = &hostname($lonhost);
+    my $sts_on;
+    if ($protocol{$lonhost} eq 'https') {
+        my $cachetime = 12*3600;
+        if (!$ignore_cache) {
+            ($sts_on,my $cached)=&is_cached_new('stspolicy',$lonhost);
+            if (defined($cached)) {
+                return $sts_on;
+            }
+        }
+        my $ua=new LWP::UserAgent;
+        my $url = $protocol{$lonhost}.'://'.$hostname.'/index.html';
+        my $request=new HTTP::Request('HEAD',$url);
+        my $response=$ua->request($request);
+        if ($response->is_success) {
+            my $has_sts = $response->header('Strict-Transport-Security');
+            if ($has_sts eq '') {
+                $sts_on = 0;
+            } else {
+                if ($has_sts =~ /\Qmax-age=\E(\d+)/) {
+                    my $maxage = $1;
+                    if ($maxage) {
+                        $sts_on = 1;
+                    } else {
+                        $sts_on = 0;
+                    }
+                } else {
+                    $sts_on = 0;
+                }
+            }
+            return &do_cache_new('stspolicy',$lonhost,$sts_on,$cachetime);
+        }
+    }
+    return;
+}
+
 # ------------------------------------------------------------- Declutters URLs
 
 sub declutter {
@@ -12652,8 +13359,6 @@ sub clutter {
 #		&logthis("Got a blank emb style");
 	    }
 	}
-    } elsif ($thisfn =~ m{^/adm/$match_domain/$match_courseid/\d+/ext\.tool$}) {
-        $thisfn='/adm/wrapper'.$thisfn;
     }
     return $thisfn;
 }
@@ -12727,15 +13432,17 @@ sub get_dns {
     }
 
     my %alldns;
-    open(my $config,"<","$perlvar{'lonTabDir'}/hosts.tab");
-    foreach my $dns (<$config>) {
-	next if ($dns !~ /^\^(\S*)/x);
-        my $line = $1;
-        my ($host,$protocol) = split(/:/,$line);
-        if ($protocol ne 'https') {
-            $protocol = 'http';
+    if (open(my $config,"<","$perlvar{'lonTabDir'}/hosts.tab")) {
+        foreach my $dns (<$config>) {
+	    next if ($dns !~ /^\^(\S*)/x);
+            my $line = $1;
+            my ($host,$protocol) = split(/:/,$line);
+            if ($protocol ne 'https') {
+                $protocol = 'http';
+            }
+	    $alldns{$host} = $protocol;
         }
-	$alldns{$host} = $protocol;
+        close($config);
     }
     while (%alldns) {
 	my ($dns) = sort { $b cmp $a } keys(%alldns);
@@ -12752,12 +13459,12 @@ sub get_dns {
 	&$func(\@content,$hashref);
 	return;
     }
-    close($config);
     my $which = (split('/',$url))[3];
     &logthis("unable to contact DNS defaulting to on disk file dns_$which.tab\n");
-    open($config,"<","$perlvar{'lonTabDir'}/dns_$which.tab");
-    my @content = <$config>;
-    &$func(\@content,$hashref);
+    if (open(my $config,"<","$perlvar{'lonTabDir'}/dns_$which.tab")) {
+        my @content = <$config>;
+        &$func(\@content,$hashref);
+    }
     return;
 }
 
@@ -13369,6 +14076,16 @@ BEGIN {
 
 }
 
+# ------------- set default texengine (domain default overrides this)
+{
+    $deftex = LONCAPA::texengine();
+}
+
+# ------------- set default minimum length for passwords for internal auth users
+{
+    $passwdmin = LONCAPA::passwd_min();
+}
+
 $memcache=new Cache::Memcached({'servers'           => ['127.0.0.1:11211'],
 				'compress_threshold'=> 20_000,
  			        });
@@ -14016,6 +14733,88 @@ Returns:
 
 =back
 
+=head2 Bubblesheet Configuration
+
+=over 4
+
+=item *
+
+get_scantron_config($which)
+
+$which - the name of the configuration to parse from the file.
+
+Parses and returns the bubblesheet configuration line selected as a
+hash of configuration file fields.
+
+
+Returns:
+    If the named configuration is not in the file, an empty
+    hash is returned.
+
+    a hash with the fields
+      name         - internal name for the this configuration setup
+      description  - text to display to operator that describes this config
+      CODElocation - if 0 or the string 'none'
+                          - no CODE exists for this config
+                     if -1 || the string 'letter'
+                          - a CODE exists for this config and is
+                            a string of letters
+                     Unsupported value (but planned for future support)
+                          if a positive integer
+                               - The CODE exists as the first n items from
+                                 the question section of the form
+                          if the string 'number'
+                               - The CODE exists for this config and is
+                                 a string of numbers
+      CODEstart   - (only matter if a CODE exists) column in the line where
+                     the CODE starts
+      CODElength  - length of the CODE
+      IDstart     - column where the student/employee ID starts
+      IDlength    - length of the student/employee ID info
+      Qstart      - column where the information from the bubbled
+                    'questions' start
+      Qlength     - number of columns comprising a single bubble line from
+                    the sheet. (usually either 1 or 10)
+      Qon         - either a single character representing the character used
+                    to signal a bubble was chosen in the positional setup, or
+                    the string 'letter' if the letter of the chosen bubble is
+                    in the final, or 'number' if a number representing the
+                    chosen bubble is in the file (1->A 0->J)
+      Qoff        - the character used to represent that a bubble was
+                    left blank
+      PaperID     - if the scanning process generates a unique number for each
+                    sheet scanned the column that this ID number starts in
+      PaperIDlength - number of columns that comprise the unique ID number
+                      for the sheet of paper
+      FirstName   - column that the first name starts in
+      FirstNameLength - number of columns that the first name spans
+      LastName    - column that the last name starts in
+      LastNameLength - number of columns that the last name spans
+      BubblesPerRow - number of bubbles available in each row used to
+                      bubble an answer. (If not specified, 10 assumed).
+
+
+=item *
+
+get_scantronformat_file($cdom)
+
+$cdom - the course's domain (optional); if not supplied, uses
+domain for current $env{'request.course.id'}.
+
+Returns an array containing lines from the scantron format file for
+the domain of the course.
+
+If a url for a custom.tab file is listed in domain's configuration.db,
+lines are from this file.
+
+Otherwise, if a default.tab has been published in RES space by the
+domainconfig user, lines are from this file.
+
+Otherwise, fall back to getting lines from the legacy file on the
+local server:  /home/httpd/lonTabs/default_scantronformat.tab
+
+=back
+
 =head2 Resource Subroutines
 
 =over 4
@@ -14707,6 +15506,7 @@ userspace, probably shouldn't be called
   formname: same as for userfileupload()
   fname: filename (including subdirectories) for the file
   parser: if 'parse', will parse (html) file to extract references to objects, links etc.
+          if hashref, and context is scantron, will convert csv format to standard format
   allfiles: reference to hash used to store objects found by parser
   codebase: reference to hash used for codebases of java objects found by parser
   thumbwidth: width (pixels) of thumbnail to be created for uploaded image