--- loncom/lonnet/perl/lonnet.pm	2018/07/04 16:58:29	1.1378
+++ loncom/lonnet/perl/lonnet.pm	2019/08/25 02:43:21	1.1416
@@ -1,7 +1,7 @@
 # The LearningOnline Network
 # TCP networking package
 #
-# $Id: lonnet.pm,v 1.1378 2018/07/04 16:58:29 raeburn Exp $
+# $Id: lonnet.pm,v 1.1416 2019/08/25 02:43:21 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -73,13 +73,13 @@ package Apache::lonnet;
 use strict;
 use HTTP::Date;
 use Image::Magick;
-
+use CGI::Cookie;
 
 use Encode;
 
-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,
@@ -101,6 +101,7 @@ use LONCAPA::Configuration;
 use LONCAPA::lonmetadata;
 use LONCAPA::Lond;
 use LONCAPA::LWPReq;
+use LONCAPA::transliterate;
 
 use File::Copy;
 
@@ -184,7 +185,7 @@ sub create_connection {
 				     Type    => SOCK_STREAM,
 				     Timeout => 10);
     return 0 if (!$client);
-    print $client (join(':',$hostname,$lonid,&machine_ids($hostname))."\n");
+    print $client (join(':',$hostname,$lonid,&machine_ids($hostname),$loncaparevs{$lonid})."\n");
     my $result = <$client>;
     chomp($result);
     return 1 if ($result eq 'done');
