--- loncom/lonnet/perl/lonnet.pm	2022/02/07 12:09:33	1.1172.2.146.2.3
+++ loncom/lonnet/perl/lonnet.pm	2025/01/15 18:43:08	1.1172.2.146.2.26
@@ -1,7 +1,7 @@
 # The LearningOnline Network
 # TCP networking package
 #
-# $Id: lonnet.pm,v 1.1172.2.146.2.3 2022/02/07 12:09:33 raeburn Exp $
+# $Id: lonnet.pm,v 1.1172.2.146.2.26 2025/01/15 18:43:08 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -221,7 +221,7 @@ sub get_server_distarch {
             }
         }
         my $rep = &reply('serverdistarch',$lonhost);
-        unless ($rep eq 'unknown_command' || $rep eq 'no_such_host' ||
+        unless ($rep eq 'unknown_cmd' || $rep eq 'no_such_host' ||
                 $rep eq 'con_lost' || $rep eq 'rejected' || $rep eq 'refused' ||
                 $rep eq '') {
             return &do_cache_new('serverdistarch',$lonhost,$rep,$cachetime);
@@ -365,6 +365,63 @@ sub remote_devalidate_cache {
     return &reply('devalidatecache:'.&escape($cachestr),$lonhost);
 }
 
+sub sign_lti {
+    my ($cdom,$cnum,$crsdef,$type,$context,$url,$ltinum,$keynum,$paramsref,$inforef) = @_;
+    my $chome;
+    if (&domain($cdom) ne '') {
+        if ($crsdef) {
+            $chome = &homeserver($cnum,$cdom);
+        } else {
+            $chome = &domain($cdom,'primary');
+        }
+    }
+    if ($cdom && $chome && ($chome ne 'no_host')) {
+        if ((ref($paramsref) eq 'HASH') &&
+            (ref($inforef) eq 'HASH')) {
+            my $rep;
+            if (grep { $_ eq $chome } &current_machine_ids()) {
+                # domain information is hosted on this machine
+                $rep =
+                    &LONCAPA::Lond::sign_lti_payload($cdom,$cnum,$crsdef,$type,
+                                                     $context,$url,$ltinum,$keynum,
+                                                     $perlvar{'lonVersion'},
+                                                     $paramsref,$inforef);
+                if (ref($rep) eq 'HASH') {
+                    return ('ok',$rep);
+                }
+            } else {
+                my ($escurl,$params,$info);
+                $escurl = &escape($url);
+                if (ref($paramsref) eq 'HASH') {
+                    $params = &freeze_escape($paramsref);
+                }
+                if (ref($inforef) eq 'HASH') {
+                    $info = &freeze_escape($inforef);
+                }
+                $rep=&reply("encrypt:signlti:$cdom:$cnum:$crsdef:$type:$context:$escurl:$ltinum:$keynum:$params:$info",$chome);
+            }
+            if (($rep eq '') || ($rep =~ /^con_lost|error|no_such_host|unknown_cmd/i)) {
+                return ();
+            } elsif (($inforef->{'respfmt'} eq 'to_post_body') ||
+                     ($inforef->{'respfmt'} eq 'to_authorization_header')) {
+                return ('ok',$rep);
+            } else {
+                my %returnhash;
+                foreach my $item (split(/\&/,$rep)) {
+                    my ($name,$value)=split(/\=/,$item);
+                    $returnhash{&unescape($name)}=&thaw_unescape($value);
+                }
+                return('ok',\%returnhash);
+            }
+        } else {
+            return ();
+        }
+    } else {
+        return ();
+        &logthis("sign_lti failed - no homeserver and/or domain ($cdom) ($chome)");
+    }
+}
+
 # -------------------------------------------------- Non-critical communication
 sub subreply {
     my ($cmd,$server)=@_;
@@ -418,14 +475,15 @@ sub reply {
             my $subcmd = $1;
             if (($subcmd eq 'auth') || ($subcmd eq 'passwd') ||
                 ($subcmd eq 'changeuserauth') || ($subcmd eq 'makeuser') ||
-                ($subcmd eq 'putdom') || ($subcmd eq 'autoexportgrades')) {
+                ($subcmd eq 'putdom') || ($subcmd eq 'autoexportgrades') ||
+                ($subcmd eq 'put')) {
                 (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')) {
+                         ($subcmd eq 'autoexportgrades') || ($subcmd eq 'put')) {
                     splice(@rest,3,1,'Hidden');
                 }
                 $logged = join(':',('encrypt:'.$subcmd,@rest));
@@ -694,6 +752,11 @@ sub check_for_valid_session {
         if ($disk_env{'request.role'}) {
             $userhashref->{'role'} = $disk_env{'request.role'};
         }
+        $userhashref->{'lti'} = $disk_env{'request.lti.login'};
+        if ($userhashref->{'lti'}) {
+            $userhashref->{'ltitarget'} = $disk_env{'request.lti.target'};
+            $userhashref->{'ltiuri'} = $disk_env{'request.lti.uri'};
+        }
     }
     untie(%disk_env);
 
@@ -1035,6 +1098,20 @@ 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 {
@@ -1254,7 +1331,7 @@ sub changepass {
 sub queryauthenticate {
     my ($uname,$udom)=@_;
     my $uhome=&homeserver($uname,$udom);
-    if (!$uhome) {
+    if ((!$uhome) || ($uhome eq 'no_host')) {
 	&logthis("User $uname at $udom is unknown when looking for authentication mechanism");
 	return 'no_host';
     }
@@ -1303,12 +1380,35 @@ sub authenticate {
     }
     if ($answer eq 'non_authorized') {
 	&logthis("User $uname at $udom rejected by $uhome");
-	return 'no_host'; 
+	return 'no_host';
     }
     &logthis("User $uname at $udom threw error $answer when checking authentication mechanism");
     return 'no_host';
 }
 
+sub can_switchserver {
+    my ($udom,$home) = @_;
+    my ($canswitch,@intdoms);
+    my $internet_names = &get_internet_names($home);
+    if (ref($internet_names) eq 'ARRAY') {
+        @intdoms = @{$internet_names};
+    }
+    my $uint_dom = &internet_dom(&domain($udom,'primary'));
+    if ($uint_dom ne '' && grep(/^\Q$uint_dom\E$/,@intdoms)) {
+        $canswitch = 1;
+    } else {
+         my $serverhomeID = &get_server_homeID(&hostname($home));
+         my $serverhomedom = &host_domain($serverhomeID);
+         my %defdomdefaults = &get_domain_defaults($serverhomedom);
+         my %udomdefaults = &get_domain_defaults($udom);
+         my $remoterev = &get_server_loncaparev('',$home);
+         $canswitch = &can_host_session($udom,$home,$remoterev,
+                                        $udomdefaults{'remotesessions'},
+                                        $defdomdefaults{'hostedsessions'});
+    }
+    return $canswitch;
+}
+
 sub can_host_session {
     my ($udom,$lonhost,$remoterev,$remotesessions,$hostedsessions) = @_;
     my $canhost = 1;
@@ -1997,6 +2097,57 @@ sub del_dom {
     }
 }
 
+sub store_dom {
+    my ($storehash,$id,$namespace,$dom,$home,$encrypt) = @_;
+    $$storehash{'ip'}=&get_requestor_ip();
+    $$storehash{'host'}=$perlvar{'lonHostID'};
+    my $namevalue='';
+    foreach my $key (keys(%{$storehash})) {
+        $namevalue.=&escape($key).'='.&freeze_escape($$storehash{$key}).'&';
+    }
+    $namevalue=~s/\&$//;
+    if (grep { $_ eq $home } current_machine_ids()) {
+        return LONCAPA::Lond::store_dom("storedom:$dom:$namespace:$id:$namevalue");
+    } else {
+        if ($namespace eq 'private') {
+            return 'refused';
+        } elsif ($encrypt) {
+            return reply("encrypt:storedom:$dom:$namespace:$id:$namevalue",$home);
+        } else {
+            return reply("storedom:$dom:$namespace:$id:$namevalue",$home);
+        }
+    }
+}
+
+sub restore_dom {
+    my ($id,$namespace,$dom,$home,$encrypt) = @_;
+    my $answer;
+    if (grep { $_ eq $home } current_machine_ids()) {
+        $answer = LONCAPA::Lond::restore_dom("restoredom:$dom:$namespace:$id");
+    } elsif ($namespace ne 'private') {
+        if ($encrypt) {
+            $answer=&reply("encrypt:restoredom:$dom:$namespace:$id",$home);
+        } else {
+            $answer=&reply("restoredom:$dom:$namespace:$id",$home);
+        }
+    }
+    my %returnhash=();
+    unless (($answer eq '') || ($answer eq 'con_lost') || ($answer eq 'refused') ||
+            ($answer eq 'unknown_cmd') || ($answer eq 'rejected')) {
+        foreach my $line (split(/\&/,$answer)) {
+            my ($name,$value)=split(/\=/,$line);
+            $returnhash{&unescape($name)}=&thaw_unescape($value);
+        }
+        my $version;
+        for ($version=1;$version<=$returnhash{'version'};$version++) {
+            foreach my $item (split(/\:/,$returnhash{$version.':keys'})) {
+                $returnhash{$item}=$returnhash{$version.':'.$item};
+            }
+        }
+    }
+    return %returnhash;
+}
+
 # ----------------------------------construct domainconfig user for a domain 
 sub get_domainconfiguser {
     my ($udom) = @_;
@@ -2275,6 +2426,10 @@ sub inst_rulecheck {
                     $response=&unescape(&reply('instidrulecheck:'.&escape($udom).
                                               ':'.&escape($id).':'.$rulestr,
                                               $homeserver));
+                } elsif ($item eq 'unamemap') {
+                    $response=&unescape(&reply('instunamemapcheck:'.
+                                               &escape($udom).':'.&escape($uname).
+                                              ':'.$rulestr,$homeserver));
                 } elsif ($item eq 'selfcreate') {
                     $response=&unescape(&reply('instselfcreatecheck:'.
                                                &escape($udom).':'.&escape($uname).
@@ -2308,6 +2463,9 @@ sub inst_userrules {
             } elsif ($check eq 'email') {
                 $response=&reply('instemailrules:'.&escape($udom),
                                  $homeserver);
+            } elsif ($check eq 'unamemap') {
+                $response=&reply('unamemaprules:'.&escape($udom),
+                                 $homeserver);
             } else {
                 $response=&reply('instuserrules:'.&escape($udom),
                                  $homeserver);
@@ -2352,9 +2510,11 @@ sub get_domain_defaults {
          &Apache::lonnet::get_dom('configuration',['defaults','quotas',
                                   'requestcourses','inststatus',
                                   'coursedefaults','usersessions',
-                                  'requestauthor','selfenrollment',
-                                  'coursecategories','autoenroll',
-                                  'helpsettings','wafproxy'],$domain);
+                                  'requestauthor','authordefaults',
+                                  'selfenrollment','coursecategories',
+                                  'autoenroll','helpsettings',
+                                  'wafproxy','ltisec','toolsec',
+                                  'domexttool','exttool'],$domain);
     my @coursetypes = ('official','unofficial','community','textbook');
     if (ref($domconfig{'defaults'}) eq 'HASH') {
         $domdefaults{'lang_def'} = $domconfig{'defaults'}{'lang_def'}; 
@@ -2363,9 +2523,12 @@ sub get_domain_defaults {
         $domdefaults{'timezone_def'} = $domconfig{'defaults'}{'timezone_def'};
         $domdefaults{'datelocale_def'} = $domconfig{'defaults'}{'datelocale_def'};
         $domdefaults{'portal_def'} = $domconfig{'defaults'}{'portal_def'};
+        $domdefaults{'portal_def_email'} = $domconfig{'defaults'}{'portal_def_email'};
+        $domdefaults{'portal_def_web'} = $domconfig{'defaults'}{'portal_def_web'};
         $domdefaults{'intauth_cost'} = $domconfig{'defaults'}{'intauth_cost'};
         $domdefaults{'intauth_switch'} = $domconfig{'defaults'}{'intauth_switch'};
         $domdefaults{'intauth_check'} = $domconfig{'defaults'}{'intauth_check'};
+        $domdefaults{'unamemap_rule'} = $domconfig{'defaults'}{'unamemap_rule'};
     } else {
         $domdefaults{'lang_def'} = &domain($domain,'lang_def');
         $domdefaults{'auth_def'} = &domain($domain,'auth_def');
@@ -2377,7 +2540,7 @@ sub get_domain_defaults {
         } else {
             $domdefaults{'defaultquota'} = $domconfig{'quotas'};
         }
-        my @usertools = ('aboutme','blog','webdav','portfolio');
+        my @usertools = ('aboutme','blog','webdav','portfolio','portaccess');
         foreach my $item (@usertools) {
             if (ref($domconfig{'quotas'}{$item}) eq 'HASH') {
                 $domdefaults{$item} = $domconfig{'quotas'}{$item};
@@ -2392,6 +2555,17 @@ sub get_domain_defaults {
             $domdefaults{$item} = $domconfig{'requestcourses'}{$item};
         }
     }
+    if (ref($domconfig{'authordefaults'}) eq 'HASH') {
+        foreach my $item ('nocodemirror','copyright','sourceavail','domcoordacc','editors','archive') {
+            if ($item eq 'editors') {
+                if (ref($domconfig{'authordefaults'}{'editors'}) eq 'ARRAY') {
+                    $domdefaults{$item} = join(',',@{$domconfig{'authordefaults'}{'editors'}});
+                }
+            } else {
+                $domdefaults{$item} = $domconfig{'authordefaults'}{$item};
+            }
+        }
+    }
     if (ref($domconfig{'requestauthor'}) eq 'HASH') {
         $domdefaults{'requestauthor'} = $domconfig{'requestauthor'};
     }
@@ -2407,6 +2581,9 @@ sub get_domain_defaults {
         if (ref($domconfig{'coursedefaults'}{'postsubmit'}) eq 'HASH') {
             $domdefaults{'postsubmit'} = $domconfig{'coursedefaults'}{'postsubmit'}{'client'};
         }
+        if (ref($domconfig{'coursedefaults'}{'crseditors'}) eq 'ARRAY') {
+            $domdefaults{'crseditors'}=join(',',@{$domconfig{'coursedefaults'}{'crseditors'}});
+        }
         foreach my $type (@coursetypes) {
             if (ref($domconfig{'coursedefaults'}{'coursecredits'}) eq 'HASH') {
                 unless ($type eq 'community') {
@@ -2416,12 +2593,25 @@ sub get_domain_defaults {
             if (ref($domconfig{'coursedefaults'}{'uploadquota'}) eq 'HASH') {
                 $domdefaults{$type.'quota'} = $domconfig{'coursedefaults'}{'uploadquota'}{$type};
             }
+            if (ref($domconfig{'coursedefaults'}{'coursequota'}) eq 'HASH') {
+                $domdefaults{$type.'coursequota'} = $domconfig{'coursedefaults'}{'coursequota'}{$type};
+            }
             if ($domdefaults{'postsubmit'} eq 'on') {
                 if (ref($domconfig{'coursedefaults'}{'postsubmit'}{'timeout'}) eq 'HASH') {
                     $domdefaults{$type.'postsubtimeout'} =
                         $domconfig{'coursedefaults'}{'postsubmit'}{'timeout'}{$type};
                 }
             }
+            if (ref($domconfig{'coursedefaults'}{'domexttool'}) eq 'HASH') {
+                $domdefaults{$type.'domexttool'} = $domconfig{'coursedefaults'}{'domexttool'}{$type};
+            } else {
+                $domdefaults{$type.'domexttool'} = 1;
+            }
+            if (ref($domconfig{'coursedefaults'}{'exttool'}) eq 'HASH') {
+                $domdefaults{$type.'exttool'} = $domconfig{'coursedefaults'}{'exttool'}{$type};
+            } else {
+                $domdefaults{$type.'exttool'} = 0;
+            }
         }
         if (ref($domconfig{'coursedefaults'}{'canclone'}) eq 'HASH') {
             if (ref($domconfig{'coursedefaults'}{'canclone'}{'instcode'}) eq 'ARRAY') {
@@ -2509,6 +2699,40 @@ sub get_domain_defaults {
             }
         }
     }
+    if (ref($domconfig{'ltisec'}) eq 'HASH') {
+        if (ref($domconfig{'ltisec'}{'encrypt'}) eq 'HASH') {
+            $domdefaults{'linkprotenc_crs'} = $domconfig{'ltisec'}{'encrypt'}{'crs'};
+            $domdefaults{'linkprotenc_dom'} = $domconfig{'ltisec'}{'encrypt'}{'dom'};
+            $domdefaults{'ltienc_consumers'} = $domconfig{'ltisec'}{'encrypt'}{'consumers'};
+        }
+        if (ref($domconfig{'ltisec'}{'private'}) eq 'HASH') {
+            if (ref($domconfig{'ltisec'}{'private'}{'keys'}) eq 'ARRAY') {
+                $domdefaults{'ltiprivhosts'} = $domconfig{'ltisec'}{'private'}{'keys'};
+            }
+        }
+        if (ref($domconfig{'ltisec'}{'suggested'}) eq 'HASH') {
+            my %suggestions = %{$domconfig{'ltisec'}{'suggested'}};
+            foreach my $item (keys(%{$domconfig{'ltisec'}{'suggested'}})) {
+                unless (ref($domconfig{'ltisec'}{'suggested'}{$item}) eq 'HASH') {
+                    delete($suggestions{$item});
+                }
+            }
+            if (keys(%suggestions)) {
+                $domdefaults{'linkprotsuggested'} = \%suggestions;
+            }
+        }
+    }
+    if (ref($domconfig{'toolsec'}) eq 'HASH') {
+        if (ref($domconfig{'toolsec'}{'encrypt'}) eq 'HASH') {
+            $domdefaults{'toolenc_crs'} = $domconfig{'toolsec'}{'encrypt'}{'crs'};
+            $domdefaults{'toolenc_dom'} = $domconfig{'toolsec'}{'encrypt'}{'dom'};
+        }
+        if (ref($domconfig{'toolsec'}{'private'}) eq 'HASH') {
+            if (ref($domconfig{'toolsec'}{'private'}{'keys'}) eq 'ARRAY') {
+                $domdefaults{'toolprivhosts'} = $domconfig{'toolsec'}{'private'}{'keys'};
+            }
+        }
+    }
     &do_cache_new('domdefaults',$domain,\%domdefaults,$cachetime);
     return %domdefaults;
 }
@@ -2545,6 +2769,7 @@ sub get_dom_instcats {
             if (&auto_instcode_format($caller,$dom,\%coursecodes,\%codes,
                                       \@codetitles,\%cat_titles,\%cat_order) eq 'ok') {
                 $instcats = {
+                                totcodes => $totcodes,
                                 codes => \%codes,
                                 codetitles => \@codetitles,
                                 cat_titles => \%cat_titles,
@@ -2613,6 +2838,26 @@ sub course_portal_url {
     return $firsturl;
 }
 
+sub url_prefix {
+    my ($r,$dom,$home,$context) = @_;
+    my $prefix;
+    my %domdefs = &get_domain_defaults($dom);
+    if ($domdefs{'portal_def'} && $domdefs{'portal_def_'.$context}) {
+        if ($domdefs{'portal_def'} =~ m{^(https?://[^/]+)}) {
+            $prefix = $1;
+        }
+    }
+    if ($prefix eq '') {
+        my $hostname = &hostname($home);
+        my $protocol = $protocol{$home};
+        $protocol = 'http' if ($protocol{$home} ne 'https');
+        my $alias = &use_proxy_alias($r,$home);
+        $hostname = $alias if ($alias ne '');
+        $prefix = $protocol.'://'.$hostname;
+    }
+    return $prefix;
+}
+
 # --------------------------------------------------- Assign a key to a student
 
 sub assign_access_key {
@@ -3424,6 +3669,29 @@ sub can_edit_resource {
         }
     }
 
+#
+# For /adm/viewcoauthors can only edit if author or co-author who is manager.
+#
+
+    if (($resurl eq '/adm/viewcoauthors') && ($cnum ne '') && ($cdom ne '')) {
+        if (((&allowed('cca',"$cdom/$cnum")) ||
+             (&allowed('caa',"$cdom/$cnum"))) ||
+             ((&allowed('vca',"$cdom/$cnum") ||
+               &allowed('vaa',"$cdom/$cnum")) &&
+              ($env{"environment.internal.manager./$cdom/$cnum"}))) {
+            $home = $env{'user.home'};
+            $cfile = $resurl;
+            if ($env{'form.forceedit'}) {
+                $forceview = 1;
+            } else {
+                $forceedit = 1;
+            }
+            return ($cfile,$home,$switchserver,$forceedit,$forceview);
+        } else {
+            return;
+        }
+    }
+
     if ($env{'request.course.id'}) {
         my $crsedit = &Apache::lonnet::allowed('mdc',$env{'request.course.id'});
         if ($group ne '') {
@@ -3458,10 +3726,15 @@ sub can_edit_resource {
                     return;
                 }
             } elsif (!$crsedit) {
+                if ($env{'request.role'} =~ m{^st\./$cdom/$cnum}) {
 #
 # No edit allowed where CC has switched to student role.
 #
-                return;
+                    return;
+                } elsif (($resurl !~ m{^/res/$match_domain/$match_username/}) ||
+                         ($resurl =~ m{^/res/lib/templates/})) {
+                    return;
+                }
             }
         }
     }
@@ -3870,7 +4143,7 @@ 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, scantron or ''. 
+#                                    canceloverwrite, scantron, toollogo 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
@@ -3882,8 +4155,8 @@ sub resizeImage {
 #                          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
-#        $dsetudom - domain for permanaent storage of uploaded file
+#        $destuname - username for permanent storage of uploaded file
+#        $destudom - domain for permanaent storage of uploaded file
 #        $thumbwidth - width (pixels) of thumbnail to make for uploaded image 
 #        $thumbheight - height (pixels) of thumbnail to make for uploaded image
 #        $resizewidth - width (pixels) to which to resize uploaded image
@@ -4093,11 +4366,24 @@ sub finishuserfileupload {
     if (($thumbwidth =~ /^\d+$/) && ($thumbheight =~ /^\d+$/)) {
         my $input = $filepath.'/'.$file;
         my $output = $filepath.'/'.'tn-'.$file;
+        my $makethumb;
         my $thumbsize = $thumbwidth.'x'.$thumbheight;
-        my @args = ('convert','-sample',$thumbsize,$input,$output);
-        system({$args[0]} @args);
-        if (-e $filepath.'/'.'tn-'.$file) {
-            $fetchthumb  = 1; 
+        if ($context eq 'toollogo') {
+            my ($fullwidth,$fullheight) = &check_dimensions($input);
+            if ($fullwidth ne '' && $fullheight ne '') {
+                if ($fullwidth > $thumbwidth && $fullheight > $thumbheight) {
+                    $makethumb = 1;
+                }
+            }
+        } else {
+            $makethumb = 1;
+        }
+        if ($makethumb) {
+            my @args = ('convert','-sample',$thumbsize,$input,$output);
+            system({$args[0]} @args);
+            if (-e $filepath.'/'.'tn-'.$file) {
+                $fetchthumb  = 1;
+            }
         }
     }
  
@@ -4329,6 +4615,30 @@ sub embedded_dependency {
     return;
 }
 
+sub check_dimensions {
+    my ($inputfile) = @_;
+    my ($fullwidth,$fullheight);
+    if (($inputfile =~ m|^[/\w.\-]+$|) && (-e $inputfile)) {
+        my $mm = new File::MMagic;
+        my $mime_type = $mm->checktype_filename($inputfile);
+        if ($mime_type =~ m{^image/}) {
+            if (open(PIPE,"identify $inputfile 2>&1 |")) {
+                my $imageinfo = <PIPE>;
+                if (!close(PIPE)) {
+                    &Apache::lonnet::logthis("Failed to close PIPE opened to retrieve image information for $inputfile");
+                }
+                chomp($imageinfo);
+                my ($fullsize) =
+                    ($imageinfo =~ /^\Q$inputfile\E\s+\w+\s+(\d+x\d+)/);
+                if ($fullsize) {
+                    ($fullwidth,$fullheight) = split(/x/,$fullsize);
+                }
+            }
+        }
+    }
+    return ($fullwidth,$fullheight);
+}
+
 sub bubblesheet_converter {
     my ($cdom,$fullpath,$config,$format) = @_;
     if ((&domain($cdom) ne '') &&
@@ -4565,6 +4875,7 @@ sub get_scantronformat_file {
                 close($fh);
             }
         }
+        chomp(@lines);
     }
     return @lines;
 }
@@ -4691,7 +5002,7 @@ sub flushcourselogs {
 # Typo in rev. 1.458 (2003/12/09)??
 # These should likely by $env{'course.'.$cid.'.domain'} and $env{'course.'.$cid.'.num'}
 #
-# While these ramain as  $env{'request.'.$cid.'.domain'} and $env{'request.'.$cid.'.num'}
+# While these remain as $env{'request.'.$cid.'.domain'} and $env{'request.'.$cid.'.num'}
 # $dom and $name will always be null, so the &inc() call will default to storing this data
 # in a nohist_accesscount.db file for the user rather than the course.
 #
@@ -4920,6 +5231,36 @@ sub courserolelog {
             $storehash{'group'} = $sec;
         } else {
             $storehash{'section'} = $sec;
+            my ($curruserdomstr,$newuserdomstr);
+            if (exists($env{'course.'.$cdom.'_'.$cnum.'.internal.userdomains'})) {
+                $curruserdomstr = $env{'course.'.$env{'request.course.id'}.'.internal.userdomains'};
+            } else {
+                my %courseinfo = &coursedescription($cdom.'/'.$cnum);
+                $curruserdomstr = $courseinfo{'internal.userdomains'};
+            }
+            if ($curruserdomstr ne '') {
+                my @udoms = split(/,/,$curruserdomstr);
+                unless (grep(/^\Q$domain\E/,@udoms)) {
+                    push(@udoms,$domain);
+                    $newuserdomstr = join(',',sort(@udoms));
+                }
+            } else {
+                $newuserdomstr = $domain;
+            }
+            if ($newuserdomstr ne '') {
+                my $putresult = &put('environment',{ 'internal.userdomains' => $newuserdomstr },
+                                     $cdom,$cnum);
+                if ($putresult eq 'ok') {
+                    unless (($selfenroll) || ($context eq 'selfenroll')) {
+                        if (($context eq 'createcourse') || ($context eq 'requestcourses') ||
+                            ($context eq 'automated') || ($context eq 'domain')) {
+                            $env{'course.'.$cdom.'_'.$cnum.'.internal.userdomains'} = $newuserdomstr;
+                        } elsif ($env{'request.course.id'} eq $cdom.'_'.$cnum) {
+                            &appenv({'course.'.$cdom.'_'.$cnum.'.internal.userdomains' => $newuserdomstr});
+                        }
+                    }
+                }
+            }
         }
         &write_log('course',$namespace,\%storehash,$delflag,$username,
                    $domain,$cnum,$cdom);
@@ -4967,6 +5308,39 @@ sub coauthorrolelog {
     return;
 }
 
+sub authorarchivelog {
+    my ($hashref,$size,$filesdest,$action) = @_;
+    my $lonprtdir = $Apache::lonnet::perlvar{'lonPrtDir'};
+    my $londocroot = $Apache::lonnet::perlvar{'lonDocRoot'};
+    $filesdest =~ s{^\Q$lonprtdir/\E}{};
+    if ($filesdest =~ m{^($match_username)_($match_domain)_archive_(\d+_\d+_\d+(|[.\w]+))$}) {
+        my ($auname,$audom,$id) = ($1,$2,$3);
+        if (ref($hashref) eq 'HASH') {
+            my $namespace = 'archivelog';
+            my $dir;
+            if ($hashref->{dir} =~ m{^\Q$londocroot/priv/$audom/$auname\E(.*)$}) {
+                $dir = $1;
+            }
+            my $delflag = 0;
+            my %storehash = (
+                              id      => $id,
+                              dir     => $dir,
+                              files   => $hashref->{numfiles},
+                              subdirs => $hashref->{numdirs},
+                              bytes   => $hashref->{bytes},
+                              size    => $size,
+                              action  => $action,
+                            );
+            if ($action eq 'delete') {
+                $delflag = 1;
+            }
+            &write_log('author',$namespace,\%storehash,$delflag,$auname,
+                       $audom,$auname,$audom);
+        }
+    }
+    return;
+}
+
 sub get_course_adv_roles {
     my ($cid,$codes) = @_;
     $cid=$env{'request.course.id'} unless (defined($cid));
@@ -5474,7 +5848,7 @@ sub courselastaccess {
 sub extract_lastaccess {
     my ($returnhash,$rep) = @_;
     if (ref($returnhash) eq 'HASH') {
-        unless ($rep eq 'unknown_command' || $rep eq 'no_such_host' || 
+        unless ($rep eq 'unknown_cmd' || $rep eq 'no_such_host' || 
                 $rep eq 'con_lost' || $rep eq 'rejected' || $rep eq 'refused' ||
                  $rep eq '') {
             my @pairs=split(/\&/,$rep);
@@ -6428,7 +6802,7 @@ sub rolesinit {
     my %firstaccess = &dump('firstaccesstimes', $domain, $username);
     my %timerinterval = &dump('timerinterval', $domain, $username);
     my (%coursetimerstarts, %firstaccchk, %firstaccenv, %coursetimerintervals,
-        %timerintchk, %timerintenv);
+        %timerintchk, %timerintenv,%coauthorenv);
 
     foreach my $key (keys(%firstaccess)) {
         my ($cid, $rest) = split(/\0/, $key);
@@ -6442,6 +6816,8 @@ sub rolesinit {
 
     my %allroles=();
     my %allgroups=();
+    my %gotcoauconfig=();
+    my %domdefaults=();
 
     for my $area (grep { ! /^rolesdef_/ } keys(%rolesdump)) {
         my $role = $rolesdump{$area};
@@ -6493,6 +6869,37 @@ sub rolesinit {
         } else {
         # Normal role, defined in roles.tab
             &standard_roleprivs(\%allroles,$trole,$tdomain,$spec,$trest,$area);
+            if (($trole eq 'ca') || ($trole eq 'aa')) {
+                (undef,my ($audom,$auname)) = split(/\//,$area);
+                unless ($gotcoauconfig{$area}) {
+                    my @ca_settings = ('authoreditors','coauthorlist','coauthoroptin');
+                    my %info = &userenvironment($audom,$auname,@ca_settings);
+                    $gotcoauconfig{$area} = 1;
+                    foreach my $item (@ca_settings) {
+                        if (exists($info{$item})) {
+                            my $name = $item;
+                            if ($item eq 'authoreditors') {
+                                $name = 'editors';
+                                unless ($info{'authoreditors'}) {
+                                    my %domdefs;
+                                    if (ref($domdefaults{$audom}) eq 'HASH') {
+                                        %domdefs = %{$domdefaults{$audom}};
+                                    } else {
+                                        %domdefs = &get_domain_defaults($audom);
+                                        $domdefaults{$audom} = \%domdefs;
+                                    }
+                                    if ($domdefs{$name} ne '') {
+                                        $info{'authoreditors'} = $domdefs{$name};
+                                    } else {
+                                        $info{'authoreditors'} = 'edit,xml';
+                                    }
+                                }
+                            }
+                            $coauthorenv{"environment.internal.$name.$area"} = $info{$item};
+                        }
+                    }
+                }
+            }
         }
 
         my $cid = $tdomain.'_'.$trest;
@@ -6521,7 +6928,7 @@ sub rolesinit {
     $env{'user.adv'} = $userroles{'user.adv'};
     $env{'user.rar'} = $userroles{'user.rar'};
 
-    return (\%userroles,\%firstaccenv,\%timerintenv);
+    return (\%userroles,\%firstaccenv,\%timerintenv,\%coauthorenv);
 }
 
 sub set_arearole {
@@ -6582,31 +6989,31 @@ sub course_adhocrole_privs {
             $full{$priv} = $restrict;
         }
         foreach my $item (split(/,/,$overrides{"internal.adhocpriv.$rolename"})) {
-             next if ($item eq '');
-             my ($rule,$rest) = split(/=/,$item);
-             next unless (($rule eq 'off') || ($rule eq 'on'));
-             foreach my $priv (split(/:/,$rest)) {
-                 if ($priv ne '') {
-                     if ($rule eq 'off') {
-                         $possremove{$priv} = 1;
-                     } else {
-                         $possadd{$priv} = 1;
-                     }
-                 }
-             }
-         }
-         foreach my $priv (sort(keys(%full))) {
-             if (exists($currprivs{$priv})) {
-                 unless (exists($possremove{$priv})) {
-                     $storeprivs{$priv} = $currprivs{$priv};
-                 }
-             } elsif (exists($possadd{$priv})) {
-                 $storeprivs{$priv} = $full{$priv};
-             }
-         }
-         $coursepriv = ':'.join(':',map { $_.'&'.$storeprivs{$_}; } sort(keys(%storeprivs)));
-     }
-     return $coursepriv;
+            next if ($item eq '');
+            my ($rule,$rest) = split(/=/,$item);
+            next unless (($rule eq 'off') || ($rule eq 'on'));
+            foreach my $priv (split(/:/,$rest)) {
+                if ($priv ne '') {
+                    if ($rule eq 'off') {
+                        $possremove{$priv} = 1;
+                    } else {
+                        $possadd{$priv} = 1;
+                    }
+                }
+            }
+        }
+        foreach my $priv (sort(keys(%full))) {
+            if (exists($currprivs{$priv})) {
+                unless (exists($possremove{$priv})) {
+                    $storeprivs{$priv} = $currprivs{$priv};
+                }
+            } elsif (exists($possadd{$priv})) {
+                $storeprivs{$priv} = $full{$priv};
+            }
+        }
+        $coursepriv = ':'.join(':',map { $_.'&'.$storeprivs{$_}; } sort(keys(%storeprivs)));
+    }
+    return $coursepriv;
 }
 
 sub group_roleprivs {
@@ -7413,7 +7820,7 @@ sub portfolio_access {
 }
 
 sub get_portfolio_access {
-    my ($udom,$unum,$file_name,$group,$clientip,$access_hash) = @_;
+    my ($udom,$unum,$file_name,$group,$clientip,$access_hash,$portaccessref) = @_;
 
     if (!ref($access_hash)) {
 	my $current_perms = &get_portfile_permissions($udom,$unum);
@@ -7422,11 +7829,19 @@ sub get_portfolio_access {
 	$access_hash = $access_controls{$file_name};
     }
 
-    my ($public,$guest,@domains,@users,@courses,@groups,@ips);
+    my $portaccess;
+    if (ref($portaccess) eq 'SCALAR') {
+        $portaccess = $$portaccessref;
+    } else {
+        $portaccess = &usertools_access($unum,$udom,'portaccess',undef,'tools');
+    }
+
+    my ($public,$guest,@domains,@users,@courses,@groups,@ips,@userips);
     my $now = time;
     if (ref($access_hash) eq 'HASH') {
         foreach my $key (keys(%{$access_hash})) {
             my ($num,$scope,$end,$start) = ($key =~ /^([^:]+):([a-z]+)_(\d*)_?(\d*)$/);
+            next if (($scope ne 'ip') && ($portaccess == 0));
             if ($start > $now) {
                 next;
             }
@@ -7448,6 +7863,8 @@ sub get_portfolio_access {
                 push(@groups,$key);
             } elsif ($scope eq 'ip') {
                 push(@ips,$key);
+            } elsif ($scope eq 'userip') {
+                push(@userips,$key);
             }
         }
         if ($public) {
@@ -7465,6 +7882,19 @@ sub get_portfolio_access {
             if ($allowed) {
                 return 'ok';
             }
+        } elsif (@userips > 0) {
+            my $allowed;
+            foreach my $useripkey (@userips) {
+                if (ref($access_hash->{$useripkey}{'ip'}) eq 'ARRAY') {
+                    if (&Apache::loncommon::check_ip_acc(join(',',@{$access_hash->{$useripkey}{'ip'}}),$clientip)) {
+                        $allowed = 1;
+                        last;
+                    }
+                }
+            }
+            if ($allowed) {
+                return 'ok';
+            }
         }
         if ($env{'user.name'} eq 'public' && $env{'user.domain'} eq 'public') {
             if ($guest) {
@@ -7640,6 +8070,17 @@ sub is_portfolio_file {
     return;
 }
 
+sub is_coursetool_logo {
+    my ($uri) = @_;
+    if ($env{'request.course.id'}) {
+        my $courseurl = &courseid_to_courseurl($env{'request.course.id'});
+        if ($uri =~ m{^/*uploaded\Q$courseurl\E/toollogo/\d+/[^/]+$}) {
+            return 1;
+        }
+    }
+    return;
+}
+
 sub usertools_access {
     my ($uname,$udom,$tool,$action,$context,$userenvref,$domdefref,$is_advref)=@_;
     my ($access,%tools);
@@ -7652,17 +8093,24 @@ sub usertools_access {
                       unofficial => 1,
                       community  => 1,
                       textbook   => 1,
+                      lti        => 1,
                  );
     } elsif ($context eq 'requestauthor') {
         %tools = (
                       requestauthor => 1,
                  );
+    } elsif ($context eq 'authordefaults') {
+        %tools = (
+                      webdav    => 1,
+                 );
     } else {
         %tools = (
                       aboutme   => 1,
                       blog      => 1,
                       webdav    => 1,
                       portfolio => 1,
+                      portaccess => 1,
+                      timezone  => 1,
                  );
     }
     return if (!defined($tools{$tool}));
@@ -7678,6 +8126,10 @@ sub usertools_access {
                 return $env{'environment.canrequest.'.$tool};
             } elsif ($context eq 'requestauthor') {
                 return $env{'environment.canrequest.author'};
+            } elsif ($context eq 'authordefaults') {
+                if ($tool eq 'webdav') {
+                    return $env{'environment.availabletools.'.$tool};
+                }
             } else {
                 return $env{'environment.availabletools.'.$tool};
             }
@@ -7687,6 +8139,10 @@ sub usertools_access {
     my ($toolstatus,$inststatus,$envkey);
     if ($context eq 'requestauthor') {
         $envkey = $context;
+    } elsif ($context eq 'authordefaults') {
+        if ($tool eq 'webdav') {
+            $envkey = 'tools.'.$tool;
+        }
     } else {
         $envkey = $context.'.'.$tool;
     }
@@ -7848,25 +8304,29 @@ sub is_advanced_user {
 }
 
 sub check_can_request {
-    my ($dom,$can_request,$request_domains) = @_;
+    my ($dom,$can_request,$request_domains,$uname,$udom) = @_;
     my $canreq = 0;
+    if (($env{'user.name'} ne '') && ($env{'user.domain'} ne '')) {
+        $uname = $env{'user.name'};
+        $udom = $env{'user.domain'};
+    }
     my ($types,$typename) = &Apache::loncommon::course_types();
     my @options = ('approval','validate','autolimit');
     my $optregex = join('|',@options);
     if ((ref($can_request) eq 'HASH') && (ref($types) eq 'ARRAY')) {
         foreach my $type (@{$types}) {
-            if (&usertools_access($env{'user.name'},
-                                  $env{'user.domain'},
-                                  $type,undef,'requestcourses')) {
+            if (&usertools_access($uname,$udom,$type,undef,
+                                  'requestcourses')) {
                 $canreq ++;
                 if (ref($request_domains) eq 'HASH') {
-                    push(@{$request_domains->{$type}},$env{'user.domain'});
+                    push(@{$request_domains->{$type}},$udom);
                 }
-                if ($dom eq $env{'user.domain'}) {
+                if ($dom eq $udom) {
                     $can_request->{$type} = 1;
                 }
             }
-            if ($env{'environment.reqcrsotherdom.'.$type} ne '') {
+            if (($env{'user.name'} ne '') && ($env{'user.domain'} ne '') &&
+                ($env{'environment.reqcrsotherdom.'.$type} ne '')) {
                 my @curr = split(',',$env{'environment.reqcrsotherdom.'.$type});
                 if (@curr > 0) {
                     foreach my $item (@curr) {
@@ -7883,7 +8343,7 @@ sub check_can_request {
                             }
                         }
                     }
-                    unless($dom eq $env{'user.domain'}) {
+                    unless ($dom eq $env{'user.domain'}) {
                         $canreq ++;
                         if (grep(/^\Q$dom\E:($optregex)(=?\d*)$/,@curr)) {
                             $can_request->{$type} = 1;
@@ -8235,7 +8695,7 @@ sub allowed {
 
 # If this is generating or modifying users, exit with special codes
 
-    if (':csu:cdc:ccc:cin:cta:cep:ccr:cst:cad:cli:cau:cdg:cca:caa:'=~/\:\Q$priv\E\:/) {
+    if (':csu:cdc:ccc:cin:cta:cep:ccr:cst:cad:cli:cau:cdg:cca:caa::vca:vaa:'=~/\:\Q$priv\E\:/) {
 	if (($priv eq 'cca') || ($priv eq 'caa')) {
 	    my ($audom,$auname)=split('/',$uri);
 # no author name given, so this just checks on the general right to make a co-author in this domain
@@ -8244,6 +8704,13 @@ sub allowed {
 	    if (($auname ne $env{'user.name'} && $env{'request.role'} !~ /^dc\./) ||
 		(($audom ne $env{'user.domain'} && $env{'request.role'} !~ /^dc\./) &&
 		 ($audom ne $env{'request.role.domain'}))) { return ''; }
+        } elsif (($priv eq 'vca') || ($priv eq 'vaa')) {
+            my ($audom,$auname)=split('/',$uri);
+            unless ($auname) { return $thisallowed; }
+            unless (($env{'request.role'} eq "dc./$audom") ||
+                    ($env{'request.role'} eq "ca./$uri")) {
+                return '';
+            }
 	}
 	return $thisallowed;
     }
@@ -8255,6 +8722,12 @@ sub allowed {
 
     if ($env{'request.course.id'}) {
 
+        if ($priv eq 'bre') {
+            if (&is_coursetool_logo($uri)) {
+                return 'F';
+            }
+        }
+
 # If this is modifying password (internal auth) domains must match for user and user's role.
 
         if ($priv eq 'mip') {
@@ -8561,7 +9034,7 @@ sub constructaccess {
     my ($ownername,$ownerdomain,$ownerhome);
 
     ($ownerdomain,$ownername) =
-        ($url=~ m{^(?:\Q$perlvar{'lonDocRoot'}\E|)/priv/($match_domain)/($match_username)(?:/|$)});
+        ($url=~ m{^(?:\Q$perlvar{'lonDocRoot'}\E|)(?:/daxepage|/daxeopen)?/priv/($match_domain)/($match_username)(?:/|$)});
 
 # The URL does not really point to any authorspace, forget it
     unless (($ownername) && ($ownerdomain)) { return ''; }
@@ -9648,7 +10121,7 @@ sub auto_instsec_reformat {
             my $info = &freeze_escape($instsecref);
             my $response=&reply('autoinstsecreformat:'.$cdom.':'.
                                 $action.':'.$info,$server);
-            next if ($response =~ /(con_lost|error|no_such_host|refused|unknown_command)/);
+            next if ($response =~ /(con_lost|error|no_such_host|refused|unknown_cmd)/);
             my @items = split(/&/,$response);
             foreach my $item (@items) {
                 my ($key,$value) = split(/=/,$item);
@@ -9729,7 +10202,7 @@ sub auto_export_grades {
             my $grades = &freeze_escape($gradesref);
             my $response=&reply('encrypt:autoexportgrades:'.$cdom.':'.$cnum.':'.
                                 $info.':'.$grades,$homeserver);
-            unless ($response =~ /(con_lost|error|no_such_host|refused|unknown_command)/) {
+            unless ($response =~ /(con_lost|error|no_such_host|refused|unknown_cmd)/) {
                 my @items = split(/&/,$response);
                 foreach my $item (@items) {
                     my ($key,$value) = split('=',$item);
@@ -9991,7 +10464,7 @@ sub plaintext {
 sub assignrole {
     my ($udom,$uname,$url,$role,$end,$start,$deleteflag,$selfenroll,
         $context)=@_;
-    my $mrole;
+    my ($mrole,$rolelogcontext);
     if ($role =~ /^cr\//) {
         my $cwosec=$url;
         $cwosec=~s/^\/($match_domain)\/($match_courseid)\/.*/$1\/$2/;
@@ -10063,8 +10536,12 @@ sub assignrole {
                             }
                         }
                     }
-                } elsif (($selfenroll == 1) && ($role eq 'st') && ($udom eq $env{'user.domain'}) && ($uname eq $env{'user.name'})) {
-                    $refused = '';
+                } elsif (($selfenroll == 1) && ($udom eq $env{'user.domain'}) && ($uname eq $env{'user.name'})) {
+                    if ($role eq 'st') {
+                        $refused = '';
+                    } elsif (($context eq 'ltienroll') && ($env{'request.lti.login'})) {
+                        $refused = '';
+                    }
                 } elsif ($context eq 'requestcourses') {
                     my @possroles = ('st','ta','ep','in','cc','co');
                     if ((grep(/^\Q$role\E$/,@possroles)) && ($env{'user.name'} ne '' && $env{'user.domain'} ne '')) {
@@ -10117,6 +10594,15 @@ sub assignrole {
                             }
                         }
                     }
+                } elsif (($context eq 'author') && (($role eq 'ca' || $role eq 'aa'))) {
+                    if ($url =~ m{^/($match_domain)/($match_username)$}) {
+                        my ($audom,$auname) = ($1,$2);
+                        if ((&Apache::lonnet::allowed('v'.$role,"$audom/$auname")) &&
+                            ($env{"environment.internal.manager.$url"})) {
+                            $refused = '';
+                            $rolelogcontext = 'coauthor';
+                        }
+                    }
                 }
                 if ($refused) {
                     &logthis('Refused assignrole: '.$udom.' '.$uname.' '.$url.
@@ -10184,8 +10670,11 @@ sub assignrole {
             &domainrolelog($role,$uname,$udom,$url,$origstart,$origend,$delflag,
                            $context);
         } elsif (($role eq 'ca') || ($role eq 'aa')) {
+            if ($rolelogcontext eq '') {
+                $rolelogcontext = $context;
+            }
             &coauthorrolelog($role,$uname,$udom,$url,$origstart,$origend,$delflag,
-                             $context);
+                             $rolelogcontext);
         }
         if ($role eq 'cc') {
             &autoupdate_coowners($url,$end,$start,$uname,$udom);
@@ -10356,10 +10845,14 @@ sub modifyuser {
     my $newuser;
     if ($uhome eq 'no_host') {
         $newuser = 1;
+        unless (($umode && ($upass ne '')) || ($umode eq 'localauth') ||
+                ($umode eq 'lti')) {
+            return 'error: more information needed to create new user';
+        }
     }
 # ----------------------------------------------------------------- Create User
     if (($uhome eq 'no_host') && 
-	(($umode && $upass) || ($umode eq 'localauth'))) {
+	(($umode && $upass) || ($umode eq 'localauth') || ($umode eq 'lti'))) {
         my $unhome='';
         if (defined($desiredhome) && &host_domain($desiredhome) eq $udom) { 
             $unhome = $desiredhome;
@@ -11696,15 +12189,22 @@ sub resdata {
 
 sub get_domain_lti {
     my ($cdom,$context) = @_;
-    my ($name,%lti);
+    my ($name,$cachename,%lti);
     if ($context eq 'consumer') {
         $name = 'ltitools';
     } elsif ($context eq 'provider') {
         $name = 'lti';
+    } elsif ($context eq 'linkprot') {
+        $name = 'ltisec';
     } else {
         return %lti;
     }
-    my ($result,$cached)=&is_cached_new($name,$cdom);
+    if ($context eq 'linkprot') {
+        $cachename = $context;
+    } else {
+        $cachename = $name;
+    }
+    my ($result,$cached)=&is_cached_new($cachename,$cdom);
     if (defined($cached)) {
         if (ref($result) eq 'HASH') {
             %lti = %{$result};
@@ -11712,39 +12212,44 @@ sub get_domain_lti {
     } else {
         my %domconfig = &get_dom('configuration',[$name],$cdom);
         if (ref($domconfig{$name}) eq 'HASH') {
-            %lti = %{$domconfig{$name}};
-            my %encdomconfig = &get_dom('encconfig',[$name],$cdom,undef,1);
-            if (ref($encdomconfig{$name}) eq 'HASH') {
-                foreach my $id (keys(%lti)) {
-                    if (ref($encdomconfig{$name}{$id}) eq 'HASH') {
-                        foreach my $item ('key','secret') {
-                            $lti{$id}{$item} = $encdomconfig{$name}{$id}{$item};
-                        }
-                    }
+            if ($context eq 'linkprot') {
+                if (ref($domconfig{$name}{'linkprot'}) eq 'HASH') {
+                    %lti = %{$domconfig{$name}{'linkprot'}};
                 }
+            } else {
+                %lti = %{$domconfig{$name}};
             }
         }
         my $cachetime = 24*60*60;
-        &do_cache_new($name,$cdom,\%lti,$cachetime);
+        &do_cache_new($cachename,$cdom,\%lti,$cachetime);
     }
     return %lti;
 }
 
 sub get_course_lti {
-    my ($cnum,$cdom) = @_;
+    my ($cnum,$cdom,$context) = @_;
+    my ($name,$cachename,%lti);
+    if ($context eq 'consumer') {
+        $name = 'ltitools';
+        $cachename = 'courseltitools';
+    } elsif ($context eq 'provider') {
+        $name = 'lti';
+        $cachename = 'courselti';
+    } else {
+        return %lti;
+    }
     my $hashid=$cdom.'_'.$cnum;
-    my %courselti;
-    my ($result,$cached)=&is_cached_new('courselti',$hashid);
+    my ($result,$cached)=&is_cached_new($cachename,$hashid);
     if (defined($cached)) {
         if (ref($result) eq 'HASH') {
-            %courselti = %{$result};
+            %lti = %{$result};
         }
     } else {
-        %courselti = &dump('lti',$cdom,$cnum,undef,undef,undef,1);
+        %lti = &dump($name,$cdom,$cnum,undef,undef,undef,1);
         my $cachetime = 24*60*60;
-        &do_cache_new('courselti',$hashid,\%courselti,$cachetime);
+        &do_cache_new($cachename,$hashid,\%lti,$cachetime);
     }
-    return %courselti;
+    return %lti;
 }
 
 sub courselti_itemid {
@@ -11753,7 +12258,6 @@ sub courselti_itemid {
     $chome = &homeserver($cnum,$cdom);
     return if ($chome eq 'no_host');
     if (ref($params) eq 'HASH') {
-        my $items = &freeze_escape($params);
         my $rep;
         if (grep { $_ eq $chome } current_machine_ids()) {
             $rep = LONCAPA::Lond::crslti_itemid($cdom,$cnum,$url,$method,$params,$perlvar{'lonVersion'});
@@ -11777,7 +12281,6 @@ sub domainlti_itemid {
     $primary_id = &domain($cdom,'primary');
     return if ($primary_id eq '');
     if (ref($params) eq 'HASH') {
-        my $items = &freeze_escape($params);
         my $rep;
         if (grep { $_ eq $primary_id } current_machine_ids()) {
             $rep = LONCAPA::Lond::domlti_itemid($cdom,$context,$url,$method,$params,$perlvar{'lonVersion'});
@@ -11796,24 +12299,131 @@ sub domainlti_itemid {
     return $itemid;
 }
 
-sub get_numsuppfiles {
-    my ($cnum,$cdom,$ignorecache)=@_;
+sub get_ltitools_id {
+    my ($context,$cdom,$cnum,$title) = @_;
+    my ($lockhash,$tries,$gotlock,$id,$error);
+
+    # get lock on ltitools db
+    $lockhash = {
+                   lock => $env{'user.name'}.
+                           ':'.$env{'user.domain'},
+                };
+    $tries = 0;
+    if ($context eq 'domain') {
+        $gotlock = &newput_dom('ltitools',$lockhash,$cdom);
+    } else {
+        $gotlock = &newput('ltitools',$lockhash,$cdom,$cnum);
+    }
+    while (($gotlock ne 'ok') && ($tries<10)) {
+        $tries ++;
+        sleep (0.1);
+        if ($context eq 'domain') {
+            $gotlock = &newput_dom('ltitools',$lockhash,$cdom);
+        } else {
+            $gotlock = &newput('ltitools',$lockhash,$cdom,$cnum);
+        }
+    }
+    if ($gotlock eq 'ok') {
+        my %currids;
+        if ($context eq 'domain') {
+            %currids = &dump_dom('ltitools',$cdom);
+        } else {
+            %currids = &dump('ltitools',$cdom,$cnum);
+        }
+        if ($currids{'lock'}) {
+            delete($currids{'lock'});
+            if (keys(%currids)) {
+                my @curr = sort { $a <=> $b } keys(%currids);
+                if ($curr[-1] =~ /^\d+$/) {
+                    $id = 1 + $curr[-1];
+                }
+            } else {
+                $id = 1;
+            }
+            if ($id) {
+                if ($context eq 'domain') {
+                    unless (&newput_dom('ltitools',{ $id => $title },$cdom) eq 'ok') {
+                        $error = 'nostore';
+                    }
+                } else {
+                    unless (&newput('ltitools',{ $id => $title },$cdom,$cnum) eq 'ok') {
+                        $error = 'nostore';
+                    }
+                }
+            } else {
+                $error = 'nonumber';
+            }
+        }
+        my $dellockoutcome;
+        if ($context eq 'domain') {
+            $dellockoutcome = &del_dom('ltitools',['lock'],$cdom);
+        } else {
+            $dellockoutcome = &del('ltitools',['lock'],$cdom,$cnum);
+        }
+    } else {
+        $error = 'nolock';
+    }
+    return ($id,$error);
+}
+
+sub count_supptools {
+    my ($cnum,$cdom,$ignorecache,$reload)=@_;
     my $hashid=$cnum.':'.$cdom;
-    my ($suppcount,$cached);
+    my ($numexttools,$cached);
     unless ($ignorecache) {
-        ($suppcount,$cached) = &is_cached_new('suppcount',$hashid);
+        ($numexttools,$cached) = &is_cached_new('supptools',$hashid);
     }
     unless (defined($cached)) {
         my $chome=&homeserver($cnum,$cdom);
+        $numexttools = 0;
         unless ($chome eq 'no_host') {
-            ($suppcount,my $errors) = (0,0);
-            my $suppmap = 'supplemental.sequence';
-            ($suppcount,$errors) =
-                &Apache::loncommon::recurse_supplemental($cnum,$cdom,$suppmap,$suppcount,$errors);
+            my ($supplemental) = &Apache::loncommon::get_supplemental($cnum,$cdom,$reload);
+            if (ref($supplemental) eq 'HASH') {
+                if ((ref($supplemental->{'ids'}) eq 'HASH') && (ref($supplemental->{'hidden'}) eq 'HASH')) {
+                    foreach my $key (keys(%{$supplemental->{'ids'}})) {
+                        if ($key =~ m{^/adm/$cdom/$cnum/\d+/ext\.tool$}) {
+                            $numexttools ++;
+                        }
+                    }
+                }
+            }
+        }
+        &do_cache_new('supptools',$hashid,$numexttools,600);
+    }
+    return $numexttools;
+}
+
+sub has_unhidden_suppfiles {
+    my ($cnum,$cdom,$ignorecache,$possdel)=@_;
+    my $hashid=$cnum.':'.$cdom;
+    my ($showsupp,$cached);
+    unless ($ignorecache) {
+        ($showsupp,$cached) = &is_cached_new('showsupp',$hashid);
+    }
+    unless (defined($cached)) {
+        my $chome=&homeserver($cnum,$cdom);
+        unless ($chome eq 'no_host') {
+            my ($supplemental) = &Apache::loncommon::get_supplemental($cnum,$cdom,$ignorecache,$possdel);
+            if (ref($supplemental) eq 'HASH') {
+                if ((ref($supplemental->{'ids'}) eq 'HASH') && (ref($supplemental->{'hidden'}) eq 'HASH')) {
+                    foreach my $key (keys(%{$supplemental->{'ids'}})) {
+                        next if ($key =~ /\.sequence$/);
+                        if (ref($supplemental->{'ids'}->{$key}) eq 'ARRAY') {
+                            foreach my $id (@{$supplemental->{'ids'}->{$key}}) {
+                                unless ($supplemental->{'hidden'}->{$id}) {
+                                    $showsupp = 1;
+                                    last;
+                                }
+                            }
+                        }
+                        last if ($showsupp);
+                    }
+                }
+            }
         }
-        &do_cache_new('suppcount',$hashid,$suppcount,600);
+        &do_cache_new('showsupp',$hashid,$showsupp,600);
     }
-    return $suppcount;
+    return $showsupp;
 }
 
 #
@@ -12084,20 +12694,25 @@ sub EXT {
 	}
 # ------------------------------------------ fourth, look in resource metadata
 
-	$spacequalifierrest=~s/\./\_/;
-	my $filename;
+        my $what = $spacequalifierrest;
+        $what=~s/\./\_/;
+        my $filename;
 	if (!$symbparm) { $symbparm=&symbread(); }
 	if ($symbparm) {
 	    $filename=(&decode_symb($symbparm))[2];
 	} else {
 	    $filename=$env{'request.filename'};
 	}
-	my $metadata=&metadata($filename,$spacequalifierrest);
+        my $toolsymb;
+        if (($filename =~ /ext\.tool$/) && ($what ne '0_gradable')) {
+            $toolsymb = $symbparm;
+        }
+	my $metadata=&metadata($filename,$what,$toolsymb);
 	if (defined($metadata)) { return &get_reply([$metadata,'resource']); }
-	$metadata=&metadata($filename,'parameter_'.$spacequalifierrest);
+	$metadata=&metadata($filename,'parameter_'.$what,$toolsymb);
 	if (defined($metadata)) { return &get_reply([$metadata,'resource']); }
 
-# ---------------------------------------------- fourth, look in rest of course
+# ----------------------------------------------- fifth, look in rest of course
 	if ($symbparm && defined($courseid) && 
 	    $courseid eq $env{'request.course.id'}) {
 	    my $coursereply=&resdata($env{'course.'.$courseid.'.num'},
@@ -12118,7 +12733,7 @@ sub EXT {
 	    if (defined($partgeneral[0])) { return &get_reply(\@partgeneral); }
 	}
 	if ($recurse) { return undef; }
-	my $pack_def=&packages_tab_default($filename,$varname);
+	my $pack_def=&packages_tab_default($filename,$varname,$toolsymb);
 	if (defined($pack_def)) { return &get_reply([$pack_def,'resource']); }
 # ---------------------------------------------------- Any other user namespace
     } elsif ($realm eq 'environment') {
@@ -12211,11 +12826,11 @@ sub sort_course_groups { # Sort groups b
 }
 
 sub packages_tab_default {
-    my ($uri,$varname)=@_;
+    my ($uri,$varname,$toolsymb)=@_;
     my (undef,$part,$name)=split(/\./,$varname);
 
     my (@extension,@specifics,$do_default);
-    foreach my $package (split(/,/,&metadata($uri,'packages'))) {
+    foreach my $package (split(/,/,&metadata($uri,'packages',$toolsymb))) {
 	my ($pack_type,$pack_part)=split(/_/,$package,2);
 	if ($pack_type eq 'default') {
 	    $do_default=1;
@@ -12284,7 +12899,7 @@ my %metaentry;
 my %importedpartids;
 my %importedrespids;
 sub metadata {
-    my ($uri,$what,$liburi,$prefix,$depthcount)=@_;
+    my ($uri,$what,$toolsymb,$liburi,$prefix,$depthcount)=@_;
     $uri=&declutter($uri);
     # if it is a non metadata possible uri return quickly
     if (($uri eq '') || 
@@ -12308,6 +12923,65 @@ sub metadata {
 	my ($result,$cached)=&is_cached_new('meta',$uri);
 	if (defined($cached)) { return $result->{':'.$what}; }
     }
+
+#
+# If the uri is for an external tool the file from
+# which metadata should be retrieved depends on whether
+# the tool had been configured to be gradable (set in the Course
+# Editor or Resource Editor).
+#
+# If a valid symb has been included as the third arg in the call
+# to &metadata() that can be used to retrieve the value of
+# parameter_0_gradable set for the resource, and included in the
+# uploaded map containing the tool. The value is retrieved via
+# &EXT(), if a valid symb is available.  Otherwise the value of
+# gradable in the exttool_$marker.db file for the tool instance
+# is retrieved via &get().
+#
+# When lonuserstate::traceroute() calls lonnet::EXT() for
+# hiddenresource and encrypturl (during course initialization)
+# the map-level parameter for resource.0.gradable included in the
+# uploaded map containing the tool will not yet have been stored
+# in the user_course_parms.db file for the user's session, so in
+# this case fall back to retrieving gradable status from the
+# exttool_$marker.db file.
+#
+# In order to avoid an infinite loop, &metadata() will return
+# before a call to &EXT(), if the uri is for an external tool
+# and the $what for which metadata is being requested is
+# parameter_0_gradable or 0_gradable.
+#
+
+    if ($uri =~ /ext\.tool$/) {
+        if (($what eq 'parameter_0_gradable') || ($what eq '0_gradable')) {
+            return;
+        } else {
+            my ($checked,$use_passback);
+            if ($toolsymb ne '') {
+                (undef,undef,my $tooluri) = &decode_symb($toolsymb);
+                if (($tooluri eq $uri) && (&EXT('resource.0.gradable',$toolsymb))) {
+                    $checked = 1;
+                    if (&EXT('resource.0.gradable',$toolsymb) =~ /^yes$/i) {
+                        $use_passback = 1;
+                    }
+                }
+            }
+            unless ($checked) {
+                my ($ignore,$cdom,$cnum,$marker) = split(m{/},$uri);
+                $marker=~s/\D//g;
+                if ($marker) {
+                    my %toolsettings=&get('exttool_'.$marker,['gradable'],$cdom,$cnum);
+                    $use_passback = $toolsettings{'gradable'};
+                }
+            }
+            if ($use_passback) {
+                $filename = '/home/httpd/html/res/lib/templates/LTIpassback.tool';
+            } else {
+                $filename = '/home/httpd/html/res/lib/templates/LTIstandard.tool';
+            }
+        }
+    }
+
     {
 # Imported parts would go here
         my @origfiletagids=();
@@ -12481,7 +13155,7 @@ sub metadata {
 
 			if ($depthcount<20) {
 			    my $metadata = 
-				&metadata($uri,'keys', $location,$unikey,
+				&metadata($uri,'keys',$toolsymb,$location,$unikey,
 					  $depthcount+1);
 			    foreach my $meta (split(',',$metadata)) {
 				$metaentry{':'.$meta}=$metaentry{':'.$meta};
@@ -12556,7 +13230,7 @@ sub metadata {
 		$dir=~s|[^/]*$||;
 		$location=&filelocation($dir,$location);
 		my $rights_metadata =
-		    &metadata($uri,'keys',$location,'_rights',
+		    &metadata($uri,'keys',$toolsymb,$location,'_rights',
 			      $depthcount+1);
 		foreach my $rights (split(',',$rights_metadata)) {
 		    #$metaentry{':'.$rights}=$metacache{$uri}->{':'.$rights};
@@ -12825,11 +13499,68 @@ sub get_coursechange {
 }
 
 sub devalidate_coursechange_cache {
-    my ($cnum,$cdom)=@_;
-    my $hashid=$cnum.':'.$cdom;
+    my ($cdom,$cnum)=@_;
+    my $hashid=$cdom.'_'.$cnum;
     &devalidate_cache_new('crschange',$hashid);
 }
 
+sub get_suppchange {
+    my ($cdom,$cnum) = @_;
+    if ($cdom eq '' || $cnum eq '') {
+        return unless ($env{'request.course.id'});
+        $cnum = $env{'course.'.$env{'request.course.id'}.'.num'};
+        $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'};
+    }
+    my $hashid=$cdom.'_'.$cnum;
+    my ($change,$cached)=&is_cached_new('suppchange',$hashid);
+    if ((defined($cached)) && ($change ne '')) {
+        return $change;
+    } else {
+        my %crshash = &get('environment',['internal.supplementalchange'],$cdom,$cnum);
+        if ($crshash{'internal.supplementalchange'} eq '') {
+            $change = $env{'course.'.$cdom.'_'.$cnum.'.internal.created'};
+            if ($change eq '') {
+                %crshash = &get('environment',['internal.created'],$cdom,$cnum);
+                $change = $crshash{'internal.created'};
+            }
+        } else {
+            $change = $crshash{'internal.supplementalchange'};
+        }
+        my $cachetime = 600;
+        &do_cache_new('suppchange',$hashid,$change,$cachetime);
+    }
+    return $change;
+}
+
+sub devalidate_suppchange_cache {
+    my ($cdom,$cnum)=@_;
+    my $hashid=$cdom.'_'.$cnum;
+    &devalidate_cache_new('suppchange',$hashid);
+}
+
+sub update_supp_caches {
+    my ($cdom,$cnum) = @_;
+    my %servers = &internet_dom_servers($cdom);
+    my @ids=&current_machine_ids();
+    foreach my $server (keys(%servers)) {
+        next if (grep(/^\Q$server\E$/,@ids));
+        my $hashid=$cnum.':'.$cdom;
+        my $cachekey = &escape('showsupp').':'.&escape($hashid);
+        &remote_devalidate_cache($server,[$cachekey]);
+    }
+    &has_unhidden_suppfiles($cnum,$cdom,1,1);
+    &count_supptools($cnum,$cdom,1);
+    my $now = time;
+    if ($env{'request.course.id'} eq $cdom.'_'.$cnum) {
+        &Apache::lonnet::appenv({'request.course.suppupdated' => $now});
+    }
+    &put('environment',{'internal.supplementalchange' => $now},
+         $cdom,$cnum);
+    &Apache::lonnet::appenv(
+        {'course.'.$cdom.'_'.$cnum.'.internal.supplementalchange' => $now});
+    &do_cache_new('suppchange',$cdom.'_'.$cnum,$now,600);
+}
+
 # ------------------------------------------------- Update symbolic store links
 
 sub symblist {
@@ -13016,17 +13747,10 @@ sub symbread {
     my %bighash;
     my $syval='';
     if (($env{'request.course.fn'}) && ($thisfn)) {
-        my $targetfn = $thisfn;
-        if ( ($thisfn =~ m/^(uploaded|editupload)\//) && ($thisfn !~ m/\.(page|sequence)$/) ) {
-            $targetfn = 'adm/wrapper/'.$thisfn;
-        }
-	if ($targetfn =~ m|^adm/wrapper/(ext/.*)|) {
-	    $targetfn=$1;
-	}
         unless ($ignoresymbdb) {
             if (tie(%hash,'GDBM_File',$env{'request.course.fn'}.'_symb.db',
                           &GDBM_READER(),0640)) {
-	        $syval=$hash{$targetfn};
+	        $syval=$hash{$thisfn};
                 untie(%hash);
             }
             if ($syval && $checkforblock) {
@@ -15564,10 +16288,6 @@ data base, returning a hash that is keye
 values that are the resource value.  I believe that the timestamps and
 versions are also returned.
 
-get_numsuppfiles($cnum,$cdom) : retrieve number of files in a course's
-supplemental content area. This routine caches the number of files for
-10 minutes.
-
 =back
 
 =head2 Course Modification
@@ -15735,10 +16455,14 @@ condval($condidx) : value of condition i
 
 =item *
 
-metadata($uri,$what,$liburi,$prefix,$depthcount) : request a
+metadata($uri,$what,$toolsymb,$liburi,$prefix,$depthcount) : request a
 resource's metadata, $what should be either a specific key, or either
 'keys' (to get a list of possible keys) or 'packages' to get a list of
-packages that this resource currently uses, the last 3 arguments are only used internally for recursive metadata.
+packages that this resource currently uses, the last 3 arguments are
+only used internally for recursive metadata.
+
+the toolsymb is only used where the uri is for an external tool (for which
+the uri as well as the symb are guaranteed to be unique).
 
 this function automatically caches all requests