--- loncom/lonnet/perl/lonnet.pm	2022/10/22 17:24:55	1.1497
+++ loncom/lonnet/perl/lonnet.pm	2024/12/27 02:32:55	1.1532
@@ -1,7 +1,7 @@
 # The LearningOnline Network
 # TCP networking package
 #
-# $Id: lonnet.pm,v 1.1497 2022/10/22 17:24:55 raeburn Exp $
+# $Id: lonnet.pm,v 1.1532 2024/12/27 02:32:55 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -188,7 +188,11 @@ sub create_connection {
 				     Type    => SOCK_STREAM,
 				     Timeout => 10);
     return 0 if (!$client);
-    print $client (join(':',$hostname,$lonid,&machine_ids($hostname),$loncaparevs{$lonid})."\n");
+    if ($loncaparevs{$lonid} =~ /^(\d+\.\d+\.[\w.]+)-\d+$/) {
+        print $client (join(':',$hostname,$lonid,$1,&machine_ids($hostname))."\n");
+    } else {
+        print $client (join(':',$hostname,$lonid,&machine_ids($hostname))."\n");
+    }
     my $result = <$client>;
     chomp($result);
     return 1 if ($result eq 'done');
@@ -224,7 +228,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);
@@ -415,6 +419,63 @@ sub remote_devalidate_cache {
     return $response;
 }
 
+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)=@_;
@@ -2696,9 +2757,11 @@ sub get_domain_defaults {
          &get_dom('configuration',['defaults','quotas',
                                   'requestcourses','inststatus',
                                   'coursedefaults','usersessions',
-                                  'requestauthor','selfenrollment',
-                                  'coursecategories','ssl','autoenroll',
-                                  'trust','helpsettings','wafproxy','ltisec'],$domain);
+                                  'requestauthor','authordefaults',
+                                  'selfenrollment','coursecategories',
+                                  'ssl','autoenroll','trust',
+                                  'helpsettings','wafproxy',
+                                  'ltisec','toolsec','privacy'],$domain);
     my @coursetypes = ('official','unofficial','community','textbook','placement');
     if (ref($domconfig{'defaults'}) eq 'HASH') {
         $domdefaults{'lang_def'} = $domconfig{'defaults'}{'lang_def'}; 
@@ -2724,7 +2787,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};
@@ -2742,6 +2805,17 @@ sub get_domain_defaults {
     if (ref($domconfig{'requestauthor'}) eq 'HASH') {
         $domdefaults{'requestauthor'} = $domconfig{'requestauthor'};
     }
+    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{'inststatus'}) eq 'HASH') {
         foreach my $item ('inststatustypes','inststatusorder','inststatusguest') {
             $domdefaults{$item} = $domconfig{'inststatus'}{$item};
@@ -2755,6 +2829,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') {
@@ -2764,12 +2841,30 @@ 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'}{'crsauthor'}) eq 'HASH') {
+                $domdefaults{$type.'crsauthor'} = $domconfig{'coursedefaults'}{'crsauthor'}{$type};
+            } else {
+                $domdefaults{$type.'crsauthor'} = 1;
+            }
         }
         if (ref($domconfig{'coursedefaults'}{'canclone'}) eq 'HASH') {
             if (ref($domconfig{'coursedefaults'}{'canclone'}{'instcode'}) eq 'ARRAY') {
@@ -2884,7 +2979,44 @@ sub get_domain_defaults {
         }
         if (ref($domconfig{'ltisec'}{'private'}) eq 'HASH') {
             if (ref($domconfig{'ltisec'}{'private'}{'keys'}) eq 'ARRAY') {
-                $domdefaults{'privhosts'} = $domconfig{'ltisec'}{'private'}{'keys'};
+                $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'};
+            }
+        }
+    }
+    if (ref($domconfig{'privacy'}) eq 'HASH') {
+        if (ref($domconfig{'privacy'}{'approval'}) eq 'HASH') {
+            foreach my $domtype ('instdom','extdom') {
+                if (ref($domconfig{'privacy'}{'approval'}{$domtype}) eq 'HASH') {
+                    foreach my $roletype ('domain','author','course','community') {
+                        if ($domconfig{'privacy'}{'approval'}{$domtype}{$roletype} eq 'user') {
+                            $domdefaults{'userapprovals'} = 1;
+                            last;
+                        }
+                    }
+                }
+                last if ($domdefaults{'userapprovals'});
             }
         }
     }
@@ -2924,6 +3056,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,
@@ -3822,6 +3955,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 = &allowed('mdc',$env{'request.course.id'});
         if ($group ne '') {
@@ -3856,10 +4012,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;
+                }
             }
         }
     }
@@ -3885,7 +4046,7 @@ sub can_edit_resource {
                     $forceedit = 1;
                 }
                 $cfile = $resurl;
-            } elsif (($resurl ne '') && (&is_on_map($resurl))) { 
+            } elsif (($resurl ne '') && (&is_on_map($resurl))) {
                 if ($resurl =~ m{^/adm/$match_domain/$match_username/\d+/smppg|bulletinboard$}) {
                     $incourse = 1;
                     if ($env{'form.forceedit'}) {
@@ -4268,7 +4429,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
@@ -4280,8 +4441,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
@@ -4491,11 +4652,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; 
+            }
         }
     }
  
@@ -4727,6 +4901,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 '') &&
@@ -5090,7 +5288,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.
 #