@@ -230,12 +231,19 @@ sub get_server_distarch {
 }
 
 sub get_servercerts_info {
-    my ($lonhost,$context) = @_;
+    my ($lonhost,$hostname,$context) = @_;
+    return if ($lonhost eq '');
+    if ($hostname eq '') {
+        $hostname = &hostname($lonhost);
+    }
+    return if ($hostname eq '');
     my ($rep,$uselocal);
-    if (grep { $_ eq $lonhost } &current_machine_ids()) {
+    if ($context eq 'install') {
+        $uselocal = 1;
+    } elsif (grep { $_ eq $lonhost } &current_machine_ids()) {
         $uselocal = 1;
     }
-    if (($context ne 'cgi') && ($uselocal)) {
+    if (($context ne 'cgi') && ($context ne 'install') && ($uselocal)) {
         my $distro = (split(/\:/,&get_server_distarch($lonhost)))[0];
         if ($distro eq '') {
             $uselocal = 0;
@@ -250,16 +258,11 @@ sub get_servercerts_info {
         }
     }
     if ($uselocal) {
-        $rep = LONCAPA::Lond::server_certs(\%perlvar);
+        $rep = LONCAPA::Lond::server_certs(\%perlvar,$lonhost,$hostname);
     } else {
         $rep=&reply('servercerts',$lonhost);
     }
     my ($result,%returnhash);
-    if (defined($lonhost)) {
-        if (!defined(&hostname($lonhost))) {
-            return;
-        }
-    }
     if (($rep=~/^(refused|rejected|error)/) || ($rep eq 'con_lost') ||
         ($rep eq 'unknown_cmd')) {
         $result = $rep;
@@ -309,9 +312,10 @@ sub get_server_loncaparev {
             $answer = &reply('serverloncaparev',$lonhost);
             if (($answer eq 'unknown_cmd') || ($answer eq 'con_lost')) {
                 if ($caller eq 'loncron') {
+                    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=&LONCAPA::LWPReq::makerequest($lonhost,$request,'',\%perlvar,4,1);
                     unless ($response->is_error()) {
@@ -456,8 +460,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;
 }
@@ -669,7 +691,7 @@ sub check_for_valid_session {
             $lonid=$cookies{$secure};
         } elsif (exists($cookies{$name})) {
             $lonid=$cookies{$name};
-        } elsif (exists($cookies{$linkname})) {
+        } elsif ((exists($cookies{$linkname})) && ($ENV{'SERVER_PORT'} != 443)) {
             $lonid=$cookies{$linkname};
         } elsif (exists($cookies{$pubname})) {
             $lonid=$cookies{$pubname};
@@ -709,6 +731,7 @@ sub check_for_valid_session {
 
     if (!defined($disk_env{'user.name'})
 	|| !defined($disk_env{'user.domain'})) {
+        untie(%disk_env);
 	return undef;
     }
 
@@ -721,6 +744,7 @@ sub check_for_valid_session {
             $userhashref->{'ltiuri'} = $disk_env{'request.lti.uri'};
         }
     }
+    untie(%disk_env);
 
     return $handle;
 }
@@ -745,6 +769,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 {
@@ -898,6 +953,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++; }
 	}
@@ -953,13 +1009,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;
             }
         }
@@ -1026,6 +1082,88 @@ sub find_existing_session {
     return;
 }
 
+sub delusersession {
+    my ($lonid,$udom,$uname) = @_;
+    my $uprimary_id = &domain($udom,'primary');
+    my $uintdom = &internet_dom($uprimary_id);
+    my $intdom = &internet_dom($lonid);
+    my $serverhomedom = &host_domain($lonid);
+    if (($uintdom ne '') && ($uintdom eq $intdom)) {
+        return &reply(join(':','delusersession',
+                            map {&escape($_)} ($udom,$uname)),$lonid);
+    }
+    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) = @_;
@@ -1061,7 +1199,7 @@ sub choose_server {
             if (ref($balancers) eq 'HASH') {
                 next if (exists($balancers->{$lonhost}));
             }
-        }   
+        }
         my $loginvia;
         if ($checkloginvia) {
             $loginvia = $domconfhash{$udom.'.login.loginvia_'.$lonhost};
@@ -1127,6 +1265,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;
 }
@@ -1363,7 +1504,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,$dom_balancers);
     my $lonhost = $perlvar{'lonHostID'};
     my @hosts = &current_machine_ids();
     my $uprimary_id = &Apache::lonnet::domain($udom,'primary');
@@ -1390,7 +1531,7 @@ sub check_loadbalancing {
         }
     }
     if (ref($result) eq 'HASH') {
-        ($is_balancer,$currtargets,$currrules) = 
+        ($is_balancer,$currtargets,$currrules,$setcookie,$dom_balancers) =
             &check_balancer_result($result,@hosts);
         if ($is_balancer) {
             if (ref($currrules) eq 'HASH') {
@@ -1451,7 +1592,7 @@ sub check_loadbalancing {
             }
         }
         if (ref($result) eq 'HASH') {
-            ($is_balancer,$currtargets,$currrules) = 
+            ($is_balancer,$currtargets,$currrules,$setcookie,$dom_balancers) =
                 &check_balancer_result($result,@hosts);
             if ($is_balancer) {
                 if (ref($currrules) eq 'HASH') {
@@ -1517,20 +1658,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,  
+                        &appenv({'user.loadbalexempt'     => $lonhost,
                                  'user.loadbalcheck.time' => time});
                     }
                 }
             }
         }
+        unless ($homeintdom) {
+            undef($setcookie);
+        }
     }
-    return ($is_balancer,$otherserver);
+    return ($is_balancer,$otherserver,$setcookie,$offloadto,$dom_balancers);
 }
 
 sub check_balancer_result {
     my ($result,@hosts) = @_;
-    my ($is_balancer,$currtargets,$currrules);
+    my ($is_balancer,$currtargets,$currrules,$setcookie,$dom_balancers);
     if (ref($result) eq 'HASH') {
         if ($result->{'lonhost'} ne '') {
             my $currbalancer = $result->{'lonhost'};
@@ -1539,19 +1682,24 @@ sub check_balancer_result {
                 $currtargets = $result->{'targets'};
                 $currrules = $result->{'rules'};
             }
+            $dom_balancers = $currbalancer;
         } else {
-            foreach my $key (keys(%{$result})) {
-                if (($key ne '') && (grep(/^\Q$key\E$/,@hosts)) &&
-                    (ref($result->{$key}) eq 'HASH')) {
-                    $is_balancer = 1;
-                    $currrules = $result->{$key}{'rules'};
-                    $currtargets = $result->{$key}{'targets'};
-                    last;
+            if (keys(%{$result})) {
+                foreach my $key (keys(%{$result})) {
+                    if (($key ne '') && (grep(/^\Q$key\E$/,@hosts)) &&
+                        (ref($result->{$key}) eq 'HASH')) {
+                        $is_balancer = 1;
+                        $currrules = $result->{$key}{'rules'};
+                        $currtargets = $result->{$key}{'targets'};
+                        $setcookie = $result->{$key}{'cookie'};
+                        last;
+                    }
                 }
+                $dom_balancers = join(',',sort(keys(%{$result})));
             }
         }
     }
-    return ($is_balancer,$currtargets,$currrules);
+    return ($is_balancer,$currtargets,$currrules,$setcookie,$dom_balancers);
 }
 
 sub get_loadbalancer_targets {
@@ -1629,7 +1777,7 @@ sub trusted_domains {
     if (&domain($calldom) eq '') {
         return ($trusted,$untrusted);
     }
-    unless ($cmdtype =~ /^(content|shared|enroll|coaurem|domroles|catalog|reqcrs|msg)$/) {
+    unless ($cmdtype =~ /^(content|shared|enroll|coaurem|othcoau|domroles|catalog|reqcrs|msg)$/) {
         return ($trusted,$untrusted);
     }
     my $callprimary = &domain($calldom,'primary');
@@ -1651,6 +1799,7 @@ sub trusted_domains {
                 map { $possexc{$_} = 1; } @{$trustconfig->{$cmdtype}->{'exc'}}; 
             }
             if (ref($trustconfig->{$cmdtype}->{'inc'}) eq 'ARRAY') {
+                $possinc{$intcalldom} = 1;
                 map { $possinc{$_} = 1; } @{$trustconfig->{$cmdtype}->{'inc'}};
             }
         }
@@ -1685,12 +1834,12 @@ sub trusted_domains {
             }
             foreach my $exc (@allexc) {
                 if (ref($doms_by_intdom{$exc}) eq 'ARRAY') {
-                    $untrusted = $doms_by_intdom{$exc};
+                    push(@{$untrusted},@{$doms_by_intdom{$exc}});
                 }
             }
             foreach my $inc (@allinc) {
                 if (ref($doms_by_intdom{$inc}) eq 'ARRAY') {
-                    $trusted = $doms_by_intdom{$inc};
+                    push(@{$trusted},@{$doms_by_intdom{$inc}});
                 }
             }
         }
@@ -2533,7 +2682,7 @@ sub get_domain_defaults {
     if (ref($domconfig{'coursecategories'}) eq 'HASH') {
         $domdefaults{'catauth'} = 'std';
         $domdefaults{'catunauth'} = 'std';
-        if ($domconfig{'coursecategories'}{'auth'}) { 
+        if ($domconfig{'coursecategories'}{'auth'}) {
             $domdefaults{'catauth'} = $domconfig{'coursecategories'}{'auth'};
         }
         if ($domconfig{'coursecategories'}{'unauth'}) {
@@ -2572,6 +2721,65 @@ sub get_domain_defaults {
     return %domdefaults;
 }
 
+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;
+}
+
 sub course_portal_url {
     my ($cnum,$cdom) = @_;
     my $chome = &homeserver($cnum,$cdom);
@@ -2588,6 +2796,29 @@ sub course_portal_url {
     return $firsturl;
 }
 
+# --------------------------------------------- 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 %passwdconf;
+}
+
 # --------------------------------------------------- Assign a key to a student
 
 sub assign_access_key {
@@ -3197,7 +3428,17 @@ sub ssi {
 
     $request->header(Cookie => $ENV{'HTTP_COOKIE'});
     my $lonhost = $perlvar{'lonHostID'};
-    my $response= &LONCAPA::LWPReq::makerequest($lonhost,$request,'',\%perlvar);
+    my $islocal;
+    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'}:'')))) {
+        $islocal = 1;
+    }
+    my $response= &LONCAPA::LWPReq::makerequest($lonhost,$request,'',\%perlvar,
+                                                '','','',$islocal);
 
     if (wantarray) {
 	return ($response->content, $response);
@@ -3235,10 +3476,10 @@ sub remove_stale_resfile {
                     (grep { $_ eq $homeserver } &current_machine_ids())) {
                 my $fname = &filelocation('',$url);
                 if (-e $fname) {
-                    my $protocol = $protocol{$homeserver};
-                    $protocol = 'http' if ($protocol ne 'https');
                     my $hostname = &hostname($homeserver);
                     if ($hostname) {
+                        my $protocol = $protocol{$homeserver};
+                        $protocol = 'http' if ($protocol ne 'https');
                         my $uri = &declutter($url);
                         my $request=new HTTP::Request('HEAD',$protocol.'://'.$hostname.'/raw/'.$uri);
                         my $response = &LONCAPA::LWPReq::makerequest($homeserver,$request,'',\%perlvar,5,0,1);
@@ -3714,6 +3955,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
@@ -3721,6 +3965,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 
@@ -3763,13 +4008,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
@@ -3792,6 +4040,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')) ||
@@ -3959,7 +4215,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);
@@ -3968,6 +4224,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;
@@ -4208,6 +4467,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);    
@@ -5231,7 +5730,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);
@@ -5247,6 +5751,9 @@ sub set_first_access {
             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;
     }
@@ -6387,7 +6894,8 @@ sub set_adhoc_privileges {
     my ($author,$adv,$rar)= &set_userprivs(\%userroles,\%rolehash);
     &appenv(\%userroles,[$role,'cm']);
     &log($env{'user.domain'},$env{'user.name'},$env{'user.home'},"Role ".$spec);
-    unless ($caller eq 'constructaccess' && $env{'request.course.id'}) {
+    unless (($caller eq 'constructaccess' && $env{'request.course.id'}) ||
+            ($caller eq 'tiny')) {
         &appenv( {'request.role'        => $spec,
                   'request.role.domain' => $dcdom,
                   'request.course.sec'  => $sec,
@@ -7647,8 +8155,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;
+            }
         }
     }
 
@@ -7661,7 +8195,10 @@ sub allowed {
             if ($env{'user.priv.'.$env{'request.role'}.'./'}
                   =~/\Q$priv\E\&([^\:]*)/) {
                 my $value = $1;
-                if ($noblockcheck) {
+                my $deeplinkblock = &deeplink_check($priv,$symb,$uri);
+                if ($deeplinkblock) {
+                    $thisallowed='D';
+                } elsif ($noblockcheck) {
                     $thisallowed.=$value;
                 } else {
                     my @blockers = &has_comm_blocking($priv,$symb,$uri);
@@ -7681,7 +8218,10 @@ sub allowed {
                     $refuri=&declutter($refuri);
                     my ($match) = &is_on_map($refuri);
                     if ($match) {
-                        if ($noblockcheck) {
+                        my $deeplinkblock = &deeplink_check($priv,$symb,$refuri);
+                        if ($deeplinkblock) {
+                            $thisallowed='D';
+                        } elsif ($noblockcheck) {
                             $thisallowed='F';
                         } else {
                             my @blockers = &has_comm_blocking($priv,$symb,$refuri);
@@ -7731,6 +8271,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'};
@@ -7786,7 +8336,10 @@ sub allowed {
                   =~/\Q$priv\E\&([^\:]*)/) {
                   my $value = $1;
                   if ($priv eq 'bre') {
-                      if ($noblockcheck) {
+                      my $deeplinkblock = &deeplink_check($priv,$symb,$refuri);
+                      if ($deeplinkblock) {
+                          $thisallowed = 'D';
+                      } elsif ($noblockcheck) {
                           $thisallowed.=$value;
                       } else {
                           my @blockers = &has_comm_blocking($priv,$symb,$refuri);
@@ -7954,6 +8507,8 @@ sub allowed {
 	return 'A';
     } elsif ($thisallowed eq 'B') {
         return 'B';
+    } elsif ($thisallowed eq 'D') {
+        return 'D';
     }
    return 'F';
 }
@@ -8247,6 +8802,93 @@ sub has_comm_blocking {
 }
 }
 
+sub deeplink_check {
+    my ($priv,$symb,$uri) = @_;
+    return unless ($env{'request.course.id'});
+    return unless ($priv eq 'bre');
+    return if ($env{'request.state'} eq 'construct');
+    return if ($env{'request.role.adv'});
+    my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'};
+    my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'};
+    my (%possibles,@symbs);
+    if (!$symb) {
+        $symb = &symbread($uri,1,1,1,\%possibles);
+    }
+    if ($symb) {
+        @symbs = ($symb);
+    } elsif (keys(%possibles)) {
+        @symbs = keys(%possibles);
+    }
+
+    my ($login,$switchrole,$allow);
+    if ($env{'request.deeplink.login'} =~ m{^\Q/tiny/$cdom/\E(\w+)$}) {
+        my $key = $1;
+        my $tinyurl;
+        my ($result,$cached)=&Apache::lonnet::is_cached_new('tiny',$cdom."\0".$key);
+        if (defined($cached)) {
+             $tinyurl = $result;
+        } else {
+             my $configuname = &Apache::lonnet::get_domainconfiguser($cdom);
+             my %currtiny = &Apache::lonnet::get('tiny',[$key],$cdom,$configuname);
+             if ($currtiny{$key} ne '') {
+                 $tinyurl = $currtiny{$key};
+                 &Apache::lonnet::do_cache_new('tiny',$cdom."\0".$key,$currtiny{$key},600);
+             }
+        }
+        if ($tinyurl ne '') {
+            my ($cnumreq,$posslogin) = split(/\&/,$tinyurl);
+            if ($cnumreq eq $cnum) {
+                $login = $posslogin;
+            } else {
+                $switchrole = 1;
+            }
+        }
+    }
+    foreach my $symb (@symbs) {
+        last if ($allow);
+        my $deeplink = &EXT("resource.0.deeplink",$symb);
+        if ($deeplink eq '') {
+            $allow = 1;
+        } else {
+            my ($listed,$scope,$access) = split(/,/,$deeplink);
+            if ($access eq 'any') {
+                $allow = 1;
+            } elsif ($login) {
+                if ($access eq 'only') {
+                    if ($scope eq 'res') {
+                        if ($symb eq $login) {
+                            $allow = 1;
+                        }
+                    } elsif ($scope eq 'map') {
+#FIXME Compare map for $env{'request.deeplink.login'} with map for $symb
+                    } elsif ($scope eq 'rec') {
+#FIXME Recurse up for $env{'request.deeplink.login'} with map for $symb
+                    }
+                } else {
+                    my ($acctype,$item) = split(/:/,$access);
+                    if (($acctype eq 'lti') && ($env{'user.linkprotector'})) {
+                        if (grep(/^\Q$item\E$/,split(/,/,$env{'user.linkprotector'}))) {
+                            my %tinyurls = &get('tiny',[$symb],$cdom,$cnum);
+                            if (grep(/\Q$tinyurls{$symb}\E$/,split(/,/,$env{'user.linkproturis'}))) {
+                                $allow = 1;
+                            }
+                        }
+                    } elsif (($acctype eq 'key') && ($env{'user.deeplinkkey'})) {
+                        if (grep(/^\Q$item\E$/,split(/,/,$env{'user.deeplinkkey'}))) {
+                            my %tinyurls = &get('tiny',[$symb],$cdom,$cnum);
+                            if (grep(/\Q$tinyurls{$symb}\E$/,split(/,/,$env{'user.keyedlinkuri'}))) {
+                                $allow = 1;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+    return if ($allow);
+    return 1;
+}
+
 # -------------------------------- Deversion and split uri into path an filename   
 
 #
@@ -9543,7 +10185,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'});  
@@ -10041,12 +10698,25 @@ 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;
 }
 
@@ -11610,6 +12280,10 @@ sub EXT {
 	if ($space eq 'name') {
 	    return $ENV{'SERVER_NAME'};
         }
+    } elsif ($realm eq 'client') {
+        if ($space eq 'remote_addr') {
+            return $ENV{'REMOTE_ADDR'};
+        }
     }
     return '';
 }
@@ -13195,9 +13869,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 = &LONCAPA::LWPReq::makerequest($homeserver,$request,$transferfile,\%perlvar,'',0,1);
 # did it work?
     if ($response->is_error()) {
@@ -13221,9 +13896,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 {
@@ -13239,9 +13915,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 $request=new HTTP::Request($reqtype,$uri);
     my $response=&LONCAPA::LWPReq::makerequest($homeserver,$request,'',\%perlvar,'',0,1);
     $$rtncode = $response->code;
@@ -13394,6 +14071,65 @@ 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 $url = $protocol{$lonhost}.'://'.$hostname.'/index.html';
+        my $request=new HTTP::Request('HEAD',$url);
+        my $response=&LONCAPA::LWPReq::makerequest($lonhost,$request,'',\%perlvar,'','','',1);
+        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 {
@@ -13519,15 +14255,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);
@@ -13535,19 +14273,33 @@ sub get_dns {
         my $response = &LONCAPA::LWPReq::makerequest('',$request,'',\%perlvar,30,0);
         delete($alldns{$dns});
 	next if ($response->is_error());
-	my @content = split("\n",$response->content);
-	unless ($nocache) {
-	    &do_cache_new('dns',$url,\@content,30*24*60*60);
-	}
-	&$func(\@content,$hashref);
-	return;
+        if ($url eq '/adm/dns/loncapaCRL') {
+            return &$func($response);
+        } else {
+	    my @content = split("\n",$response->content);
+	    unless ($nocache) {
+	        &do_cache_new('dns',$url,\@content,30*24*60*60);
+	    }
+	    &$func(\@content,$hashref);
+            return;
+        }
+    }
+    my $which = (split('/',$url,4))[3];
+    if ($which eq 'loncapaCRL') {
+        my $diskfile = "$perlvar{'lonCertificateDirectory'}/$perlvar{'lonnetCertRevocationList'}";
+        if (-e $diskfile) {
+            &logthis("unable to contact DNS, on disk file $diskfile not updated");
+        } else {
+            &logthis("unable to contact DNS, no on disk file $diskfile available");
+        }
+    } else {
+        &logthis("unable to contact DNS defaulting to on disk file dns_$which.tab\n");
+        if (open(my $config,"<","$perlvar{'lonTabDir'}/dns_$which.tab")) {
+            my @content = <$config>;
+            close($config);
+            &$func(\@content,$hashref);
+        }
     }
-    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);
     return;
 }
 
@@ -13607,6 +14359,79 @@ sub fetch_dns_checksums {
     return \%checksums;
 }
 
+sub fetch_crl_pemfile {
+    return &get_dns("/adm/dns/loncapaCRL",\&save_crl_pem,1,1);
+}
+
+sub save_crl_pem {
+    my ($response) = @_;
+    my ($msg,$hadchanges);
+    if (ref($response)) {
+        my $now = time;
+        my $lonca = $perlvar{'lonCertificateDirectory'}.'/'.$perlvar{'lonnetCertificateAuthority'};
+        my $tmpcrl = $tmpdir.'/'.$perlvar{'lonnetCertRevocationList'}.'_'.$now.'.'.$$.'.tmp';
+        if (open(my $fh,'>',"$tmpcrl")) {
+            print $fh $response->content;
+            close($fh);
+            if (-e $lonca) {
+                if (open(PIPE,"openssl crl -in $tmpcrl -inform pem -CAfile $lonca -noout 2>&1 |")) {
+                    my $check = <PIPE>;
+                    close(PIPE);
+                    chomp($check);
+                    if ($check eq 'verify OK') {
+                        my $dest = "$perlvar{'lonCertificateDirectory'}/$perlvar{'lonnetCertRevocationList'}";
+                        my $backup;
+                        if (-e $dest) {
+                            if (&File::Copy::move($dest,"$dest.bak")) {
+                                $backup = 'ok';
+                            }
+                        }
+                        if (&File::Copy::move($tmpcrl,$dest)) {
+                            $msg = 'ok';
+                            if ($backup) {
+                                my (%oldnums,%newnums);
+                                if (open(PIPE, "openssl crl -inform PEM -text -noout -in $dest.bak |grep 'Serial Number' |")) {
+                                    while (<PIPE>) {
+                                        $oldnums{(split(/:/))[1]} = 1;
+                                    }
+                                    close(PIPE);
+                                }
+                                if (open(PIPE, "openssl crl -inform PEM -text -noout -in $dest |grep 'Serial Number' |")) {
+                                    while(<PIPE>) {
+                                        $newnums{(split(/:/))[1]} = 1;
+                                    }
+                                    close(PIPE);
+                                }
+                                foreach my $key (sort {$b <=> $a } (keys(%newnums))) {
+                                    unless (exists($oldnums{$key})) {
+                                        $hadchanges = 1;
+                                        last;
+                                    }
+                                }
+                                unless ($hadchanges) {
+                                    foreach my $key (sort {$b <=> $a } (keys(%oldnums))) {
+                                        unless (exists($newnums{$key})) {
+                                            $hadchanges = 1;
+                                            last;
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    } else {
+                        unlink($tmpcrl);
+                    }
+                } else {
+                    unlink($tmpcrl);
+                }
+            } else {
+                unlink($tmpcrl);
+            }
+        }
+    }
+    return ($msg,$hadchanges);
+}
+
 # ------------------------------------------------------------ Read domain file
 {
     my $loaded;
@@ -14173,6 +14998,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,
  			        });
@@ -14512,6 +15347,7 @@ prevents recursive calls to &allowed.
  2: browse allowed
  A: passphrase authentication needed
  B: access temporarily blocked because of a blocking event in a course.
+ D: access blocked because access is required via session initiated via deep-link 
 
 =item *
 
@@ -14842,6 +15678,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
@@ -15539,6 +16457,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