--- loncom/lonnet/perl/lonnet.pm	2017/11/30 14:41:38	1.1360
+++ loncom/lonnet/perl/lonnet.pm	2018/04/02 17:27:52	1.1373
@@ -1,7 +1,7 @@
 # The LearningOnline Network
 # TCP networking package
 #
-# $Id: lonnet.pm,v 1.1360 2017/11/30 14:41:38 raeburn Exp $
+# $Id: lonnet.pm,v 1.1373 2018/04/02 17:27:52 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -707,6 +707,7 @@ sub check_for_valid_session {
     if (ref($userhashref) eq 'HASH') {
         $userhashref->{'name'} = $disk_env{'user.name'};
         $userhashref->{'domain'} = $disk_env{'user.domain'};
+        $userhashref->{'lti'} = $disk_env{'request.lti.login'};
     }
 
     return $handle;
@@ -5228,6 +5229,9 @@ sub set_first_access {
                         'course.'.$courseid.'.timerinterval.'.$res => $interval,
                      }
                   );
+            if (($cachedtime) && (abs($start-$cachedtime) < 5)) {
+                $cachedtimes{"$courseid\0$res"} = $start;
+            }
         }
         return $putres;
     }
@@ -7142,6 +7146,7 @@ sub usertools_access {
                       community  => 1,
                       textbook   => 1,
                       placement  => 1,
+                      lti        => 1,
                  );
     } elsif ($context eq 'requestauthor') {
         %tools = (
@@ -7338,25 +7343,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) {
@@ -7373,7 +7382,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;
@@ -7503,7 +7512,10 @@ sub allowed {
 # Free bre to public access
 
     if ($priv eq 'bre') {
-        my $copyright=&metadata($uri,'copyright');
+        my $copyright;
+        unless ($uri =~ /ext\.tool/) {
+            $copyright=&metadata($uri,'copyright');
+        }
 	if (($copyright eq 'public') && (!$env{'request.course.id'})) { 
            return 'F'; 
         }
@@ -8885,6 +8897,33 @@ sub auto_validate_class_sec {
     return $response;
 }
 
+sub auto_validate_instclasses {
+    my ($cdom,$cnum,$owners,$classesref) = @_;
+    my ($homeserver,%validations);
+    $homeserver = &homeserver($cnum,$cdom);
+    unless ($homeserver eq 'no_host') {
+        my $ownerlist;
+        if (ref($owners) eq 'ARRAY') {
+            $ownerlist = join(',',@{$owners});
+        } else {
+            $ownerlist = $owners;
+        }
+        if (ref($classesref) eq 'HASH') {
+            my $classes = &freeze_escape($classesref);
+            my $response=&reply('autovalidateinstclasses:'.&escape($ownerlist).
+                                ':'.$cdom.':'.$classes,$homeserver);
+            unless ($response =~ /(con_lost|error|no_such_host|refused)/) {
+                my @items = split(/&/,$response);
+                foreach my $item (@items) {
+                    my ($key,$value) = split('=',$item);
+                    $validations{&unescape($key)} = &thaw_unescape($value);
+                }
+            }
+        }
+    }
+    return %validations;
+}
+
 sub auto_crsreq_update {
     my ($cdom,$cnum,$crstype,$action,$ownername,$ownerdomain,$fullname,$title,
         $code,$accessstart,$accessend,$inbound) = @_;
@@ -9264,8 +9303,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'})) {
+                        $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 '')) {
@@ -9531,10 +9574,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;
@@ -11197,10 +11244,11 @@ sub get_numsuppfiles {
     unless (defined($cached)) {
         my $chome=&homeserver($cnum,$cdom);
         unless ($chome eq 'no_host') {
-            ($suppcount,my $errors) = (0,0);
+            ($suppcount,my $supptools,my $errors) = (0,0,0);
             my $suppmap = 'supplemental.sequence';
-            ($suppcount,$errors) = 
-                &Apache::loncommon::recurse_supplemental($cnum,$cdom,$suppmap,$suppcount,$errors);
+            ($suppcount,$supptools,$errors) =
+                &Apache::loncommon::recurse_supplemental($cnum,$cdom,$suppmap,$suppcount,
+                                                         $supptools,$errors);
         }
         &do_cache_new('suppcount',$hashid,$suppcount,600);
     }
@@ -11490,9 +11538,13 @@ sub EXT {
 	} else {
 	    $filename=$env{'request.filename'};
 	}
-	my $metadata=&metadata($filename,$what);
+        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_'.$what);
+	$metadata=&metadata($filename,'parameter_'.$what,$toolsymb);
 	if (defined($metadata)) { return &get_reply([$metadata,'resource']); }
 
 # ----------------------------------------------- fifth, look in rest of course
@@ -11518,7 +11570,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') {
@@ -11612,11 +11664,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;
@@ -11683,8 +11735,9 @@ sub add_prefix_and_part {
 
 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 '') || 
@@ -11708,11 +11761,72 @@ 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 %importedids=();
-        my @origfileimportpartids=();
+        my @origfiletagids=();
         my $importedparts=0;
+
+# Imported responseids would go here
+        my $importedresponses=0;
 #
 # Is this a recursive call for a library?
 #
@@ -11807,8 +11921,45 @@ sub metadata {
                         my $dir=$filename;
                         $dir=~s|[^/]*$||;
                         $location=&filelocation($dir,$location);
-                       
+
+                        my $importid=$token->[2]->{'id'};
                         my $importmode=$token->[2]->{'importmode'};
+#
+# Check metadata for imported file to
+# see if it contained response items
+#
+                        my ($origfile,@libfilekeys);
+                        my %currmetaentry = %metaentry;
+                        @libfilekeys = split(/,/,&metadata($location,'keys',undef,undef,undef,
+                                                           $depthcount+1));
+                        if (grep(/^responseorder$/,@libfilekeys)) {
+                            my $libresponseorder = &metadata($location,'responseorder',undef,undef,
+                                                             undef,$depthcount+1);
+                            if ($libresponseorder ne '') {
+                                if ($#origfiletagids<0) {
+                                    undef(%importedrespids);
+                                    undef(%importedpartids);
+                                }
+                                my @respids = split(/\s*,\s*/,$libresponseorder);
+                                if (@respids) {
+                                    $importedrespids{$importid} = join(',',map { $importid.'_'.$_ } @respids);
+                                }
+                                if ($importedrespids{$importid} ne '') {
+                                    $importedresponses = 1;
+# We need to get the original file and the imported file to get the response order correct
+# Load and inspect original file
+                                    if ($#origfiletagids<0) {
+                                        my $origfilelocation=$perlvar{'lonDocRoot'}.&clutter($uri);
+                                        $origfile=&getfile($origfilelocation);
+                                        @origfiletagids=($origfile=~/<((?:\w+)response|import|part)[^>]*id\s*=\s*[\"\']([^\"\']+)[\"\'][^>]*>/gs);
+                                    }
+                                }
+                            }
+                        }
+# Do not overwrite contents of %metaentry hash for resource itself with 
+# hash populated for imported library file
+                        %metaentry = %currmetaentry;
+                        undef(%currmetaentry);
                         if ($importmode eq 'problem') {
 # Import as problem/response
                            $unikey=&add_prefix_and_part($prefix,$token->[2]->{'part'});
@@ -11817,17 +11968,33 @@ sub metadata {
                            $importedparts=1;
 # We need to get the original file and the imported file to get the part order correct
 # Good news: we do not need to worry about nested libraries, since parts cannot be nested
-# Load and inspect original file
-                           if ($#origfileimportpartids<0) {
-                              undef(%importedpartids);
-                              my $origfilelocation=$perlvar{'lonDocRoot'}.&clutter($uri);
-                              my $origfile=&getfile($origfilelocation);
-                              @origfileimportpartids=($origfile=~/<(part|import)[^>]*id\s*=\s*[\"\']([^\"\']+)[\"\'][^>]*>/gs);
+# Load and inspect original file if we didn't do that already
+                           if ($#origfiletagids<0) {
+                               undef(%importedrespids);
+                               undef(%importedpartids);
+                               if ($origfile eq '') {
+                                   my $origfilelocation=$perlvar{'lonDocRoot'}.&clutter($uri);
+                                   $origfile=&getfile($origfilelocation);
+                                   @origfiletagids=($origfile=~/<(part|import)[^>]*id\s*=\s*[\"\']([^\"\']+)[\"\'][^>]*>/gs);
+                               }
+                           }
+                           my @impfilepartids;
+# If <partorder> tag is included in metadata for the imported file
+# get the parts in the imported file from that.
+                           if (grep(/^partorder$/,@libfilekeys)) {
+                               %currmetaentry = %metaentry;
+                               my $libpartorder = &metadata($location,'partorder',undef,undef,undef,
+                                                            $depthcount+1);
+                               %metaentry = %currmetaentry;
+                               undef(%currmetaentry);
+                               if ($libpartorder ne '') {
+                                   @impfilepartids=split(/\s*,\s*/,$libpartorder);
+                               }
+                           } else {
+# If no <partorder> tag available, load and inspect imported file
+                               my $impfile=&getfile($location);
+                               @impfilepartids=($impfile=~/<part[^>]*id\s*=\s*[\"\']([^\"\']+)[\"\'][^>]*>/gs);
                            }
-
-# Load and inspect imported file
-                           my $impfile=&getfile($location);
-                           my @impfilepartids=($impfile=~/<part[^>]*id\s*=\s*[\"\']([^\"\']+)[\"\'][^>]*>/gs);
                            if ($#impfilepartids>=0) {
 # This problem had parts
                                $importedpartids{$token->[2]->{'id'}}=join(',',@impfilepartids);
@@ -11847,13 +12014,12 @@ 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};
 				$metathesekeys{$meta}=1;
 			    }
-			
                         }
 		    } else {
 #
@@ -11922,7 +12088,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};
@@ -11936,26 +12102,57 @@ sub metadata {
 	    grep { ! $seen{$_} ++ } (split(',',$metaentry{':packages'}));
 	$metaentry{':packages'} = join(',',@uniq_packages);
 
-        if ($importedparts) {
+        if (($importedresponses) || ($importedparts)) {
+            if ($importedparts) {
 # We had imported parts and need to rebuild partorder
-           $metaentry{':partorder'}='';
-           $metathesekeys{'partorder'}=1;
-           for (my $index=0;$index<$#origfileimportpartids;$index+=2) {
-               if ($origfileimportpartids[$index] eq 'part') {
-# original part, part of the problem
-                  $metaentry{':partorder'}.=','.$origfileimportpartids[$index+1];
-               } else {
-# we have imported parts at this position
-                  $metaentry{':partorder'}.=','.$importedpartids{$origfileimportpartids[$index+1]};
-               }
-           }
-           $metaentry{':partorder'}=~s/^\,//;
+                $metaentry{':partorder'}='';
+                $metathesekeys{'partorder'}=1;
+            }
+            if ($importedresponses) {
+# We had imported responses and need to rebuil responseorder
+                $metaentry{':responseorder'}='';
+                $metathesekeys{'responseorder'}=1;
+            }
+            for (my $index=0;$index<$#origfiletagids;$index+=2) {
+                my $origid = $origfiletagids[$index+1];
+                if ($origfiletagids[$index] eq 'part') {
+# Original part, part of the problem
+                    if ($importedparts) {
+                        $metaentry{':partorder'}.=','.$origid;
+                    }
+                } elsif ($origfiletagids[$index] eq 'import') {
+                    if ($importedparts) {
+# We have imported parts at this position
+                        if ($importedpartids{$origid} ne '') {
+                            $metaentry{':partorder'}.=','.$importedpartids{$origid};
+                        }
+                    }
+                    if ($importedresponses) {
+# We have imported responses at this position
+                        if ($importedrespids{$origid} ne '') {
+                            $metaentry{':responseorder'}.=','.$importedrespids{$origid};
+                        }
+                    }
+                } else {
+# Original response item, part of the problem
+                    if ($importedresponses) {
+                        $metaentry{':responseorder'}.=','.$origid;
+                    }
+                }
+            }
+            if ($importedparts) {
+                $metaentry{':partorder'}=~s/^\,//;
+            }
+            if ($importedresponses) {
+                $metaentry{':responseorder'}=~s/^\,//;
+            }
         }
-
 	$metaentry{':keys'} = join(',',keys(%metathesekeys));
 	&metadata_generate_part0(\%metathesekeys,\%metaentry,$uri);
 	$metaentry{':allpossiblekeys'}=join(',',keys(%metathesekeys));
-	&do_cache_new('meta',$uri,\%metaentry,$cachetime);
+        unless ($liburi) {
+	    &do_cache_new('meta',$uri,\%metaentry,$cachetime);
+        }
 # this is the end of "was not already recently cached
     }
     return $metaentry{':'.$what};
@@ -14669,12 +14866,18 @@ 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
+this function automatically caches all requests except any made recursively
+to retrieve a list of metadata keys for an imported library file ($liburi is 
+defined).
 
 =item *