@@ -5301,7 +5499,8 @@ sub userrolelog {
 }
 
 sub courserolelog {
-    my ($trole,$username,$domain,$area,$tstart,$tend,$delflag,$selfenroll,$context)=@_;
+    my ($trole,$username,$domain,$area,$tstart,$tend,$delflag,$selfenroll,
+        $context,$othdomby,$requester)=@_;
     if ($area =~ m-^/($match_domain)/($match_courseid)/?([^/]*)-) {
         my $cdom = $1;
         my $cnum = $2;
@@ -5314,11 +5513,51 @@ sub courserolelog {
                            selfenroll => $selfenroll,
                            context    => $context,
                         );
+        if ($othdomby) {
+            if ($othdomby eq 'othdombydc') {
+                $storehash{'approval'} = 'domain';
+            } elsif ($othdomby eq 'othdombyuser') {
+                $storehash{'approval'} = 'user'; 
+            }
+            if ($requester ne '') {
+                $storehash{'requester'} = $requester;
+            }
+        }
         if ($trole eq 'gr') {
             $namespace = 'groupslog';
             $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);
@@ -5330,7 +5569,8 @@ sub courserolelog {
 }
 
 sub domainrolelog {
-    my ($trole,$username,$domain,$area,$tstart,$tend,$delflag,$context)=@_;
+    my ($trole,$username,$domain,$area,$tstart,$tend,$delflag,
+        $context,$othdomby,$requester)=@_;
     if ($area =~ m{^/($match_domain)/$}) {
         my $cdom = $1;
         my $domconfiguser = &get_domainconfiguser($cdom);
@@ -5341,6 +5581,16 @@ sub domainrolelog {
                            end     => $tend,
                            context => $context,
                         );
+        if ($othdomby) {
+            if ($othdomby eq 'othdombydc') {
+                $storehash{'approval'} = 'domain';
+            } elsif ($othdomby eq 'othdombyuser') {
+                $storehash{'approval'} = 'user';
+            }
+            if ($requester ne '') {
+                $storehash{'requester'} = $requester;
+            }
+        }
         &write_log('domain',$namespace,\%storehash,$delflag,$username,
                    $domain,$domconfiguser,$cdom);
     }
@@ -5349,7 +5599,8 @@ sub domainrolelog {
 }
 
 sub coauthorrolelog {
-    my ($trole,$username,$domain,$area,$tstart,$tend,$delflag,$context)=@_;
+    my ($trole,$username,$domain,$area,$tstart,$tend,$delflag,
+        $context,$othdomby,$requester)=@_;
     if ($area =~ m{^/($match_domain)/($match_username)$}) {
         my $audom = $1;
         my $auname = $2;
@@ -5360,12 +5611,55 @@ sub coauthorrolelog {
                            end     => $tend,
                            context => $context,
                         );
+        if ($othdomby) {
+            if ($othdomby eq 'othdombydc') {
+                $storehash{'approval'} = 'domain';
+            } elsif ($othdomby eq 'othdombyuser') {
+                $storehash{'approval'} = 'user';
+            }
+            if ($requester ne '') {
+                $storehash{'requester'} = $requester;
+            }
+        }
         &write_log('author',$namespace,\%storehash,$delflag,$username,
                    $domain,$auname,$audom);
     }
     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));
@@ -5873,7 +6167,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);
@@ -6455,18 +6749,22 @@ sub store {
 # -------------------------------------------------------------- Critical Store
 
 sub cstore {
-    my ($storehash,$symb,$namespace,$domain,$stuname,$laststore) = @_;
+    my ($storehash,$symb,$namespace,$domain,$stuname,$laststore,$ip,$nolog) = @_;
     my $home='';
 
     if ($stuname) { $home=&homeserver($stuname,$domain); }
 
-    $symb=&symbclean($symb);
+    unless (($symb eq '_feedback') || ($symb eq '_discussion')) {
+        $symb=&symbclean($symb);
+    }
     if (!$symb) { unless ($symb=&symbread()) { return ''; } }
 
     if (!$domain) { $domain=$env{'user.domain'}; }
     if (!$stuname) { $stuname=$env{'user.name'}; }
 
-    &devalidate($symb,$stuname,$domain);
+    unless (($symb eq '_feedback') || ($symb eq '_discussion')) {
+        &devalidate($symb,$stuname,$domain);
+    }
 
     $symb=escape($symb);
     if (!$namespace) { 
@@ -6476,7 +6774,11 @@ sub cstore {
     }
     if (!$home) { $home=$env{'user.home'}; }
 
-    $$storehash{'ip'}=&get_requestor_ip();
+    if ($ip ne '') {
+        $$storehash{'ip'} = $ip;
+    } else {
+        $$storehash{'ip'} = &get_requestor_ip();
+    }
     $$storehash{'host'}=$perlvar{'lonHostID'};
 
     my $namevalue='';
@@ -6484,7 +6786,9 @@ sub cstore {
         $namevalue.=&escape($key).'='.&freeze_escape($$storehash{$key}).'&';
     }
     $namevalue=~s/\&$//;
-    &courselog($symb.':'.$stuname.':'.$domain.':CSTORE:'.$namevalue);
+    unless ($nolog) {
+        &courselog($symb.':'.$stuname.':'.$domain.':CSTORE:'.$namevalue);
+    }
     return critical
                 ("store:$domain:$stuname:$namespace:$symb:$namevalue:$laststore","$home");
 }
@@ -6740,7 +7044,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);
@@ -6754,6 +7058,8 @@ sub rolesinit {
 
     my %allroles=();
     my %allgroups=();
+    my %gotcoauconfig=();
+    my %domdefaults=();
 
     for my $area (grep { ! /^rolesdef_/ } keys(%rolesdump)) {
         my $role = $rolesdump{$area};
@@ -6805,6 +7111,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;
@@ -6833,7 +7170,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 {
@@ -7727,7 +8064,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);
@@ -7736,11 +8073,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;
             }
@@ -7762,6 +8107,8 @@ sub get_portfolio_access {
                 push(@groups,$key);
             } elsif ($scope eq 'ip') {
                 push(@ips,$key);
+            } elsif ($scope eq 'userip') {
+                push(@userips,$key);
             }
         }
         if ($public) {
@@ -7779,6 +8126,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) {
@@ -7954,6 +8314,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);
@@ -7973,12 +8344,17 @@ sub usertools_access {
         %tools = (
                       requestauthor => 1,
                  );
+    } elsif ($context eq 'authordefaults') {
+        %tools = (
+                      webdav    => 1,
+                 );
     } else {
         %tools = (
                       aboutme   => 1,
                       blog      => 1,
                       webdav    => 1,
                       portfolio => 1,
+                      portaccess => 1,
                       timezone  => 1,
                  );
     }
@@ -7995,6 +8371,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};
             }
@@ -8003,7 +8383,11 @@ sub usertools_access {
 
     my ($toolstatus,$inststatus,$envkey);
     if ($context eq 'requestauthor') {
-        $envkey = $context; 
+        $envkey = $context;
+    } elsif ($context eq 'authordefaults') {
+        if ($tool eq 'webdav') {
+            $envkey = 'tools.'.$tool;
+        }
     } else {
         $envkey = $context.'.'.$tool;
     }
@@ -8115,7 +8499,8 @@ sub is_course_owner {
 }
 
 sub is_advanced_user {
-    my ($udom,$uname) = @_;
+    my ($udom,$uname,$nocache) = @_;
+    my ($is_adv,$is_author,$use_cache,$hashid);
     if ($udom ne '' && $uname ne '') {
         if (($udom eq $env{'user.domain'}) && ($uname eq $env{'user.name'})) {
             if (wantarray) {
@@ -8123,11 +8508,21 @@ sub is_advanced_user {
             } else {
                 return $env{'user.adv'};
             }
+        } elsif (!$nocache) {
+            $use_cache = 1;
+            $hashid = "$udom:$uname";  
+            my ($info,$cached)=&is_cached_new('isadvau',$hashid);
+            if ($cached) {
+                ($is_adv,$is_author) = split(/:/,$info);
+                if (wantarray) {
+                    return ($is_adv,$is_author);
+                }
+                return $is_adv; 
+            }
         }
     }
     my %roleshash = &get_my_roles($uname,$udom,'userroles',undef,undef,undef,1);
     my %allroles;
-    my ($is_adv,$is_author);
     foreach my $role (keys(%roleshash)) {
         my ($trest,$tdomain,$trole,$sec) = split(/:/,$role);
         my $area = '/'.$tdomain.'/'.$trest;
@@ -8158,6 +8553,10 @@ sub is_advanced_user {
             }
         }
     }
+    if ($use_cache) {
+        my $cachetime = 600;
+        &do_cache_new('isadvau',$hashid,$is_adv.':'.$is_author,$cachetime);
+    }
     if (wantarray) {
         return ($is_adv,$is_author);
     }
@@ -8562,7 +8961,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
@@ -8571,6 +8970,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;
     }
@@ -8582,6 +8988,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') {
@@ -8902,22 +9314,41 @@ sub constructaccess {
        if (exists($env{'user.priv.au./'.$ownerdomain.'/./'})) {
           return ($ownername,$ownerdomain,$ownerhome);
        }
-    } else {
-# Co-author for this?
-        if (exists($env{'user.priv.ca./'.$ownerdomain.'/'.$ownername.'./'}) ||
-            exists($env{'user.priv.aa./'.$ownerdomain.'/'.$ownername.'./'}) ) {
-            $ownerhome = &homeserver($ownername,$ownerdomain);
-            return ($ownername,$ownerdomain,$ownerhome);
-        }
+    } elsif (&is_course($ownerdomain,$ownername)) {
+# Course Authoring Space?
         if ($env{'request.course.id'}) {
             if (($ownername eq $env{'course.'.$env{'request.course.id'}.'.num'}) &&
                 ($ownerdomain eq $env{'course.'.$env{'request.course.id'}.'.domain'})) {
                 if (&allowed('mdc',$env{'request.course.id'})) {
+                    return if ($env{'course.'.$env{'request.course.id'}.'.internal.crsauthor'} eq '0');
+                    unless ($env{'course.'.$env{'request.course.id'}.'.internal.crsauthor'}) {
+                        my %domdefs = &get_domain_defaults($ownerdomain);
+                        my $type = lc($env{'course.'.$env{'request.course.id'}.'.type'});
+                        unless (($type eq 'community') || ($type eq 'placement')) {
+                            $type = 'unofficial';
+                            if ($env{'course.'.$env{'request.course.id'}.'internal.coursecode'} ne '') {
+                                $type = 'official';
+                            } elsif ($env{'course.'.$env{'request.course.id'}.'internal.textbook'} ne '') {
+                                $type = 'textbook';
+                            } else {
+                                $type = 'unofficial';
+                            }
+                        }
+                        return if ($domdefs{$type.'crsauthor'} eq '0');
+                    }
                     $ownerhome = $env{'course.'.$env{'request.course.id'}.'.home'};
                     return ($ownername,$ownerdomain,$ownerhome);
                 }
             }
         }
+        return '';
+    } else {
+# Co-author for this?
+        if (exists($env{'user.priv.ca./'.$ownerdomain.'/'.$ownername.'./'}) ||
+            exists($env{'user.priv.aa./'.$ownerdomain.'/'.$ownername.'./'}) ) {
+            $ownerhome = &homeserver($ownername,$ownerdomain);
+            return ($ownername,$ownerdomain,$ownerhome);
+        }
     }
 
 # We don't have any access right now. If we are not possibly going to do anything about this,
@@ -9984,7 +10415,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);
@@ -10066,7 +10497,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);
@@ -10196,11 +10627,13 @@ sub toggle_coursegroup_status {
 }
 
 sub modify_group_roles {
-    my ($cdom,$cnum,$group_id,$user,$end,$start,$userprivs,$selfenroll,$context) = @_;
+    my ($cdom,$cnum,$group_id,$user,$end,$start,$userprivs,$selfenroll,$context,
+        $othdomby,$requester) = @_;
     my $url = '/'.$cdom.'/'.$cnum.'/'.$group_id;
     my $role = 'gr/'.&escape($userprivs);
     my ($uname,$udom) = split(/:/,$user);
-    my $result = &assignrole($udom,$uname,$url,$role,$end,$start,'',$selfenroll,$context);
+    my $result = &assignrole($udom,$uname,$url,$role,$end,$start,'',$selfenroll,$context,
+                             $othdomby,$requester);
     if ($result eq 'ok') {
         &devalidate_getgroups_cache($udom,$uname,$cdom,$cnum);
     }
@@ -10328,43 +10761,66 @@ sub plaintext {
 
 sub assignrole {
     my ($udom,$uname,$url,$role,$end,$start,$deleteflag,$selfenroll,
-        $context)=@_;
-    my $mrole;
+        $context,$othdomby,$requester,$reqsec,$reqrole)=@_;
+    my ($mrole,$rolelogcontext);
     if ($role =~ /^cr\//) {
         my $cwosec=$url;
         $cwosec=~s/^\/($match_domain)\/($match_courseid)\/.*/$1\/$2/;
-	unless (&allowed('ccr',$cwosec)) {
-           my $refused = 1;
-           if ($context eq 'requestcourses') {
-               if (($env{'user.name'} ne '') && ($env{'user.domain'} ne '')) {
-                   if ($role =~ m{^cr/($match_domain)/($match_username)/([^/]+)$}) {
-                       if (($1 eq $env{'user.domain'}) && ($2 eq $env{'user.name'})) {
-                           my ($cdom,$cnum) = ($cwosec =~ m{^/?($match_domain)/($match_courseid)$});
-                           my %crsenv = &userenvironment($cdom,$cnum,('internal.courseowner'));
-                           if ($crsenv{'internal.courseowner'} eq
-                               $env{'user.name'}.':'.$env{'user.domain'}) {
-                               $refused = '';
-                           }
-                       }
-                   }
-               }
-           }
-           if ($refused) {
-               &logthis('Refused custom assignrole: '.
-                        $udom.' '.$uname.' '.$url.' '.$role.' '.$end.' '.$start.
-                        ' by '.$env{'user.name'}.' at '.$env{'user.domain'});
-               return 'refused';
-           }
+        if ((!&allowed('ccr',$cwosec)) && (!&allowed('ccr',$udom))) {
+            my $refused = 1;
+            if ($context eq 'requestcourses') {
+                if (($env{'user.name'} ne '') && ($env{'user.domain'} ne '')) {
+                    if ($role =~ m{^cr/($match_domain)/($match_username)/([^/]+)$}) {
+                        if (($1 eq $env{'user.domain'}) && ($2 eq $env{'user.name'})) {
+                            my ($cdom,$cnum) = ($cwosec =~ m{^/?($match_domain)/($match_courseid)$});
+                            my %crsenv = &userenvironment($cdom,$cnum,('internal.courseowner'));
+                            if ($crsenv{'internal.courseowner'} eq
+                                $env{'user.name'}.':'.$env{'user.domain'}) {
+                                $refused = '';
+                            }
+                        }
+                    }
+                }
+            } elsif (($context eq 'course') && ($othdomby eq 'othdombyuser')) {
+                my ($cdom,$cnum) = ($cwosec =~ m{^/?($match_domain)/($match_courseid)$});
+                my ($sec) = ($url =~ m{^/\Q$cwosec\E/(.*)$});
+                my $key = "$uname:$udom:$role:$sec";
+                my %queuedrolereq = &Apache::lonnet::get('nohist_othdomqueued',[$key],$cdom,$cnum);
+                if ((exists($queuedrolereq{$key})) && (ref($queuedrolereq{$key}) eq 'HASH')) {
+                    if (($queuedrolereq{$key}{'adj'} eq 'user') && ($queuedrolereq{$key}{'requester'} eq $requester)) {
+                        $refused = '';
+                    }
+                }
+            }
+            if ($refused) {
+                &logthis('Refused custom assignrole: '.
+                         $udom.' '.$uname.' '.$url.' '.$role.' '.$end.' '.$start.
+                         ' by '.$env{'user.name'}.' at '.$env{'user.domain'});
+                return 'refused';
+            }
         }
         $mrole='cr';
     } elsif ($role =~ /^gr\//) {
         my $cwogrp=$url;
         $cwogrp=~s{^/($match_domain)/($match_courseid)/.*}{$1/$2};
-        unless (&allowed('mdg',$cwogrp)) {
-            &logthis('Refused group assignrole: '.
-              $udom.' '.$uname.' '.$url.' '.$role.' '.$end.' '.$start.' by '.
-                    $env{'user.name'}.' at '.$env{'user.domain'});
-            return 'refused';
+        if (!&allowed('mdg',$cwogrp)) {
+            my $refused = 1;
+            if (($refused) && ($othdomby eq 'othdombyuser') && ($requester ne '') && ($reqrole ne '')) {
+                my ($cdom,$cnum) = ($cwogrp =~ m{^/?($match_domain)/($match_courseid)$});
+                my $key = "$uname:$udom:$reqrole:$reqsec";
+                my %queuedrolereq = &Apache::lonnet::get('nohist_othdomqueued',[$key],$cdom,$cnum);
+                if ((exists($queuedrolereq{$key})) && (ref($queuedrolereq{$key}) eq 'HASH')) {
+                    if (($queuedrolereq{$key}{'adj'} eq 'user') && ($queuedrolereq{$key}{'requester'} eq $requester)) {
+                        $refused = '';
+                    }
+                }
+            }
+            if ($refused) {
+                &logthis('Refused group assignrole: '.
+                         $udom.' '.$uname.' '.$url.' '.$role.' '.$end.' '.$start.' by '.
+                         $env{'user.name'}.' at '.$env{'user.domain'});
+                return 'refused';
+            }
         }
         $mrole='gr';
     } else {
@@ -10381,7 +10837,8 @@ sub assignrole {
             }
             if ($refused) {
                 my ($cdom,$cnum) = ($cwosec =~ m{^/?($match_domain)/($match_courseid)$});
-                if (!$selfenroll && (($context eq 'course') || ($context eq 'ltienroll' && $env{'request.lti.login'}))) {
+                if (!$selfenroll && ($othdomby ne 'othdombyuser') &&
+                   (($context eq 'course') || ($context eq 'ltienroll' && $env{'request.lti.login'}))) {
                     my %crsenv;
                     if ($role eq 'cc' || $role eq 'co') {
                         %crsenv = &userenvironment($cdom,$cnum,('internal.courseowner'));
@@ -10407,6 +10864,49 @@ sub assignrole {
                     } elsif (($context eq 'ltienroll') && ($env{'request.lti.login'})) {
                         $refused = '';
                     }
+                } elsif ($othdomby eq 'othdombyuser') {
+                    my ($key,%queuedrolereq);
+                    if ($context eq 'course') {
+                        my ($sec) = ($url =~ m{^/\Q$cwosec\E/(.*)$});
+                        $key = "$uname:$udom:$role:$sec";
+                        %queuedrolereq = &Apache::lonnet::get('nohist_othdomqueued',[$key],$cdom,$cnum);
+                        if ((exists($queuedrolereq{$key})) && (ref($queuedrolereq{$key}) eq 'HASH')) {
+                            if (($queuedrolereq{$key}{'adj'} eq 'user') && ($queuedrolereq{$key}{'requester'} eq $requester)) {
+                                if ((($role eq 'cc') && ($cnum !~ /^$match_community$/)) || 
+                                    (($role eq 'co') && ($cnum =~ /^$match_community$/))) {
+                                    my %crsenv = &userenvironment($cdom,$cnum,('internal.courseowner'));
+                                    if ($crsenv{'internal.courseowner'} eq $requester) {
+                                        $refused = '';
+                                    }
+                                } elsif ($role =~ /^(?:in|ta|ep|st)$/) {
+                                    $refused = '';
+                                }
+                            }
+                        }
+                    } elsif (($context eq 'author') && ($role =~ /^ca|aa$/)) {
+                        my $key = "$uname:$udom:$role"; 
+                        my ($audom,$auname) = ($url =~ m{^/($match_domain)/($match_username)$});
+                        if (($audom ne '') && ($auname ne '')) {
+                            my %queuedrolereq = &Apache::lonnet::get('nohist_othdomqueued',[$key],$audom,$auname);
+                            if ((exists($queuedrolereq{$key})) && (ref($queuedrolereq{$key}) eq 'HASH')) {
+                                if (($queuedrolereq{$key}{'adj'} eq 'user') && ($queuedrolereq{$key}{'requester'} eq $requester)) {
+                                    $refused = '';
+                                }
+                            }
+                        }
+                    } elsif (($context eq 'domain') && ($role ne 'dc') && ($role ne 'su')) {
+                        my $key = "$uname:$udom:$role";
+                        my ($roledom) = ($url =~ m{^/($match_domain)/\Q$role\E$});
+                        if ($roledom ne '') {
+                            my $confname = $roledom.'-domainconfig';
+                            my %queuedrolereq = &Apache::lonnet::get('nohist_othdomqueued',[$key],$roledom,$confname);
+                            if ((exists($queuedrolereq{$key})) && (ref($queuedrolereq{$key}) eq 'HASH')) {
+                                if (($queuedrolereq{$key}{'adj'} eq 'user') && ($queuedrolereq{$key}{'requester'} eq $requester)) {
+                                    $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 '')) {
@@ -10459,6 +10959,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.
@@ -10519,15 +11028,18 @@ sub assignrole {
                                                  $origstart,$selfenroll,$context);
             }
             &courserolelog($role,$uname,$udom,$url,$origstart,$origend,$delflag,
-                           $selfenroll,$context);
+                           $selfenroll,$context,$othdomby,$requester);
         } elsif (($role eq 'li') || ($role eq 'dg') || ($role eq 'sc') ||
                  ($role eq 'au') || ($role eq 'dc') || ($role eq 'dh') ||
                  ($role eq 'da')) {
             &domainrolelog($role,$uname,$udom,$url,$origstart,$origend,$delflag,
-                           $context);
+                           $context,$othdomby,$requester);
         } elsif (($role eq 'ca') || ($role eq 'aa')) {
+            if ($rolelogcontext eq '') {
+                $rolelogcontext = $context;
+            }
             &coauthorrolelog($role,$uname,$udom,$url,$origstart,$origend,$delflag,
-                             $context); 
+                             $rolelogcontext,$othdomby,$requester); 
         }
         if ($role eq 'cc') {
             &autoupdate_coowners($url,$end,$start,$uname,$udom);
@@ -10879,7 +11391,7 @@ sub modifystudent {
 
 sub modify_student_enrollment {
     my ($udom,$uname,$uid,$first,$middle,$last,$gene,$usec,$end,$start,$type,
-        $locktype,$cid,$selfenroll,$context,$credits,$instsec) = @_;
+        $locktype,$cid,$selfenroll,$context,$credits,$instsec,$othdomby,$requester) = @_;
     my ($cdom,$cnum,$chome);
     if (!$cid) {
 	unless ($cid=$env{'request.course.id'}) {
@@ -10940,7 +11452,7 @@ sub modify_student_enrollment {
 	$uurl.='/'.$usec;
     }
     my $result = &assignrole($udom,$uname,$uurl,'st',$end,$start,undef,
-                             $selfenroll,$context);
+                             $selfenroll,$context,$othdomby,$requester);
     if ($result ne 'ok') {
         if ($old_entry{$user} ne '') {
             $reply = &cput('classlist',\%old_entry,$cdom,$cnum);
@@ -11216,9 +11728,11 @@ sub store_userdata {
 # ---------------------------------------------------------- Assign Custom Role
 
 sub assigncustomrole {
-    my ($udom,$uname,$url,$rdom,$rnam,$rolename,$end,$start,$deleteflag,$selfenroll,$context)=@_;
+    my ($udom,$uname,$url,$rdom,$rnam,$rolename,$end,$start,$deleteflag,
+        $selfenroll,$context,$othdomby,$requester)=@_;
     return &assignrole($udom,$uname,$url,'cr/'.$rdom.'/'.$rnam.'/'.$rolename,
-                       $end,$start,$deleteflag,$selfenroll,$context);
+                       $end,$start,$deleteflag,$selfenroll,$context,$othdomby,
+                       $requester);
 }
 
 # ----------------------------------------------------------------- Revoke Role
@@ -12025,18 +12539,26 @@ sub stat_file {
 # or corresponding Published Resource Space, and populate the hash ref:
 # $dirhashref with URLs of all directories, and if $filehashref hash
 # ref arg is provided, the URLs of any files, excluding versioned, .meta,
-# or .rights files in resource space, and .meta, .save, .log, and .bak
-# files in Authoring Space.
+# or .rights files in resource space, and .meta, .save, .log, .bak and
+# .rights files in Authoring Space.
 #
 # Inputs:
 #
 # $is_home - true if current server is home server for user's space
-# $context - either: priv, or res respectively for Authoring or Resource Space.
-# $docroot - Document root (i.e., /home/httpd/html
+# $recurse - if true will also traverse subdirectories recursively
+# $include - reference to hash containing allowed file extensions.  If provided,
+#             files which do not have a matching extension will be ignored.
+# $exclude - reference to hash containing excluded file extensions.  If provided,
+#             files which have a matching extension will be ignored.
+# $nonemptydir - if true, will only populate $fileshashref hash entry for a particular
+#             directory with first file found (with acceptable extension).
+# $addtopdir - if true, set $dirhashref->{'/'} = 1 
 # $toppath - Top level directory (i.e., /res/$dom/$uname or /priv/$dom/$uname
 # $relpath - Current path (relative to top level).
 # $dirhashref - reference to hash to populate with URLs of directories (Required)
 # $filehashref - reference to hash to populate with URLs of files (Optional)
+# $getlastmod - if true, will set value for each key in innerhash in $filehashref
+#               to last modification time of file; value set to 1 otherwise.
 #
 # Returns: nothing
 #
@@ -12049,49 +12571,83 @@ sub stat_file {
 #
 
 sub recursedirs {
-    my ($is_home,$context,$docroot,$toppath,$relpath,$dirhashref,$filehashref) = @_;
+    my ($is_home,$recurse,$include,$exclude,$nonemptydir,$addtopdir,$toppath,
+        $relpath,$dirhashref,$filehashref,$getlastmod) = @_;
     return unless (ref($dirhashref) eq 'HASH');
+    my $docroot = $perlvar{'lonDocRoot'};
     my $currpath = $docroot.$toppath;
-    if ($relpath) {
+    if ($relpath ne '') {
         $currpath .= "/$relpath";
     }
-    my $savefile;
-    if (ref($filehashref)) {
+    my ($savefile,$checkinc,$checkexc);
+    if (ref($filehashref) eq 'HASH') {
         $savefile = 1;
     }
+    if (ref($include) eq 'HASH') {
+        $checkinc = 1;
+    }
+    if (ref($exclude) eq 'HASH') {
+        $checkexc = 1;
+    }
     if ($is_home) {
-        if (opendir(my $dirh,$currpath)) {
+        if ((-e $currpath) && (opendir(my $dirh,$currpath))) {
+            my $filecount = 0;
             foreach my $item (sort { lc($a) cmp lc($b) } grep(!/^\.+$/,readdir($dirh))) {
                 next if ($item eq '');
                 if (-d "$currpath/$item") {
                     my $newpath;
-                    if ($relpath) {
+                    if ($relpath ne '') {
                         $newpath = "$relpath/$item";
                     } else {
                         $newpath = $item;
                     }
                     $dirhashref->{&Apache::lonlocal::js_escape($newpath)} = 1;
-                    &recursedirs($is_home,$context,$docroot,$toppath,$newpath,$dirhashref,$filehashref);
-                } elsif ($savefile) {
-                    if ($context eq 'priv') {
-                        unless ($item =~ /\.(meta|save|log|bak|DS_Store)$/) {
-                            $filehashref->{&Apache::lonlocal::js_escape($relpath)}{$item} = 1;
+                    if ($recurse) {
+                        &recursedirs($is_home,$recurse,$include,$exclude,$nonemptydir,$addtopdir,
+                                     $toppath,$newpath,$dirhashref,$filehashref,$getlastmod);
+                    }
+                } elsif (($savefile) || ($relpath eq '')) {
+                    next if ($nonemptydir && $filecount);
+                    if ($checkinc || $checkexc) {
+                        my ($extension) = ($item =~ /\.(\w+)$/);
+                        if ($checkinc) {
+                            next unless ($extension && $include->{$extension});
+                        }
+                        if ($checkexc) {
+                            next if ($extension && $exclude->{$extension});
+                        }
+                    }
+                    if (($relpath eq '') && (!exists($dirhashref->{'/'}))) {
+                        $dirhashref->{'/'} = 1;
+                    }
+                    if ($savefile) {
+                        my $value;
+                        if ($getlastmod) {
+                            ($value) = (stat("$currpath/$item"))[9];
+                        } else {
+                            $value = 1;
                         }
-                    } else {
-                        unless (($item =~ /\.meta$/) || ($item =~ /\.\d+\.\w+$/) || ($item =~ /\.rights$/)) {
-                            $filehashref->{&Apache::lonlocal::js_escape($relpath)}{$item} = 1;
+                        if ($relpath eq '') {
+                            $filehashref->{'/'}{$item} = $value
+                        } else {
+                            $filehashref->{&Apache::lonlocal::js_escape($relpath)}{$item} = $value;
                         }
                     }
+                    $filecount ++;
                 }
             }
             closedir($dirh);
         }
     } else {
-        my ($dirlistref,$listerror) =
-            &dirlist($toppath.$relpath);
+        my $url = $toppath;
+        if ($relpath ne '') {
+            $url = $toppath.'/'.$relpath;
+        }
+        my ($dirlistref,$listerror) = &dirlist($url);
         my @dir_lines;
         my $dirptr=16384;
         if (ref($dirlistref) eq 'ARRAY') {
+            my $filecount = 0;
             foreach my $dir_line (sort
                               {
                                   my ($afile)=split('&',$a,2);
@@ -12107,28 +12663,72 @@ sub recursedirs {
                     if ($relpath) {
                         $newpath = "$relpath/$item";
                     } else {
-                        $relpath = '/';
                         $newpath = $item;
                     }
                     $dirhashref->{&Apache::lonlocal::js_escape($newpath)} = 1;
-                    &recursedirs($is_home,$context,$docroot,$toppath,$newpath,$dirhashref,$filehashref);
-                } elsif ($savefile) {
-                    if ($context eq 'priv') {
-                        unless ($item =~ /\.(meta|save|log|bak|DS_Store)$/) {
-                            $filehashref->{$relpath}{$item} = 1;
+                    if ($recurse) {
+                        &recursedirs($is_home,$recurse,$include,$exclude,$nonemptydir,$addtopdir,
+                                     $toppath,$newpath,$dirhashref,$filehashref,$getlastmod);
+                    }
+                } elsif (($savefile) || ($relpath eq '')) {
+                    next if ($nonemptydir && $filecount);
+                    if ($checkinc || $checkexc) {
+                        my ($extension) = ($item =~ /\.(\w+)$/);
+                        if ($checkinc) {
+                            next unless ($extension && $include->{$extension});
+                        }
+                        if ($checkexc) {
+                            next if ($extension && $exclude->{$extension});
+                        }
+                    }
+                    if (($relpath eq '') && (!exists($dirhashref->{'/'}))) {
+                        $dirhashref->{'/'} = 1;
+                    }
+                    if ($savefile) {
+                        my $value;
+                        if ($getlastmod) {
+                            $value = $mtime;
+                        } else {
+                            $value = 1;
                         }
-                    } else {
-                        unless (($item =~ /\.meta$/) || ($item =~ /\.\d+\.\w+$/)) {
-                            $filehashref->{$relpath}{$item} = 1;
+                        if ($relpath eq '') {
+                            $filehashref->{'/'}{$item} = $value;
+                        } else {
+                            $filehashref->{&Apache::lonlocal::js_escape($relpath)}{$item} = $value;
                         }
                     }
+                    $filecount ++; 
                 }
             }
         }
     }
+    if ($addtopdir) {
+        if (($relpath eq '') && (!exists($dirhashref->{'/'}))) {
+            $dirhashref->{'/'} = 1;
+        }
+    }
     return;
 }
 
+sub priv_exclude {
+    return {
+             meta => 1,
+             save => 1,
+             log => 1,
+             bak => 1,
+             rights => 1,
+             DS_Store => 1,
+           };
+}
+
+sub res_exclude {
+    return {
+             meta => 1,
+             subscription => 1,
+             rights => 1,
+           };
+}
+
 # -------------------------------------------------------- Value of a Condition
 
 # gets the value of a specific preevaluated condition
@@ -12352,13 +12952,11 @@ sub get_domain_lti {
     } else {
         return %lti;
     }
-
     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') {
@@ -12374,18 +12972,6 @@ sub get_domain_lti {
             } else {
                 %lti = %{$domconfig{$name}};
             }
-            if (($context eq 'consumer') && (keys(%lti))) {
-                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};
-                            }
-                        }
-                    }
-                }
-            }
         }
         my $cachetime = 24*60*60;
         &do_cache_new($cachename,$cdom,\%lti,$cachetime);
@@ -12394,20 +12980,29 @@ sub get_domain_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 {
@@ -12457,6 +13052,73 @@ sub domainlti_itemid {
     return $itemid;
 }
 
+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;
@@ -12468,7 +13130,7 @@ sub count_supptools {
         my $chome=&homeserver($cnum,$cdom);
         $numexttools = 0;
         unless ($chome eq 'no_host') {
-            my ($supplemental) = &get_supplemental($cnum,$cdom,$reload);
+            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'}})) {
@@ -12485,7 +13147,7 @@ sub count_supptools {
 }
 
 sub has_unhidden_suppfiles {
-    my ($cnum,$cdom,$ignorecache,$possdell)=@_;
+    my ($cnum,$cdom,$ignorecache,$possdel)=@_;
     my $hashid=$cnum.':'.$cdom;
     my ($showsupp,$cached);
     unless ($ignorecache) {
@@ -12494,7 +13156,7 @@ sub has_unhidden_suppfiles {
     unless (defined($cached)) {
         my $chome=&homeserver($cnum,$cdom);
         unless ($chome eq 'no_host') {
-            my ($supplemental) = &get_supplemental($cnum,$cdom,$ignorecache,$possdel);
+            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'}})) {
@@ -12517,35 +13179,6 @@ sub has_unhidden_suppfiles {
     return $showsupp;
 }
 
-sub get_supplemental {
-    my ($cnum,$cdom,$ignorecache,$possdel)=@_;
-    my $hashid=$cnum.':'.$cdom;
-    my ($supplemental,$cached,$set_httprefs);
-    unless ($ignorecache) {
-        ($supplemental,$cached) = &is_cached_new('supplemental',$hashid);
-    }
-    unless (defined($cached)) {
-        my $chome=&homeserver($cnum,$cdom);
-        unless ($chome eq 'no_host') {
-            my ($errors,%ids,%hidden);
-            $errors =
-                &Apache::loncommon::recurse_supplemental($cnum,$cdom,
-                                                         'supplemental.sequence',
-                                                         $errors,$possdel,\%ids,\%hidden);
-            $set_httprefs = 1;
-            if ($env{'request.course.id'} eq $cdom.'_'.$cnum) {
-                &Apache::lonnet::appenv({'request.course.suppupdated' => time});
-            }
-            $supplemental = {
-                               ids => \%ids,
-                               hidden => \%hidden,
-                            };
-            &do_cache_new('supplemental',$hashid,$supplemental,600);
-        }
-    }
-    return ($supplemental,$set_httprefs);
-}
-
 #
 # EXT resource caching routines
 #
@@ -14565,6 +15198,49 @@ sub repcopy_userfile {
     return 'ok';
 }
 
+sub repcopy_crsprivfile {
+    my ($src,$dest) = @_;
+    my $result;
+    if ($src =~ m{^/priv/($match_domain)/($match_courseid)/(.+)$}) {
+        my ($cdom,$cnum,$filepath) = ($1,$2,$3);
+        $filepath =~ s/\.{2,}//g;
+        my $chome = &homeserver($cnum,$cdom);
+        unless ($chome eq 'no_host') {
+            my @ids=&current_machine_ids();
+            unless (grep(/^\Q$chome\E$/,@ids)) {
+                if (&is_course($cdom,$cnum)) {
+                    my $londocroot = $perlvar{'lonDocRoot'};
+                    if ($dest =~ m{^\Q$londocroot/priv/\E$match_domain/$match_username/.*\Q$filepath\E$}) {
+                        my $cmd = 'crsfilefrompriv:'.&escape($filepath).':'.&escape($cnum).':'.&escape($cdom);
+                        $result = &reply($cmd,$chome);
+                        unless (($result eq 'unknown_cmd') || ($result =~ /^error:/)) {
+                            my $url = &unescape($result);
+                            if ($url =~ m{^https?://[^/]+\Q/userfiles/$cdom/$cnum/priv/$filepath\E$}) {
+                                my $request=new HTTP::Request('GET',$url);
+                                my $response=&LONCAPA::LWPReq::makerequest($chome,$request,'',\%perlvar,1200,1);
+                                if ($response->is_error()) {
+                                    $result = 'error: '.$response->status_line;
+                                } else {
+                                    if (open(my $fh,'>',$dest)) {
+                                        print $fh $response->content;
+                                        close($fh);
+                                        $result = 'ok';
+                                    } else {
+                                        $result = 'error: nowrite';
+                                    }
+                                }
+                            } else {
+                                $result = 'error: invalidurl';
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+    return $result;
+}
+
 sub tokenwrapper {
     my $uri=shift;
     $uri=~s|^https?\://([^/]+)||;