[LON-CAPA-cvs] cvs: loncom / Lond.pm lond /interface courseprefs.pm domainprefs.pm loncommon.pm londocs.pm lonextresedit.pm lonexttool.pm lonparmset.pm /lonnet/perl lonnet.pm

raeburn raeburn at source.lon-capa.org
Mon May 22 17:10:56 EDT 2023


raeburn		Mon May 22 21:10:56 2023 EDT

  Modified files:              
    /loncom/interface	courseprefs.pm domainprefs.pm loncommon.pm 
                     	londocs.pm lonextresedit.pm lonexttool.pm 
                     	lonparmset.pm 
    /loncom/lonnet/perl	lonnet.pm 
    /loncom	Lond.pm lond 
  Log:
  - Bug 6754
    - Can select from tool definitions set in course's domain or in course 
      itself when using External > "External Tool" in Course Editor.
    - DC's configuration for allowed use of esternal tools within course types                                                             
      (can be overridden for specific course(s)) determines which External Tool
      types (if any) may be added to a course.
    - Signing of LTI payload used to launch an external tool now carried out 
      on course's home server.
  
  
-------------- next part --------------
Index: loncom/interface/courseprefs.pm
diff -u loncom/interface/courseprefs.pm:1.119 loncom/interface/courseprefs.pm:1.120
--- loncom/interface/courseprefs.pm:1.119	Thu Apr 13 15:21:00 2023
+++ loncom/interface/courseprefs.pm	Mon May 22 21:10:55 2023
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # Handler to set configuration settings for a course
 #
-# $Id: courseprefs.pm,v 1.119 2023/04/13 15:21:00 raeburn Exp $
+# $Id: courseprefs.pm,v 1.120 2023/05/22 21:10:55 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -1883,7 +1883,7 @@
     my @allfields = ('fullname','firstname','lastname','email','user','roles');
     map { $possfield{$_} = 1; } @allfields;
 
-    my ($dest,$privnum,$cipher,$errors,%ltitools);
+    my ($dest,$privnum,$cipher);
 
     ($cipher,$privnum) = &get_credentials($cdom,$cnum,'ltitools',$context);
     if ($context eq 'domain') {
@@ -3223,7 +3223,7 @@
                 &Apache::lonnet::devalidate_cache_new('courseltitools',$hashid);
                 unless (($home eq 'no_host') || ($home eq '')) {
                     if (grep(/^\Q$home\E$/, at ids)) {
-                        &Apache::lonnet::devalidate_cache_new('courseltitoolsenc',$hashid);
+                        &Apache::lonnet::devalidate_cache_new('crsltitoolsenc',$hashid);
                     }
                 }
             } else {
@@ -6310,8 +6310,35 @@
 sub print_ltitools {
     my ($cdom,$cnum,$settings,$rowtotal,$crstype,$noedit,$context) = @_;
     my ($datatable,$disabled,$css_class,$dest);
-    my %lt = &ltitools_names();
     my $itemcount = 1;
+    unless ($context eq 'domain') {
+        my %tooltypes = &Apache::loncommon::usable_exttools();
+        unless ($tooltypes{'crs'}) {
+            my $showtype = 'course';
+            if ($crstype eq 'Community') {
+                $showtype = lc($crstype);
+            }
+            $css_class = $itemcount%2?' class="LC_odd_row"':'';
+            $datatable = '<tr '.$css_class.'><td colspan="2">'.
+                         &mt("Definition of external tools is not enabled for this $showtype.").'<br />';
+            if ($tooltypes{'dom'}) {
+                         $datatable .= &mt("Contact an administrator for the $showtype domain ([_1]) to request this feature be enabled.",
+                                           '<i>'.$cdom.'</i>').
+                         '<br /><br />'.
+                         &mt("Use of external tools defined at a domain level is enabled, so the $showtype editor can be used to add tool(s), if any have been defined.");
+            } else {
+                $datatable .= &mt("Use of external tools defined at a domain level is not enabled, either, for this $showtype.").
+                              '<br /><br />'.
+                              &mt("Contact an administrator for the $showtype domain ([_1]) to request changes.",
+                                  '<i>'.$cdom.'</i>');
+
+            }
+            $datatable .= '</tr>';
+            $itemcount ++;
+            return $datatable;
+        }
+    }
+    my %lt = &ltitools_names();
     my $maxnum = 0;
     my %ordered;
     if (ref($settings) eq 'HASH') {
@@ -6393,7 +6420,7 @@
                 '<option value="HMAC-SHA1"'.$sigsel{'HMAC-SHA1'}.'>HMAC-SHA1</option>'.
                 '<option value="HMAC-SHA256"'.$sigsel{'HMAC-SHA256'}.'>HMAC-SHA256</option></select></span>'.
                 '<br /><br />'.
-                '<span class="LC_nobreak">'.$lt{'url'}.':<input type="text" size="40" name="ltitools_url_'.$i.'"'.
+                '<span class="LC_nobreak">'.$lt{'url'}.':<input type="text" size="60" name="ltitools_url_'.$i.'"'.
                 ' value="'.$url.'" /></span>'.
                 (' 'x2).
                 '<span class="LC_nobreak">'.$lt{'lifetime'}.':'.
@@ -6671,7 +6698,7 @@
                   '<option value="HMAC-SHA1" selected="selected">HMAC-SHA1</option>'.
                   '<option value="HMAC-SHA256">HMAC-SHA256</option></select></span>'.
                   '<br />'.
-                  '<span class="LC_nobreak">'.$lt{'url'}.':<input type="text" size="40" name="ltitools_add_url" value="" /></span> '."\n".
+                  '<span class="LC_nobreak">'.$lt{'url'}.':<input type="text" size="60" name="ltitools_add_url" value="" /></span> '."\n".
                   (' 'x2).
                   '<span class="LC_nobreak">'.$lt{'lifetime'}.':<input type="text" size="5" name="ltitools_add_lifetime" value="300" /></span><br />';
     if ($switchserver) {
@@ -8224,7 +8251,7 @@
     my %servers = &Apache::lonnet::internet_dom_servers($cdom);
     my %thismachine;
     map { $thismachine{$_} = 1; } &Apache::lonnet::current_machine_ids();
-    my @posscached = ('courselti');
+    my @posscached = ('courselti','courseltitools');
     if (keys(%servers)) {
         foreach my $server (keys(%servers)) {
             next if ($thismachine{$server});
Index: loncom/interface/domainprefs.pm
diff -u loncom/interface/domainprefs.pm:1.422 loncom/interface/domainprefs.pm:1.423
--- loncom/interface/domainprefs.pm:1.422	Tue Apr 11 20:35:19 2023
+++ loncom/interface/domainprefs.pm	Mon May 22 21:10:55 2023
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # Handler to set domain-wide configuration settings
 #
-# $Id: domainprefs.pm,v 1.422 2023/04/11 20:35:19 raeburn Exp $
+# $Id: domainprefs.pm,v 1.423 2023/05/22 21:10:55 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -227,10 +227,21 @@
         &Apache::lonnet::get_dom('encconfig',['ltitools','lti','proctoring','linkprot'],$dom,undef,1);
     if (ref($domconfig{'ltitools'}) eq 'HASH') {
         if (ref($encconfig{'ltitools'}) eq 'HASH') {
+            my $is_home;
+            my $home = &Apache::lonnet::domain($dom,'primary');
+            unless (($home eq 'no_host') || ($home eq '')) {
+                my @ids=&Apache::lonnet::current_machine_ids();
+                if (grep(/^\Q$home\E$/, at ids)) {
+                    $is_home = 1;
+                }
+            }
             foreach my $id (keys(%{$domconfig{'ltitools'}})) {
                 if ((ref($domconfig{'ltitools'}{$id}) eq 'HASH') &&
                     (ref($encconfig{'ltitools'}{$id}) eq 'HASH')) {
                     $domconfig{'ltitools'}{$id}{'key'} = $encconfig{'ltitools'}{$id}{'key'};
+                    if (($is_home) && ($phase eq 'process')) {
+                        $domconfig{'ltitools'}{$id}{'secret'} = $encconfig{'ltitools'}{$id}{'secret'};
+                    }
                 }
             }
         }
@@ -14245,15 +14256,22 @@
                                    $action => { %newtoolsenc }
                                );
             &Apache::lonnet::put_dom('encconfig',\%toolsenchash,$dom,undef,1);
+            my $cachetime = 24*60*60;
+            &Apache::lonnet::do_cache_new('ltitoolsenc',$dom,\%newtoolsenc,$cachetime);
             &store_security($dom,'ltitools',\%secchanges,\%newkeyset,\%keystore,$lastactref);
         }
         $resulttext = &mt('Changes made:').'<ul>';
         if (keys(%secchanges) > 0) {
-            $resulttext .= &lti_security_results('ltitools',\%secchanges,\%newtoolsec,\%newkeyset,\%keystore);
+            $resulttext .= &lti_security_results($dom,'ltitools',\%secchanges,\%newtoolsec,\%newkeyset,\%keystore);
         }
         if (keys(%ltitoolschg) > 0) {
             $resulttext .= $ltitoolsoutput;
         }
+        my $cachetime = 24*60*60;
+        &Apache::lonnet::do_cache_new('ltitools',$dom,\%newltitools,$cachetime);
+        if (ref($lastactref) eq 'HASH') {
+            $lastactref->{'ltitools'} = 1;
+        }
     } else {
         $errors .= '<li><span class="LC_error">'.&mt('Failed to save changes').'</span></li>';
     }
@@ -14382,10 +14400,13 @@
 }
 
 sub lti_security_results {
-    my ($context,$secchanges,$newsec,$newkeyset,$keystore) = @_;
+    my ($dom,$context,$secchanges,$newsec,$newkeyset,$keystore) = @_;
     my $output;
+    my %domdefaults = &Apache::lonnet::get_domain_defaults($dom);
+    my $needs_update;
     foreach my $item (keys(%{$secchanges})) {
         if ($item eq 'encrypt') {
+            $needs_update = 1;
             my %encrypted;
             if ($context eq 'lti') {
                 %encrypted = (
@@ -14417,12 +14438,29 @@
             }
             my @types= ('crs','dom');
             if ($context eq 'lti') {
+                foreach my $type (@types) {
+                    undef($domdefaults{'linkprotenc_'.$type});
+                }
                 push(@types,'consumers');
+                undef($domdefaults{'ltienc_consumers'});
+            } elsif ($context eq 'ltitools') {
+                foreach my $type (@types) {
+                    undef($domdefaults{'toolenc_'.$type});
+                }
             }
             foreach my $type (@types) {
                 my $shown = $encrypted{$type}{'off'};
                 if (ref($newsec->{$item}) eq 'HASH') {
                     if ($newsec->{$item}{$type}) {
+                        if ($context eq 'lti') {
+                            if ($type eq 'consumers') {
+                                $domdefaults{'ltienc_consumers'} = 1;
+                            } else {
+                                $domdefaults{'linkprotenc_'.$type} = 1;
+                            }
+                        } elsif ($context eq 'ltitools') {
+                            $domdefaults{'toolenc_'.$type} = 1;
+                        }
                         $shown = $encrypted{$type}{'on'};
                     }
                 }
@@ -14466,10 +14504,27 @@
                 $output .= '<li>'.&mt('[_1] set to none',$titles{'chars'}).'</li>';
             }
         } elsif ($item eq 'private') {
+            $needs_update = 1;
+            if ($context eq 'lti') {
+                undef($domdefaults{'ltiprivhosts'});
+            } elsif ($context eq 'ltitools') {
+                undef($domdefaults{'toolprivhosts'});
+            }
             if (keys(%{$newkeyset})) {
+                my @privhosts;
                 foreach my $hostid (sort(keys(%{$newkeyset}))) {
                     if ($keystore->{$hostid} eq 'ok') {
                         $output .= '<li>'.&mt('Encryption key for storage of shared secrets saved for [_1]',$hostid).'</li>';
+                        unless (grep(/^\Q$hostid\E$/, at privhosts)) {
+                            push(@privhosts,$hostid);
+                        }
+                    }
+                }
+                if (@privhosts) {
+                    if ($context eq 'lti') {
+                        $domdefaults{'ltiprivhosts'} = \@privhosts;
+                    } elsif ($context eq 'ltitools') {
+                        $domdefaults{'toolprivhosts'} = \@privhosts;
                     }
                 }
             }
@@ -14477,6 +14532,10 @@
             next;
         }
     }
+    if ($needs_update) {
+        my $cachetime = 24*60*60;
+        &Apache::lonnet::do_cache_new('domdefaults',$dom,\%domdefaults,$cachetime);
+    }
     return $output;
 }
 
@@ -15423,7 +15482,7 @@
         }
         $resulttext = &mt('Changes made:').'<ul>';
         if (keys(%secchanges) > 0) {
-            $resulttext .= &lti_security_results('lti',\%secchanges,\%newltisec,\%newkeyset,\%keystore);
+            $resulttext .= &lti_security_results($dom,'lti',\%secchanges,\%newltisec,\%newkeyset,\%keystore);
             if (exists($secchanges{'linkprot'})) {
                 $resulttext .= $linkprotoutput;
             }
Index: loncom/interface/loncommon.pm
diff -u loncom/interface/loncommon.pm:1.1404 loncom/interface/loncommon.pm:1.1405
--- loncom/interface/loncommon.pm:1.1404	Sun Apr  2 03:16:27 2023
+++ loncom/interface/loncommon.pm	Mon May 22 21:10:55 2023
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # a pile of common routines
 #
-# $Id: loncommon.pm,v 1.1404 2023/04/02 03:16:27 raeburn Exp $
+# $Id: loncommon.pm,v 1.1405 2023/05/22 21:10:55 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -9764,6 +9764,50 @@
     }
 }
 
+sub usable_exttools {
+    my %tooltypes;
+    if ($env{'request.course.id'}) {
+        if ($env{'course.'.$env{'request.course.id'}.'.internal.exttool'}) {
+           if ($env{'course.'.$env{'request.course.id'}.'.internal.exttool'} eq 'both') {
+               %tooltypes = (
+                             crs => 1,
+                             dom => 1,
+                            );
+           } elsif ($env{'course.'.$env{'request.course.id'}.'.internal.exttool'} eq 'crs') {
+               $tooltypes{'crs'} = 1;
+           } elsif ($env{'course.'.$env{'request.course.id'}.'.internal.exttool'} eq 'dom') {
+               $tooltypes{'dom'} = 1;
+           }
+        } else {
+            my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'};
+            my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'};
+            my $crstype = lc($env{'course.'.$env{'request.course.id'}.'.type'});
+            if ($crstype eq '') {
+                $crstype = 'course';
+            }
+            if ($crstype eq 'course') {
+                if ($env{'course.'.$env{'request.course.id'}.'internal.coursecode'}) {
+                    $crstype = 'official';
+                } elsif ($env{'course.'.$env{'request.course.id'}.'.internal.textbook'}) {
+                    $crstype = 'textbook';
+                } elsif ($env{'course.'.$env{'request.course.id'}.'.internal.lti'}) {
+                    $crstype = 'lti';
+                } else {
+                    $crstype = 'unofficial';
+                }
+            }
+            my %domdefaults = &Apache::lonnet::get_domain_defaults($cdom);
+            if ($domdefaults{$crstype.'domexttool'}) {
+                $tooltypes{'dom'} = 1;
+            }
+            if ($domdefaults{$crstype.'exttool'}) {
+                $tooltypes{'crs'} = 1;
+            }
+        }
+    }
+    return %tooltypes;
+}
+
 sub wishlist_window {
     return(<<'ENDWISHLIST');
 <script type="text/javascript">
Index: loncom/interface/londocs.pm
diff -u loncom/interface/londocs.pm:1.698 loncom/interface/londocs.pm:1.699
--- loncom/interface/londocs.pm:1.698	Mon Mar 27 18:41:04 2023
+++ loncom/interface/londocs.pm	Mon May 22 21:10:55 2023
@@ -1,7 +1,7 @@
 # The LearningOnline Network
 # Documents
 #
-# $Id: londocs.pm,v 1.698 2023/03/27 18:41:04 raeburn Exp $
+# $Id: londocs.pm,v 1.699 2023/05/22 21:10:55 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -719,7 +719,7 @@
                 $url = $1;
                 my $marker = $2;
                 my $info = $3;
-                my ($toolid,%toolhash,%toolsettings);
+                my ($toolid,$toolprefix,$tooltype,%toolhash,%toolsettings);
                 my @extras = ('linktext','explanation','crslabel','crstitle','crsappend');
                 my @toolinfo = split(/:/,$info);
                 if ($residx) {
@@ -728,6 +728,12 @@
                 } else {
                     $toolid = shift(@toolinfo);
                 }
+                if ($toolid =~ /^c/) {
+                    $tooltype = 'crs';
+                    $toolprefix = 'c';
+                } else {
+                    $tooltype = 'dom';
+                }
                 $toolid =~ s/\D//g;
                 ($toolhash{'target'},$toolhash{'width'},$toolhash{'height'},
                  $toolhash{'linktext'},$toolhash{'explanation'},$toolhash{'crslabel'},
@@ -741,127 +747,130 @@
                     $toolhash{'gradable'} =~ s/\D+//g;
                 }
                 if (ref($ltitoolsref) eq 'HASH') {
-                    if (ref($ltitoolsref->{$toolid}) eq 'HASH') {
-                        my @deleted;
-                        $toolhash{'id'} = $toolid;
-                        if (($toolhash{'target'} eq 'iframe') || ($toolhash{'target'} eq 'tab') ||
-                            ($toolhash{'target'} eq 'window')) {
-                            if ($toolhash{'target'} eq 'window') {
-                                foreach my $item ('width','height') {
-                                    $toolhash{$item} =~ s/^\s+//;
-                                    $toolhash{$item} =~ s/\s+$//;
-                                    if ($toolhash{$item} =~ /\D/) {
-                                        delete($toolhash{$item});
-                                        if ($residx) {
-                                            if ($toolsettings{$item}) {
-                                                push(@deleted,$item);
+                    if (ref($ltitoolsref->{$tooltype}) eq 'HASH') {
+                        if (ref($ltitoolsref->{$tooltype}->{$toolid}) eq 'HASH') {
+                            my %tools = %{$ltitoolsref->{$tooltype}->{$toolid}};
+                            my @deleted;
+                            $toolhash{'id'} = $toolprefix.$toolid;
+                            if (($toolhash{'target'} eq 'iframe') || ($toolhash{'target'} eq 'tab') ||
+                                ($toolhash{'target'} eq 'window')) {
+                                if ($toolhash{'target'} eq 'window') {
+                                    foreach my $item ('width','height') {
+                                        $toolhash{$item} =~ s/^\s+//;
+                                        $toolhash{$item} =~ s/\s+$//;
+                                        if ($toolhash{$item} =~ /\D/) {
+                                            delete($toolhash{$item});
+                                            if ($residx) {
+                                                if ($toolsettings{$item}) {
+                                                    push(@deleted,$item);
+                                                }
                                             }
                                         }
                                     }
                                 }
-                            }
-                        } elsif ($residx) {
-                            $toolhash{'target'} = $toolsettings{'target'};
-                            if ($toolhash{'target'} eq 'window') {
-                                foreach my $item ('width','height') {
-                                    $toolhash{$item} = $toolsettings{$item};
+                            } elsif ($residx) {
+                                $toolhash{'target'} = $toolsettings{'target'};
+                                if ($toolhash{'target'} eq 'window') {
+                                    foreach my $item ('width','height') {
+                                        $toolhash{$item} = $toolsettings{$item};
+                                    }
+                                }
+                            } elsif (ref($tools{'display'}) eq 'HASH') {
+                                $toolhash{'target'} = $tools{'display'}{'target'};
+                                if ($toolhash{'target'} eq 'window') {
+                                    $toolhash{'width'} = $tools{'display'}{'width'};
+                                    $toolhash{'height'} = $tools{'display'}{'height'};
                                 }
                             }
-                        } elsif (ref($ltitoolsref->{$toolid}->{'display'}) eq 'HASH') {
-                            $toolhash{'target'} = $ltitoolsref->{$toolid}->{'display'}->{'target'};
-                            if ($toolhash{'target'} eq 'window') {
-                                $toolhash{'width'} = $ltitoolsref->{$toolid}->{'display'}->{'width'};
-                                $toolhash{'height'} = $ltitoolsref->{$toolid}->{'display'}->{'height'};
-                            }
-                        }
-                        if ($toolhash{'target'} eq 'iframe') {
-                            foreach my $item ('width','height','linktext','explanation') {
-                                delete($toolhash{$item});
-                                if ($residx) {
-                                    if ($toolsettings{$item}) {
-                                        push(@deleted,$item);
+                            if ($toolhash{'target'} eq 'iframe') {
+                                foreach my $item ('width','height','linktext','explanation') {
+                                    delete($toolhash{$item});
+                                    if ($residx) {
+                                        if ($toolsettings{$item}) {
+                                            push(@deleted,$item);
+                                        }
                                     }
                                 }
-                            }
-                        } elsif ($toolhash{'target'} eq 'tab') {
-                            foreach my $item ('width','height') {
-                                delete($toolhash{$item});
-                                if ($residx) {
-                                    if ($toolsettings{$item}) {
-                                        push(@deleted,$item);
+                            } elsif ($toolhash{'target'} eq 'tab') {
+                                foreach my $item ('width','height') {
+                                    delete($toolhash{$item});
+                                    if ($residx) {
+                                        if ($toolsettings{$item}) {
+                                            push(@deleted,$item);
+                                        }
                                     }
                                 }
                             }
-                        }
-                        if (ref($ltitoolsref->{$toolid}->{'crsconf'}) eq 'HASH') {
-                            foreach my $item ('label','title','linktext','explanation') {
-                                my $crsitem;
-                                if (($item eq 'label') || ($item eq 'title')) {
-                                    $crsitem = 'crs'.$item;
-                                } else {
-                                    $crsitem = $item;
-                                }
-                                if ($ltitoolsref->{$toolid}->{'crsconf'}->{$item}) {
-                                    $toolhash{$crsitem} =~ s/^\s+//;
-                                    $toolhash{$crsitem} =~ s/\s+$//;
-                                    if ($toolhash{$crsitem} eq '') {
+                            if (ref($tools{'crsconf'}) eq 'HASH') {
+                                foreach my $item ('label','title','linktext','explanation') {
+                                    my $crsitem;
+                                    if (($item eq 'label') || ($item eq 'title')) {
+                                        $crsitem = 'crs'.$item;
+                                    } else {
+                                        $crsitem = $item;
+                                    }
+                                    if ($tools{'crsconf'}{$item}) {
+                                        $toolhash{$crsitem} =~ s/^\s+//;
+                                        $toolhash{$crsitem} =~ s/\s+$//;
+                                        if ($toolhash{$crsitem} eq '') {
+                                            delete($toolhash{$crsitem});
+                                        }
+                                    } else {
                                         delete($toolhash{$crsitem});
                                     }
-                                } else {
-                                    delete($toolhash{$crsitem});
-                                }
-                                if (($residx) && (exists($toolsettings{$crsitem}))) {
-                                    unless (exists($toolhash{$crsitem})) {
-                                        push(@deleted,$crsitem);
+                                    if (($residx) && (exists($toolsettings{$crsitem}))) {
+                                        unless (exists($toolhash{$crsitem})) {
+                                            push(@deleted,$crsitem);
+                                        }
                                     }
                                 }
                             }
-                        }
-                        if ($toolhash{'passback'}) {
-                            my $gradesecret = UUID::Tiny::create_uuid_as_string(UUID_V4);
-                            $toolhash{'gradesecret'} = $gradesecret;
-                            $toolhash{'gradesecretdate'} = time;
-                        }
-                        if ($toolhash{'roster'}) {
-                            my $rostersecret = UUID::Tiny::create_uuid_as_string(UUID_V4);
-                            $toolhash{'rostersecret'} = $rostersecret;
-                            $toolhash{'rostersecretdate'} = time;
-                        }
-                        my $changegradable;
-                        if (($residx) && ($folder =~ /^default/)) {
-                            if ($toolsettings{'gradable'}) {
-                                unless (($toolhash{'gradable'}) || (defined($LONCAPA::map::zombies[$residx]))) {
-                                    push(@deleted,'gradable');
-                                    $changegradable = 1;
-                                }
-                            } elsif ($toolhash{'gradable'}) {
-                                $changegradable = 1;
+                            if ($toolhash{'passback'}) {
+                                my $gradesecret = UUID::Tiny::create_uuid_as_string(UUID_V4);
+                                $toolhash{'gradesecret'} = $gradesecret;
+                                $toolhash{'gradesecretdate'} = time;
+                            }
+                            if ($toolhash{'roster'}) {
+                                my $rostersecret = UUID::Tiny::create_uuid_as_string(UUID_V4);
+                                $toolhash{'rostersecret'} = $rostersecret;
+                                $toolhash{'rostersecretdate'} = time;
                             }
-                            if (($caller eq 'londocs') && (defined($LONCAPA::map::zombies[$residx]))) {
-                                $changegradable = 1;
+                            my $changegradable;
+                            if (($residx) && ($folder =~ /^default/)) {
                                 if ($toolsettings{'gradable'}) {
-                                    $toolhash{'gradable'} = 1;
+                                    unless (($toolhash{'gradable'}) || (defined($LONCAPA::map::zombies[$residx]))) {
+                                        push(@deleted,'gradable');
+                                        $changegradable = 1;
+                                    }
+                                } elsif ($toolhash{'gradable'}) {
+                                    $changegradable = 1;
+                                }
+                                if (($caller eq 'londocs') && (defined($LONCAPA::map::zombies[$residx]))) {
+                                    $changegradable = 1;
+                                    if ($toolsettings{'gradable'}) {
+                                        $toolhash{'gradable'} = 1;
+                                    }
                                 }
                             }
-                        }
-                        my $putres = &Apache::lonnet::put('exttool_'.$marker,\%toolhash,$coursedom,$coursenum);
-                        if ($putres eq 'ok') {
-                            if (@deleted) {
-                                &Apache::lonnet::del('exttool_'.$marker,\@deleted,$coursedom,$coursenum);
-                            }
-                            if (($changegradable) && ($folder =~ /^default/)) {
-                                my $val;
-                                if ($toolhash{'gradable'}) {
-                                    $val = 'yes';
-                                } else {
-                                    $val = 'no';
+                            my $putres = &Apache::lonnet::put('exttool_'.$marker,\%toolhash,$coursedom,$coursenum);
+                            if ($putres eq 'ok') {
+                                if (@deleted) {
+                                    &Apache::lonnet::del('exttool_'.$marker,\@deleted,$coursedom,$coursenum);
                                 }
-                                &LONCAPA::map::storeparameter($residx,'parameter_0_gradable',$val,
-                                                              'string_yesno');
-                                &remember_parms($residx,'gradable','set',$val);
+                                if (($changegradable) && ($folder =~ /^default/)) {
+                                    my $val;
+                                    if ($toolhash{'gradable'}) {
+                                        $val = 'yes';
+                                    } else {
+                                        $val = 'no';
+                                    }
+                                    &LONCAPA::map::storeparameter($residx,'parameter_0_gradable',$val,
+                                                                  'string_yesno');
+                                    &remember_parms($residx,'gradable','set',$val);
+                                }
+                            } else {
+                                return (&mt('Failed to save update to external tool.'),1);
                             }
-                        } else {
-                            return (&mt('Failed to save update to external tool.'),1);
                         }
                     }
                 }
@@ -5725,6 +5734,7 @@
     my $containertag;
     my $pathitem;
     my %ltitools;
+    my $posslti;
     my $hiddentop;
     my $navmap;
     my $filterFunc = sub { my $res = shift; return (!$res->randomout() && !$res->is_map()) };
@@ -5958,8 +5968,19 @@
                 }
             }
             my $tabidstr = join("','", at tabids);
-            %ltitools = &Apache::lonnet::get_domain_lti($coursedom,'consumer');
-            my $posslti = keys(%ltitools);
+            my (%domtools,%crstools);
+            my %tooltypes = &Apache::loncommon::usable_exttools();
+            if ($tooltypes{'dom'}) {
+                %domtools = &Apache::lonnet::get_domain_lti($coursedom,'consumer');
+            }
+            if ($tooltypes{'crs'}) {
+                %crstools = &Apache::lonnet::get_course_lti($coursenum,$coursedom,'consumer');
+            }
+            %ltitools = (
+                          dom => \%domtools,
+                          crs => \%crstools,
+                        );
+            $posslti = scalar(keys(%domtools)) + scalar(keys(%crstools));
             my $hostname = $r->hostname();
 	    $script .= &editing_js($udom,$uname,$supplementalflag,$coursedom,$coursenum,$posslti,
                                    $londocroot,$canedit,$hostname,\$navmap).
@@ -6316,7 +6337,7 @@
         <input type="hidden" name="active" value="bb" />
         $pickfile
         <p>
-        $lt{'title'}: <input type="textbox" name="crsrestitle" value="" $disabled />
+        $lt{'title'}: <input type="text" name="crsrestitle" value="" $disabled />
         </p>
         <input type="hidden" name="importdetail" value="" />
         <input type="submit" name="crsres" value="$lt{'impo'}" $disabled /><br />
@@ -6660,6 +6681,7 @@
         <label><input type="radio" name="newsubdir" value="1" onclick="toggleNewsubdir(this.form);" $disabled />Yes</label>
         </span><span id="newsubdir"></span>
         <input type="hidden" name="newsubdirname" id="newsubdirname" value="" autocomplete="off" />
+        </div>
         </p>
         $lt{'fnam'}
         <input type="text" size="20" name="newresourcename" autocomplete="off" $disabled />
@@ -6770,7 +6792,7 @@
         my @external = (
         {'<img class="LC_noBorder LC_middle" src="/res/adm/pages/extres.png" alt="'.$lt{extr}.'" onclick="toggleExternal(\'ext\');" />'=>$extresourcesform}
         );
-        if (keys(%ltitools)) {
+        if ($posslti) {
             push(@external,
                  {'<img class="LC_noBorder LC_middle" src="/res/adm/pages/exttool.png" alt="'.$lt{extt}.'" onclick="toggleExternal(\'tool\');" />'=>$exttoolform},
             );
@@ -6953,7 +6975,7 @@
         my @supexternal = (
             {'<img class="LC_noBorder LC_middle" src="/res/adm/pages/extres.png" alt="'.$lt{extr}.'" onclick="javascript:toggleExternal(\'suppext\')" />'
              =>$supextform});
-        if (keys(%ltitools)) {
+        if ($posslti) {
             push(@supexternal,
                  {'<img class="LC_noBorder LC_middle" src="/res/adm/pages/exttool.png" alt="'.$lt{extt}.'" onclick="javascript:toggleExternal(\'supptool\')" />'
             =>$supexttoolform});
Index: loncom/interface/lonextresedit.pm
diff -u loncom/interface/lonextresedit.pm:1.30 loncom/interface/lonextresedit.pm:1.31
--- loncom/interface/lonextresedit.pm:1.30	Fri Sep  9 14:24:30 2022
+++ loncom/interface/lonextresedit.pm	Mon May 22 21:10:55 2023
@@ -1,7 +1,7 @@
 # The LearningOnline Network
 # Documents
 #
-# $Id: lonextresedit.pm,v 1.30 2022/09/09 14:24:30 raeburn Exp $
+# $Id: lonextresedit.pm,v 1.31 2023/05/22 21:10:55 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -98,7 +98,18 @@
     }
     my %ltitools;
     if ($type eq 'tool') {
-        %ltitools = &Apache::lonnet::get_domain_lti($cdom,'consumer');
+        my (%domtools,%crstools);
+        my %tooltypes = &Apache::loncommon::usable_exttools();
+        if ($tooltypes{'dom'}) {
+            %domtools = &Apache::lonnet::get_domain_lti($cdom,'consumer');
+        }
+        if ($tooltypes{'crs'}) {
+            %crstools = &Apache::lonnet::get_course_lti($cnum,$cdom,'consumer');
+        }
+        %ltitools = (
+                      dom => \%domtools,
+                      crs => \%crstools,
+                    );  
     }
     my $js = &Apache::lonhtmlcommon::scripttag(&extedit_javascript());
     my $pathitem = '<input type="hidden" name="folderpath" value="'.
@@ -375,15 +386,17 @@
         ti => 'Title',
         al => 'Add Link',
         at => 'Add Tool',
+        dd => 'Defined in domain',
+        dc => 'Defined in course',
     );
     my $tabid = 'aa';
     my $size = 60;
     if ($supplementalflag) {
         $tabid = 'ee';
     }
-    my ($formname,$formid,$toggle,$fieldsetid,$urlid,$dispdivstyle,$dimendivstyle,
+    my ($formname,$formid,$toggle,$fieldsetid,$urlid,$subdivid,$dispdivstyle,$dimendivstyle,
         $windivstyle,$linktextstyle,$explanationstyle,$labelstyle,$titlestyle,
-        $appendstyle,$gradablestyle,$legend,$urlelem,$toolelem,%toolattr);
+        $appendstyle,$gradablestyle,$subdivstyle,$legend,$urlelem,$toolelem,%toolattr);
     $formname = 'new'.$type;
     $toggle = $type;
     $fieldsetid = 'external'.$type.'form';
@@ -402,6 +415,7 @@
     $titlestyle = 'display:none';
     $appendstyle = 'display:none';
     $gradablestyle = 'display:none';
+    $subdivstyle = 'display:block';
     if ($supplementalflag) {
         $formname = 'newsupp'.$type;
         $toggle = 'supp'.$type;
@@ -490,95 +504,157 @@
             if ($orig_url =~ m{^/adm/$cdom/$cnum/(\d+)/ext\.tool$}) {
                 my $marker = $1;
                 my %toolhash=&Apache::lonnet::dump('exttool_'.$marker,$cdom,$cnum);
-                if ($toolhash{'id'}) {
+                my ($tooltype,$tool,$ltihash);
+                if ($toolhash{'id'} =~/^c(\d+)$/) {
+                    $tool = $1;
+                    $tooltype = 'crs';
                     if (ref($ltitools) eq 'HASH') {
-                        if (keys(%{$ltitools})) {
-                            if (ref($ltitools->{$toolhash{'id'}}) eq 'HASH') {
-                                my $tooltitle = $ltitools->{$toolhash{'id'}}->{'title'};
-                                my $icon = $ltitools->{$toolhash{'id'}}->{'image'};
-                                my $image;
-                                if ($icon) {
-                                    $image = '<img src="'.$icon.'" alt="'.$tooltitle.'" />';
-                                }
-                                if ($ltitools->{$toolhash{'id'}}->{'url'} =~ m{://}) {
-                                    (my $prot,my $host,$providerurl) = ($ltitools->{$toolhash{'id'}}->{'url'} =~ m{^([^/]+)://([^/]+)(|/.+)$});
-                                } else {
-                                    $providerurl = $ltitools->{$toolhash{'id'}}->{'url'};
-                                }
-                                $tooltarget = $toolhash{'target'};
-                                if ($tooltarget eq 'window') {
-                                    $dimendivstyle = 'display:block';
-                                    $windivstyle = 'display:block';
-                                    $chkstate{'window'} = 'checked="checked" ';
-                                } elsif ($tooltarget eq 'tab') {
-                                    $windivstyle = 'display:block';
-                                    $chkstate{'tab'} = 'checked="checked" ';
-                                } else {
-                                    $chkstate{'iframe'} = 'checked="checked" ';
-                                }
-                                $width = $toolhash{'width'};
-                                $height = $toolhash{'height'};
-                                $linktext = $toolhash{'linktext'};
-                                $explanation = $toolhash{'explanation'};
-                                if ($toolhash{'gradable'}) {
-                                    $chkgrd = ' checked="checked"';
-                                } else {
-                                    $chknogrd = ' checked="checked"';
-                                }
-                                if (ref($ltitools->{$toolhash{'id'}}->{'crsconf'}) eq 'HASH') {
-                                    if ($ltitools->{$toolhash{'id'}}->{'crsconf'}->{'title'}) {
-                                        $crstitle = $toolhash{'crstitle'};
-                                        $titlestyle = 'display:inline';
-                                    }
-                                    if ($ltitools->{$toolhash{'id'}}->{'crsconf'}->{'label'}) {  
-                                        $crslabel = $toolhash{'crslabel'};
-                                        $labelstyle = 'display:inline';
-                                    }
-                                    if ($ltitools->{$toolhash{'id'}}->{'crsconf'}->{'append'}) {
-                                        $crsappend = $toolhash{'crsappend'};
-                                        $appendstyle = 'display:inline';
-                                    }
-                                    if ($ltitools->{$toolhash{'id'}}->{'crsconf'}->{'target'}) {
-                                        $dispdivstyle = 'display:block';
-                                    }
-                                    if ($ltitools->{$toolhash{'id'}}->{'crsconf'}->{'linktext'}) {
-                                        $linktextstyle = 'padding:0;display:inline';
-                                    }
-                                    if ($ltitools->{$toolhash{'id'}}->{'crsconf'}->{'explanation'}) {
-                                        $explanationstyle = 'padding:0;display:inline';
-                                    }
-                                }
-                                $toolelem = '<span class="LC_nobreak">'.$image.' '.$tooltitle.'</span><br />';
-                                $gradablestyle = 'display:inline';
-                            }
+                        if (ref($ltitools->{'crs'}) eq 'HASH') {
+                            $ltihash = $ltitools->{'crs'}->{$tool};
+                        }
+                    }
+                } elsif ($toolhash{'id'} =~/^\d+$/) {
+                    $tooltype = 'dom';
+                    $tool = $toolhash{'id'};
+                    if (ref($ltitools) eq 'HASH') {
+                        if (ref($ltitools->{'dom'}) eq 'HASH') {
+                            $ltihash = $ltitools->{'dom'}->{$tool};
+                        }
+                    }     
+                }
+                if (($tool ne '') && (ref($ltihash) eq 'HASH')) {
+                    my $tooltitle = $ltihash->{'title'};
+                    my $icon = $ltihash->{'image'};
+                    my $image;
+                    if ($icon) {
+                        $image = '<img src="'.$icon.'" alt="'.$tooltitle.'" />';
+                    }
+                    if ($ltihash->{'url'} =~ m{://}) {
+                        (my $prot,my $host,$providerurl) = ($ltihash->{'url'} =~ m{^([^/]+)://([^/]+)(|/.+)$});
+                    } else {
+                        $providerurl = $ltihash->{'url'};
+                    }
+                    $tooltarget = $toolhash{'target'};
+                    if ($tooltarget eq 'window') {
+                        $dimendivstyle = 'display:block';
+                        $windivstyle = 'display:block';
+                        $chkstate{'window'} = 'checked="checked" ';
+                    } elsif ($tooltarget eq 'tab') {
+                        $windivstyle = 'display:block';
+                        $chkstate{'tab'} = 'checked="checked" ';
+                    } else {
+                        $chkstate{'iframe'} = 'checked="checked" ';
+                    }
+                    $width = $toolhash{'width'};
+                    $height = $toolhash{'height'};
+                    $linktext = $toolhash{'linktext'};
+                    $explanation = $toolhash{'explanation'};
+                    if ($toolhash{'gradable'}) {
+                        $chkgrd = ' checked="checked"';
+                    } else {
+                        $chknogrd = ' checked="checked"';
+                    }
+                    if (ref($ltihash->{'crsconf'}) eq 'HASH') {
+                        if ($ltihash->{'crsconf'}->{'title'}) {
+                            $crstitle = $toolhash{'crstitle'};
+                            $titlestyle = 'display:inline';
+                        }
+                        if ($ltihash->{'crsconf'}->{'label'}) {  
+                            $crslabel = $toolhash{'crslabel'};
+                            $labelstyle = 'display:inline';
+                        }
+                        if ($ltihash->{'crsconf'}->{'append'}) {
+                            $crsappend = $toolhash{'crsappend'};
+                            $appendstyle = 'display:inline';
+                        }
+                        if ($ltihash->{'crsconf'}->{'target'}) {
+                            $dispdivstyle = 'display:block';
+                        }
+                        if ($ltihash->{'crsconf'}->{'linktext'}) {
+                            $linktextstyle = 'padding:0;display:inline';
+                        }
+                        if ($ltihash->{'crsconf'}->{'explanation'}) {
+                            $explanationstyle = 'padding:0;display:inline';
                         }
                     }
+                    $toolelem = '<span class="LC_nobreak">'.$image.' '.$tooltitle.'</span><br />';
+                    $gradablestyle = 'display:inline';
                 }
             }
         } else {
-            $toolelem = '<span class="LC_docs_ext_edit">'."\n".
-                       '<select name="exttoolid" id="LC_exttoolid" onchange="javascript:updateExttool(this,'.
-                       'this.form,'."'$supplementalflag'".');"'.$disabled.'>'."\n".
-                       '<option value="" selected="selected">'.&mt('Select').'</option>';
-            my %bynum;
+            $subdivstyle = 'display:none';
+            my $toolradio = 'exttooltype';
+            my $exttypeon = 'LC_exttoolon';
+            my $exttypeoff = 'LC_exttooloff';
+            my $exttypeonsty = 'display:none';
+            my $exttypeoffsty = 'display:none';
+            my $exttypeofftext;
+            if ($supplementalflag) {
+                $toolradio = 'suppexttooltype';
+                $exttypeon = 'LC_exttoolonsupp';
+                $exttypeoff = 'LC_exttooloffsupp';
+            }
+            my ($numcrstools,$numdomtools,$typeclick,%defcheck,%typedesc);
+            %typedesc = (
+                          crs => 'Defined in course',
+                          dom => 'Defined in domain',
+                        );
+#FIXME need crstype
+            my $seloptions;
+            $subdivid = 'LC_addtool';
+            if ($supplementalflag) {
+                $subdivid = 'LC_addtoolsupp';
+            }
             if (ref($ltitools) eq 'HASH') {
-                foreach my $id (keys(%{$ltitools})) {
-                    if (ref($ltitools->{$id}) eq 'HASH') {
-                        my $order = $ltitools->{$id}->{'order'};
-                        $bynum{$order} = [$id,$ltitools->{$id}];
-                    }
+                if (ref($ltitools->{'crs'}) eq 'HASH') {
+                    $numcrstools = scalar(keys(%{$ltitools->{'crs'}}));
                 }
-            }
-            foreach my $item (sort { $a <=> $b } keys(%bynum)) {
-                if (ref($bynum{$item}) eq 'ARRAY') {
-                    if (ref($bynum{$item}->[1]) eq 'HASH') {
-                        my $tooltitle = $bynum{$item}->[1]->{'title'};
-                        my $icon =  $bynum{$item}->[1]->{'image'};
-                        $toolelem .= '<option value="'.$bynum{$item}->[0].'">'.$tooltitle.'</option>';
+                if (ref($ltitools->{'dom'}) eq 'HASH') {
+                    $numdomtools = scalar(keys(%{$ltitools->{'dom'}}));
+                }
+                if ($numcrstools || $numdomtools) {
+                    $typeclick = ' onclick="'.
+                        'javascript:updateExttoolSel(this.form,'."'$toolradio','$supplementalflag'".');"';
+                } else {
+                    $exttypeoffsty = 'display:block';
+                    $exttypeofftext = &mt('No external tools defined in either the domain or the course are available for selection.');
+                }
+                if ($numcrstools && !$numdomtools) {
+                    $defcheck{'crs'} = ' checked="checked"';
+                    $subdivstyle = 'display:block';
+                    $exttypeonsty = 'display:block';
+                    my $firstoption = '<option value="" selected="selected">'.&mt('Select').'</option>';
+                    $seloptions = &ordered_tooloptions($ltitools->{'crs'});
+                    if ($seloptions) {
+                        $seloptions = "$firstoption\n$seloptions";
+                    }
+                    $exttypeofftext = &mt('No external tools defined in the domain are available for selection.');
+                } elsif (!$numcrstools && $numdomtools) {
+                    $defcheck{'dom'} = ' checked="checked"';
+                    $subdivstyle = 'display:none';
+                    $exttypeonsty = 'display:block';
+                    my $firstoption = '<option value="" selected="selected">'.&mt('Select').'</option>';
+                    $seloptions = &ordered_tooloptions($ltitools->{'dom'});
+                    if ($seloptions) {
+                        $seloptions = "$firstoption\n$seloptions";
                     }
+#FIXME need crstype
+                    $exttypeofftext = &mt('No external tools defined in the course are available for selection.');
                 }
             }
-            $toolelem .= '</select></span><br />';
+            foreach my $type ('crs','dom') {
+                $toolelem .= '<span class="LC_nobreak"> <label>'.
+                             '<input type="radio" name="'.$toolradio.'" value="'.$type.'"'.$defcheck{$type}.
+                             $typeclick.$disabled.' />'.$typedesc{$type}.'</label></span> '."\n";
+            }
+            $toolelem .= '<div id="'.$exttypeon.'" style="'.$exttypeonsty.'">'.
+                         '<select name="exttoolid" onchange="javascript:updateExttool(this,'.
+                         'this.form,'."'$supplementalflag'".');"'.$disabled.'>'."\n".
+                         $seloptions.
+                         '</select><br /></div>'."\n".
+                         '<div id="'.$exttypeoff.'" style="'.$exttypeoffsty.'">'.
+                         $exttypeofftext.
+                         '<br /></div>'."\n";
             $crslabel = $env{'course.'.$cdom.'_'.$cnum.'.internal.coursecode'};
             $crstitle = $env{'course.'.$cdom.'_'.$cnum.'.description'};
             $crsappend = '';
@@ -646,7 +722,7 @@
 $legend
 $active
 $chooser
-<div>
+<div id="$subdivid" style="$subdivstyle">
 <span$srcclass>
 $title
 <input type="text" size="$size" name="exttitle" value="$orig_title" $disabled />
@@ -666,6 +742,35 @@
     }
 }
 
+sub ordered_tooloptions {
+    my ($toolsref) = @_;
+    my ($seloptions, at ids, at titles);
+    if (ref($toolsref) eq 'HASH') {
+        my %bynum;
+        foreach my $id (keys(%{$toolsref})) {
+            if (ref($toolsref->{$id}) eq 'HASH') {
+                my $order = $toolsref->{$id}->{'order'};
+                $bynum{$order} = [$id,$toolsref->{$id}];
+            }
+        }
+        foreach my $item (sort { $a <=> $b } keys(%bynum)) {
+            if (ref($bynum{$item}) eq 'ARRAY') {
+                if (ref($bynum{$item}->[1]) eq 'HASH') {
+                    my $tooltitle = $bynum{$item}->[1]->{'title'};
+                    push(@titles,$tooltitle);
+                    push(@ids,$bynum{$item}->[0]);
+                    $seloptions .= '<option value="'.$bynum{$item}->[0].'">'.$tooltitle.'</option>'."\n";
+                }
+            }
+        }
+    }
+    if (wantarray) {
+        return (\@ids,\@titles);
+    } else {
+        return $seloptions;
+    }
+}
+
 sub display_editor {
     my ($url,$folderpath,$symb,$idx,$type,$cdom,$cnum,$hostname) = @_;
     my ($residx,$supplementalflag,$title,$pathitem,$output,$js,$navmap);
@@ -681,9 +786,20 @@
         my $path = &Apache::loncommon::symb_to_docspath($symb,\$navmap);
         $pathitem = '<input type="hidden" name="folderpath" value="'.&HTML::Entities::encode($path,'<>&"').'" />';
     }
-    my %ltitools;
+    my (%ltitools,%tooltypes);
     if ($type eq 'tool') {
-        %ltitools = &Apache::lonnet::get_domain_lti($cdom,'consumer');
+        my (%domtools,%crstools);
+        %tooltypes = &Apache::loncommon::usable_exttools();
+        if ($tooltypes{'dom'}) {
+            %domtools = &Apache::lonnet::get_domain_lti($cdom,'consumer');
+        }
+        if ($tooltypes{'crs'}) {
+            %crstools = &Apache::lonnet::get_course_lti($cnum,$cdom,'consumer');
+        }
+        %ltitools = (
+                      dom => \%domtools,
+                      crs => \%crstools,
+                    );
     }
     $js = &Apache::lonhtmlcommon::scripttag(&extedit_javascript());
     my $args = { 'force_register' => $env{'form.register'} };
@@ -704,63 +820,99 @@
 
 sub extedit_javascript {
     my ($toolsref) = @_;
-    my $toolsjs;
+    my ($toolsjs,$exttoolnums,$exttooloptions);
     if (ref($toolsref) eq 'HASH') {
-        my $num = scalar(keys(%{$toolsref}));
-        $toolsjs = "        var ltitools = new Array($num);\n".
-                   "        var ltitoolsUrl = new Array($num);\n".
-                   "        var ltitoolsTarget = new Array($num);\n".
-                   "        var ltitoolsWidth = new Array($num);\n".
-                   "        var ltitoolsHeight = new Array($num);\n".
-                   "        var ltitoolsLinkDef = new Array($num);\n".
-                   "        var ltitoolsExplainDef = new Array($num);\n".
-                   "        var ltitoolsDisplay = new Array($num);\n".
-                   "        var ltitoolsLink = new Array($num);\n".
-                   "        var ltitoolsExplain = new Array($num);\n".
-                   "        var ltitoolsLabel = new Array($num);\n".
-                   "        var ltitoolsTitle = new Array($num);\n".
-                   "        var ltitoolsAppend = new Array($num);\n";
-        my $i = 0;
-        foreach my $key (sort { $a <=> $b } keys(%{$toolsref})) {
-            if (ref($toolsref->{$key}) eq 'HASH') {
-                if (ref($toolsref->{$key}->{'display'}) eq 'HASH') {
-                    my $target = $toolsref->{$key}->{'display'}->{'target'};
-                    my $width = $toolsref->{$key}->{'display'}->{'width'};
-                    my $height = $toolsref->{$key}->{'display'}->{'height'};
-                    my $linkdef = $toolsref->{$key}->{'display'}->{'linktext'};
-                    my $explaindef = $toolsref->{$key}->{'display'}->{'explanation'};
-                    my $providerurl;
-                    if ($toolsref->{$key}->{'url'} =~ m{://}) {
-                        (my $prot,my $host,$providerurl) = ($toolsref->{$key}->{'url'} =~ m{^([^/]+)://([^/]+)(|/.+)$});
-                    } else {
-                        $providerurl = $toolsref->{$key}->{'url'};
+        $toolsjs = "        var ltitools = new Array();\n".
+                   "        var ltitoolsUrl = new Array();\n".
+                   "        var ltitoolsTarget = new Array();\n".
+                   "        var ltitoolsWidth = new Array();\n".
+                   "        var ltitoolsHeight = new Array();\n".
+                   "        var ltitoolsLinkDef = new Array();\n".
+                   "        var ltitoolsExplainDef = new Array();\n".
+                   "        var ltitoolsDisplay = new Array();\n".
+                   "        var ltitoolsLink = new Array();\n".
+                   "        var ltitoolsExplain = new Array();\n".
+                   "        var ltitoolsLabel = new Array();\n".
+                   "        var ltitoolsTitle = new Array();\n".
+                   "        var ltitoolsAppend = new Array();\n";
+        $exttoolnums = "        var ltitoolsnum = new Array();\n".
+                       "        var tooloptval = new Array();\n".
+                       "        var toolopttxt = new Array();\n";
+        my $idx = 0;
+        foreach my $type ('crs','dom') {
+            if (ref($toolsref->{$type}) eq 'HASH') {
+                my $num = scalar(keys(%{$toolsref->{$type}}));
+                $toolsjs .= "        ltitools[$idx] = new Array($num);\n".
+                            "        ltitoolsUrl[$idx] = new Array($num);\n".
+                            "        ltitoolsTarget[$idx] = new Array($num);\n".
+                            "        ltitoolsWidth[$idx] = new Array($num);\n".
+                            "        ltitoolsHeight[$idx] = new Array($num);\n".
+                            "        ltitoolsLinkDef[$idx] = new Array($num);\n".
+                            "        ltitoolsExplainDef[$idx] = new Array($num);\n".
+                            "        ltitoolsDisplay[$idx] = new Array($num);\n".
+                            "        ltitoolsLink[$idx] = new Array($num);\n".
+                            "        ltitoolsExplain[$idx] = new Array($num);\n".
+                            "        ltitoolsLabel[$idx] = new Array($num);\n".
+                            "        ltitoolsTitle[$idx] = new Array($num);\n".
+                            "        ltitoolsAppend[$idx] = new Array($num);\n";
+                my $i=0;
+                foreach my $key (sort { $a <=> $b } keys(%{$toolsref->{$type}})) {
+                    if (ref($toolsref->{$type}->{$key}) eq 'HASH') {
+                        if (ref($toolsref->{$type}->{$key}->{'display'}) eq 'HASH') {
+                            my $target = $toolsref->{$type}->{$key}->{'display'}->{'target'};
+                            my $width = $toolsref->{$type}->{$key}->{'display'}->{'width'};
+                            my $height = $toolsref->{$type}->{$key}->{'display'}->{'height'};
+                            my $linkdef = $toolsref->{$type}->{$key}->{'display'}->{'linktext'};
+                            my $explaindef = $toolsref->{$type}->{$key}->{'display'}->{'explanation'};
+                            my $providerurl;
+                            if ($toolsref->{$type}->{$key}->{'url'} =~ m{://}) {
+                                (my $prot,my $host,$providerurl) =
+                                    ($toolsref->{$type}->{$key}->{'url'} =~ m{^([^/]+)://([^/]+)(|/.+)$});
+                            } else {
+                                $providerurl = $toolsref->{$type}->{$key}->{'url'};
+                            }
+                            $providerurl = &LONCAPA::map::qtunescape($providerurl);
+                            $toolsjs .= "        ltitools[$idx][$i] = '$key';\n".
+                                        "        ltitoolsTarget[$idx][$i] = '$target';\n".
+                                        "        ltitoolsWidth[$idx][$i] = '$width';\n".
+                                        "        ltitoolsHeight[$idx][$i] = '$height';\n".
+                                        "        ltitoolsLinkDef[$idx][$i] = '$linkdef';\n".
+                                        "        ltitoolsExplainDef[$idx][$i] = '$explaindef';\n".
+                                        "        ltitoolsUrl[$idx][$i] = '$providerurl';\n";
+                        }
+                        if (ref($toolsref->{$type}->{$key}->{'crsconf'}) eq 'HASH') {
+                            my $display = $toolsref->{$type}->{$key}->{'crsconf'}->{'target'};
+                            $toolsjs .= "         ltitoolsDisplay[$idx][$i] = '$display';\n";
+                            my $linktext = $toolsref->{$type}->{$key}->{'crsconf'}->{'linktext'};
+                            $toolsjs .= "         ltitoolsLink[$idx][$i] = '$linktext';\n";
+                            my $explanation = $toolsref->{$type}->{$key}->{'crsconf'}->{'explanation'};
+                            $toolsjs .= "         ltitoolsExplain[$idx][$i] = '$explanation';\n";
+                            my $label = $toolsref->{$type}->{$key}->{'crsconf'}->{'label'};
+                            $toolsjs .= "         ltitoolsLabel[$idx][$i] = '$label';\n";
+                            my $title = $toolsref->{$type}->{$key}->{'crsconf'}->{'title'};
+                            $toolsjs .= "         ltitoolsTitle[$idx][$i] = '$title';\n";
+                            my $append = $toolsref->{$type}->{$key}->{'crsconf'}->{'append'};
+                            $toolsjs .= "         ltitoolsAppend[$idx][$i] = '$append';\n";
+                        }
                     }
-                    $providerurl = &LONCAPA::map::qtunescape($providerurl);
-                    $toolsjs .= '        ltitools['.$i.'] = '."'$key';\n".
-                                '        ltitoolsTarget['.$i.'] = '."'$target';\n".
-                                '        ltitoolsWidth['.$i.'] = '."'$width';\n".
-                                '        ltitoolsHeight['.$i.'] = '."'$height';\n".
-                                '        ltitoolsLinkDef['.$i.'] = '."'$linkdef';\n".
-                                '        ltitoolsExplainDef['.$i.'] = '."'$explaindef';\n".
-                                '        ltitoolsUrl['.$i.'] = '."'$providerurl';\n";
-                }
-                if (ref($toolsref->{$key}->{'crsconf'}) eq 'HASH') {
-                    my $display = $toolsref->{$key}->{'crsconf'}->{'target'};
-                    $toolsjs .= '         ltitoolsDisplay['.$i.'] = '."'$display';\n";
-                    my $linktext = $toolsref->{$key}->{'crsconf'}->{'linktext'};
-                    $toolsjs .= '         ltitoolsLink['.$i.'] = '."'$linktext';\n";
-                    my $explanation = $toolsref->{$key}->{'crsconf'}->{'explanation'};
-                    $toolsjs .= '         ltitoolsExplain['.$i.'] = '."'$explanation';\n";
-                    my $label = $toolsref->{$key}->{'crsconf'}->{'label'};
-                    $toolsjs .= '         ltitoolsLabel['.$i.'] = '."'$label';\n";
-                    my $title = $toolsref->{$key}->{'crsconf'}->{'title'};
-                    $toolsjs .= '         ltitoolsTitle['.$i.'] = '."'$title';\n";
-                    my $append = $toolsref->{$key}->{'crsconf'}->{'append'};
-                    $toolsjs .= '         ltitoolsAppend['.$i.'] = '."'$append';\n";
+                    $i++;
                 }
-
-                $i++;
+                my $firstoption = '<option value="" selected="selected">'.&mt('Select').'</option>';
+                my ($idsref,$titlesref) = &ordered_tooloptions($toolsref->{$type});
+                if ((ref($idsref) eq 'ARRAY') && (ref($titlesref) eq 'ARRAY')) {
+                    my $count = scalar(@{$idsref});
+                    $exttooloptions .= "        tooloptval[$idx] = new Array($count);\n".
+                                       "        toolopttxt[$idx] = new Array($count);\n";
+                    for (my $n=0; $n<@{$idsref}; $n++) {
+                        my $id = $idsref->[$n];
+                        my $text = $titlesref->[$n];
+                        $exttooloptions .= "         tooloptval[$idx][$n] = '$id';\n".
+                                           "         toolopttxt[$idx][$n] = '$text';\n";
+                    }
+                }
+                $exttoolnums .= "        ltitoolsnum[$idx] = $i;\n";
             }
+            $idx ++;
         }
     }
     my %js_lt = &Apache::lonlocal::texthash(
@@ -773,6 +925,7 @@
         nopopup => 'Pop-up blocked',
         nopriv  => 'Insufficient privileges to use preview',
         badurl  => 'URL is not: http://hostname/path or https://hostname/path',
+        sele    => 'Select',
     );
     &js_escape(\%js_lt);
 
@@ -811,17 +964,29 @@
     } else {
         title = escape(title);
         var info = exttoolurl;
+        var prefix = '';
+        if (supplementalflag == 1) {
+           prefix = 'supp';
+        }
         if (residx == 0) {
             var toolid = parseInt(extform.exttoolid.options[extform.exttoolid.selectedIndex].value);
             if (isNaN(toolid)) {
                 alert("$js_lt{'invtool'}");
                 return;
             }
-            info += ':'+toolid;
-        }
-        var prefix = '';
-        if (supplementalflag == 1) {
-           prefix = 'supp';
+            var typeelem = extform.elements[prefix+'exttooltype'];
+            if (typeelem.length) {
+                for (var i=0; i<typeelem.length; i++) {
+                    if (typeelem[i].checked) {
+                        tooltype = typeelem[i].value;
+                    }
+                }
+            }
+            if (tooltype == 'crs') {
+                info += ':c'+toolid;
+            } else {
+                info += ':'+toolid;
+            }
         }
         var dispdiv = prefix+'tooldispdiv';
         var windiv = prefix+'toolwindiv';
@@ -1035,6 +1200,77 @@
     }
 }
 
+function updateExttoolSel(form,radioname,supplementalflag) {
+    var prefix = '';
+    var typepick;
+    var radelem = form.elements[radioname];
+    if (radelem.length) {
+        for (var i=0; i<radelem.length; i++) {
+            if (radelem[i].checked) {
+                if (radelem[i].value == 'crs') {
+                    typepick = 0;
+                } else if (radelem[i].value == 'dom') {
+                    typepick = 1;
+                }
+                break;
+            }
+        }
+    }
+    if (supplementalflag == 1) {
+        prefix = 'supp';
+    }
+    $exttoolnums
+    $exttooloptions
+    if ((typepick == 0) || (typepick == 1)) {
+        var selelem = form.elements['exttoolid'];
+        var i, numopts = selelem.options.length -1;
+        if (numopts >=0) {
+            for (i = numopts; i >= 0; i--) {
+                selelem.remove(i);
+            }
+        }
+        if (ltitoolsnum[typepick]) {
+            if ((Array.isArray(tooloptval[typepick])) && (Array.isArray(toolopttxt[typepick]))) {
+                var len = tooloptval[typepick].length;
+                if (len) {
+                    selelem.options[selelem.options.length] = new Option('$js_lt{sele}','',1,1);
+                    var j;
+                    for (j=0; j<len; j++) {
+                        selelem.options[selelem.options.length] = new Option(toolopttxt[typepick][j],tooloptval[typepick][j]);
+                    }
+                    selelem.selectedIndex = 0;
+                }
+            }
+            if (document.getElementById('LC_exttoolon'+prefix)) {
+                document.getElementById('LC_exttoolon'+prefix).style.display = 'block';
+            }
+            if (document.getElementById('LC_exttooloff'+prefix)) {
+                document.getElementById('LC_exttooloff'+prefix).style.display = 'none';
+            }
+            if (document.getElementById('LC_addtool'+prefix)) {
+                document.getElementById('LC_addtool'+prefix).style.display = 'block';
+            }
+        } else {
+            if (document.getElementById('LC_exttoolon'+prefix)) {
+                document.getElementById('LC_exttoolon'+prefix).style.display = 'none';
+            }
+            if (document.getElementById('LC_exttooloff'+prefix)) {
+                document.getElementById('LC_exttooloff'+prefix).style.display = 'block';
+            }
+            if (document.getElementById('LC_addtool'+prefix)) {
+                document.getElementById('LC_addtool'+prefix).style.display = 'none';
+            }
+        }
+        if (selelem.options.length == 0) {
+            selelem.options[selelem.options.length] = new Option('','');
+            selelem.selectedIndex = 0;
+        }
+        updateExttool(selelem,form,supplementalflag);
+        resize_scrollbox('contentscroll','1','1');
+    }
+    return;
+}
+
 function updateExttool(caller,form,supplementalflag) {
     var prefix = '';
     if (supplementalflag == 1) {
@@ -1089,109 +1325,124 @@
                 document.getElementById(gradablediv).style.display = 'none';
             }
         } else {
-            if (ltitools.length > 0) {
-                for (var j=0; j<ltitools.length; j++) {
-                    if (ltitools[j] == toolpick) {
-                        if (document.getElementById(dispdiv)) {
-                            if (ltitoolsDisplay[j]) {
-                                document.getElementById(dispdiv).style.display = 'block';
-                                if (form.exttooltarget.length) {
-                                    for (var k=0; k<form.exttooltarget.length; k++) {
-                                        if (form.exttooltarget[k].value == ltitoolsTarget[j]) {
-                                            form.exttooltarget[k].checked = true;
-                                            break;
+            var tooltype = '';
+            var typeelem = form.elements[prefix+'exttooltype'];
+            if (typeelem.length) {
+                for (var i=0; i<typeelem.length; i++) {
+                    if (typeelem[i].checked) {
+                        tooltype = typeelem[i].value;
+                    }
+                }
+            }
+            if ((tooltype == 'crs') || (tooltype == 'dom')) {
+                var i = 0;
+                if (tooltype == 'dom') {
+                    i = 1;
+                }
+                if (ltitools[i].length > 0) {
+                    for (var j=0; j<ltitools[i].length; j++) {
+                        if (ltitools[i][j] == toolpick) {
+                            if (document.getElementById(dispdiv)) {
+                                if (ltitoolsDisplay[i][j]) {
+                                    document.getElementById(dispdiv).style.display = 'block';
+                                    if (form.exttooltarget.length) {
+                                        for (var k=0; k<form.exttooltarget.length; k++) {
+                                            if (form.exttooltarget[k].value == ltitoolsTarget[i][j]) {
+                                                form.exttooltarget[k].checked = true;
+                                                break;
+                                            }
                                         }
                                     }
                                 }
-                            }
-                            var dimen = 'none';
-                            var dimenwidth = '';
-                            var dimenheight = '';
-                            if ((ltitoolsDisplay[j]) && (ltitoolsTarget[j] == 'window')) {
-                                dimen = 'block';
-                                dimenwidth = ltitoolsWidth[j];
-                                dimenheight = ltitoolsHeight[j];                    
-                            }
-                            if (document.getElementById(dimendiv)) {
-                                document.getElementById(dimendiv).style.display = dimen;
-                            }
-                            if (document.getElementById(widthinput)) {
-                                document.getElementById(widthinput).value = dimenwidth;
-                            }
-                            if (document.getElementById(heightinput)) {
-                                document.getElementById(heightinput).value = dimenheight;
-                            }
-                        }
-                        if (document.getElementById(windiv)) {
-                            if ((ltitoolsTarget[j] == 'window') || (ltitoolsTarget[j] == 'tab')) {
-                                document.getElementById(windiv).style.display = 'block';
-                            } else {
-                                document.getElementById(windiv).style.display = 'none';
-                            }
-                            if (document.getElementById(linktextdiv)) {
-                                if (ltitoolsLink[j]) {
-                                    document.getElementById(linktextdiv).style.display = 'inline';
-                                } else {
-                                    document.getElementById(linktextdiv).style.display = 'none';
+                                var dimen = 'none';
+                                var dimenwidth = '';
+                                var dimenheight = '';
+                                if ((ltitoolsDisplay[i][j]) && (ltitoolsTarget[i][j] == 'window')) {
+                                    dimen = 'block';
+                                    dimenwidth = ltitoolsWidth[i][j];
+                                    dimenheight = ltitoolsHeight[i][j];                    
+                                }
+                                if (document.getElementById(dimendiv)) {
+                                    document.getElementById(dimendiv).style.display = dimen;
+                                }
+                                if (document.getElementById(widthinput)) {
+                                    document.getElementById(widthinput).value = dimenwidth;
+                                }
+                                if (document.getElementById(heightinput)) {
+                                    document.getElementById(heightinput).value = dimenheight;
                                 }
                             }
-                            if (document.getElementById(linktextinput)) {
-                                if (ltitoolsLink[j]) {
-                                    document.getElementById(linktextinput).value = ltitoolsLinkDef[j]; 
+                            if (document.getElementById(windiv)) {
+                                if ((ltitoolsTarget[i][j] == 'window') || (ltitoolsTarget[i][j] == 'tab')) {
+                                    document.getElementById(windiv).style.display = 'block';
                                 } else {
-                                    document.getElementById(linktextinput).value = '';
+                                    document.getElementById(windiv).style.display = 'none';
+                                }
+                                if (document.getElementById(linktextdiv)) {
+                                    if (ltitoolsLink[i][j]) {
+                                        document.getElementById(linktextdiv).style.display = 'inline';
+                                    } else {
+                                        document.getElementById(linktextdiv).style.display = 'none';
+                                    }
+                                }
+                                if (document.getElementById(linktextinput)) {
+                                    if (ltitoolsLink[i][j]) {
+                                        document.getElementById(linktextinput).value = ltitoolsLinkDef[i][j]; 
+                                    } else {
+                                        document.getElementById(linktextinput).value = '';
+                                    }
+                                }
+                                if (document.getElementById(explanationdiv)) {
+                                    if (ltitoolsExplain[i][j]) {
+                                        document.getElementById(explanationdiv).style.display = 'inline';
+                                    } else {
+                                        document.getElementById(explanationdiv).style.display = 'none';
+                                    }
+                                }
+                                if (document.getElementById(explanationinput)) {
+                                    if (ltitoolsExplain[i][j]) {
+                                        document.getElementById(explanationinput).value = ltitoolsExplainDef[i][j];
+                                    } else {
+                                        document.getElementById(explananationinput).value = '';
+                                    }
                                 }
                             }
-                            if (document.getElementById(explanationdiv)) {
-                                if (ltitoolsExplain[j]) {
-                                    document.getElementById(explanationdiv).style.display = 'inline';
+                            if (document.getElementById(labeldiv)) {
+                                if (ltitoolsLabel[i][j]) {
+                                    document.getElementById(labeldiv).style.display = 'inline';
                                 } else {
-                                    document.getElementById(explanationdiv).style.display = 'none';
+                                    document.getElementById(labeldiv).style.display = 'none';
                                 }
                             }
-                            if (document.getElementById(explanationinput)) {
-                                if (ltitoolsExplain[j]) {
-                                    document.getElementById(explanationinput).value = ltitoolsExplainDef[j];
+                            if (document.getElementById(titlediv)) {
+                                if (ltitoolsTitle[i][j]) {
+                                    document.getElementById(titlediv).style.display = 'inline';
                                 } else {
-                                    document.getElementById(explananationinput).value = '';
+                                    document.getElementById(titlediv).style.display = 'none';
                                 }
                             }
-                        }
-                        if (document.getElementById(labeldiv)) {
-                            if (ltitoolsLabel[j]) {
-                                document.getElementById(labeldiv).style.display = 'inline';
-                            } else {
-                                document.getElementById(labeldiv).style.display = 'none';
-                            } 
-                        }
-                        if (document.getElementById(titlediv)) {
-                            if (ltitoolsTitle[j]) {
-                                document.getElementById(titlediv).style.display = 'inline';
-                            } else {
-                                document.getElementById(titlediv).style.display = 'none';
-                            }
-                        }
-                        if (document.getElementById(appenddiv)) {
-                            if (ltitoolsAppend[j]) {
-                                document.getElementById(appenddiv).style.display = 'inline';
-                                if (document.getElementById(providerurl)) {
-                                    if ((ltitoolsUrl[j] != '') && (ltitoolsUrl[j] != null)) {
-                                        document.getElementById(providerurl).innerHTML = ' ('+ltitoolsUrl[j]+')<br />';
+                            if (document.getElementById(appenddiv)) {
+                                if (ltitoolsAppend[i][j]) {
+                                    document.getElementById(appenddiv).style.display = 'inline';
+                                    if (document.getElementById(providerurl)) {
+                                        if ((ltitoolsUrl[i][j] != '') && (ltitoolsUrl[i][j] != null)) {
+                                            document.getElementById(providerurl).innerHTML = ' ('+ltitoolsUrl[i][j]+')<br />';
+                                        }
+                                    }
+                                } else {
+                                    document.getElementById(appenddiv).style.display = 'none';
+                                    if (document.getElementById(providerurl)) {
+                                        document.getElementById(providerurl).innerHTML = '';
                                     }
-                                }
-                            } else {
-                                document.getElementById(appenddiv).style.display = 'none';
-                                if (document.getElementById(providerurl)) {
-                                    document.getElementById(providerurl).innerHTML = '';
                                 }
                             }
-                        }
-                        if (document.getElementById(gradablediv)) {
-                            if (supplementalflag != 1) {
-                                document.getElementById(gradablediv).style.display = 'inline';
+                            if (document.getElementById(gradablediv)) {
+                                if (supplementalflag != 1) {
+                                    document.getElementById(gradablediv).style.display = 'inline';
+                                }
                             }
+                            break;
                         }
-                        break;
                     }
                 }
             }
Index: loncom/interface/lonexttool.pm
diff -u loncom/interface/lonexttool.pm:1.23 loncom/interface/lonexttool.pm:1.24
--- loncom/interface/lonexttool.pm:1.23	Tue Mar 29 20:12:46 2022
+++ loncom/interface/lonexttool.pm	Mon May 22 21:10:55 2023
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # Launch External Tool Provider (LTI)
 #
-# $Id: lonexttool.pm,v 1.23 2022/03/29 20:12:46 raeburn Exp $
+# $Id: lonexttool.pm,v 1.24 2023/05/22 21:10:55 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -96,13 +96,20 @@
     my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'};
     my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'};
     my $chome = $env{'course.'.$env{'request.course.id'}.'.home'};
-    my ($idx,$is_tool,%toolhash,%toolsettings);
+    my ($idx,$crstool,$is_tool,%toolhash,%toolsettings);
 
     if ($r->uri eq "/adm/$cdom/$cnum/$marker/$exttool") {
         %toolsettings=&Apache::lonnet::dump('exttool_'.$marker,$cdom,$cnum);
         if ($toolsettings{'id'}) {
-            $idx = $toolsettings{'id'};
-            my %ltitools = &Apache::lonnet::get_domain_lti($cdom,'consumer');
+            my %ltitools;
+            if ($toolsettings{'id'} =~ /^c(\d+)$/) {
+                $idx = $1;
+                $crstool = 1;
+                %ltitools = &Apache::lonnet::get_course_lti($cnum,$cdom,'consumer');
+            } else {
+                $idx = $toolsettings{'id'};
+                %ltitools = &Apache::lonnet::get_domain_lti($cdom,'consumer');
+            }
             if (ref($ltitools{$idx}) eq 'HASH') {
                 %toolhash = %{$ltitools{$idx}};
                 $toolhash{'display'} = {
@@ -204,17 +211,19 @@
             }
         }
         my $submittext = &mt('Launch [_1]',$toolhash{'title'});
-        if (($toolhash{'key'} ne '') && ($toolhash{'secret'} ne '') && 
-            ($toolhash{'url'} ne '') && ($launchok)) {
+        if (($toolhash{'url'} ne '') && ($launchok)) {
             my %lti = &lti_params($r,$cnum,$cdom,$idx,$submittext,\%toolhash);
             my $url = $toolhash{'url'};
             if ($toolhash{'crsappend'} ne '') {
                 $url .= $toolhash{'crsappend'};
             }
-            $r->print(&launch_html($url,$toolhash{'key'},$toolhash{'secret'},
-                                   $toolhash{'sigmethod'},$submittext,\%lti));
+            my %info = (
+                         method => $toolhash{'sigmethod'},
+                       );
+            $r->print(&launch_html($cdom,$cnum,$crstool,$url,$idx,
+                                   $toolhash{'cipher'},$submittext,\%lti,\%info));
         } else {
-            $r->print('<div>'.&mt('External Tool Unavailable').'</div>');
+            $r->print('<div class="LC_warning">'.&mt('External Tool Unavailable').'</div>');
         }
     }
     return OK;
@@ -434,8 +443,13 @@
 }
 
 sub launch_html {
-    my ($url,$key,$secret,$sigmethod,$submittext,$paramsref) = @_;
-    my $hashref = &LONCAPA::ltiutils::sign_params($url,$key,$secret,$paramsref,$sigmethod);
+    my ($cdom,$cnum,$crstool,$url,$idx,$keynum,$submittext,$paramsref,$inforef) = @_;
+    my ($status,$hashref) =
+        &Apache::lonnet::sign_lti($cdom,$cnum,$crstool,$url,$idx,$keynum,
+                                  '',$paramsref,$inforef);
+    unless ($status eq 'ok') {
+        return '<div class="LC_warning">'.&mt('External Tool Unavailable').'</div>';
+    }
     my $action = &HTML::Entities::encode($url,'<>&"');
     my $form = <<"END";
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
Index: loncom/interface/lonparmset.pm
diff -u loncom/interface/lonparmset.pm:1.619 loncom/interface/lonparmset.pm:1.620
--- loncom/interface/lonparmset.pm:1.619	Mon Apr  3 19:53:30 2023
+++ loncom/interface/lonparmset.pm	Mon May 22 21:10:55 2023
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # Handler to set parameters for assessments
 #
-# $Id: lonparmset.pm,v 1.619 2023/04/03 19:53:30 raeburn Exp $
+# $Id: lonparmset.pm,v 1.620 2023/05/22 21:10:55 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -1849,7 +1849,7 @@
                 $extra = 'ltid_'.$domltistr;
             }
         }
-        my %courselti = &Apache::lonnet::get_course_lti($cnum,$cdom);
+        my %courselti = &Apache::lonnet::get_course_lti($cnum,$cdom,'provider');
         if (keys(%courselti)) {
             foreach my $item (sort { $a <=> $b } keys(%courselti)) {
                 if (($item =~ /^\d+$/) && (ref($courselti{$item}) eq 'HASH')) {
@@ -5345,7 +5345,8 @@
     }
     my %courselti =
         &Apache::lonnet::get_course_lti($env{'course.'.$env{'request.course.id'}.'.num'},
-                                        $env{'course.'.$env{'request.course.id'}.'.domain'});
+                                        $env{'course.'.$env{'request.course.id'}.'.domain'},
+                                        'provider');
     foreach my $item (keys(%courselti)) {
         if (ref($courselti{$item}) eq 'HASH') {
             $crslti{$item} = $courselti{$item}{'name'};
Index: loncom/lonnet/perl/lonnet.pm
diff -u loncom/lonnet/perl/lonnet.pm:1.1509 loncom/lonnet/perl/lonnet.pm:1.1510
--- loncom/lonnet/perl/lonnet.pm:1.1509	Mon May 22 15:09:15 2023
+++ loncom/lonnet/perl/lonnet.pm	Mon May 22 21:10:55 2023
@@ -1,7 +1,7 @@
 # The LearningOnline Network
 # TCP networking package
 #
-# $Id: lonnet.pm,v 1.1509 2023/05/22 15:09:15 raeburn Exp $
+# $Id: lonnet.pm,v 1.1510 2023/05/22 21:10:55 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -415,6 +415,60 @@
     return $response;
 }
 
+sub sign_lti {
+    my ($cdom,$cnum,$crstool,$url,$idx,$keynum,$post,$paramsref,$inforef) = @_;
+    my $chome;
+    if (&domain($cdom) ne '') {
+        if ($crstool) {
+            $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_params($cdom,$cnum,$crstool,$url,
+                                                $idx,$keynum,$post,
+                                                $perlvar{'lonVersion'},
+                                                $paramsref,$inforef);
+                if ($rep ne '') {
+                    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:$crstool:$escurl:$idx:$keynum:$post:$params:$info",$chome);
+            }
+            if (($rep eq '') || ($rep =~ /^con_lost|error|no_such_host|unknown_cmd/i)) {
+                return ();
+            } 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)=@_;
@@ -2700,7 +2754,7 @@
                                   'coursecategories','ssl','autoenroll',
                                   'trust','helpsettings','wafproxy',
                                   'ltisec','toolsec','domexttool',
-                                  'exttool',],$domain);
+                                  'exttool'],$domain);
     my @coursetypes = ('official','unofficial','community','textbook','placement');
     if (ref($domconfig{'defaults'}) eq 'HASH') {
         $domdefaults{'lang_def'} = $domconfig{'defaults'}{'lang_def'}; 
@@ -12538,20 +12592,29 @@
 }
 
 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 {
Index: loncom/Lond.pm
diff -u loncom/Lond.pm:1.21 loncom/Lond.pm:1.22
--- loncom/Lond.pm:1.21	Thu Feb 17 22:35:50 2022
+++ loncom/Lond.pm	Mon May 22 21:10:56 2023
@@ -1,6 +1,6 @@
 # The LearningOnline Network
 #
-# $Id: Lond.pm,v 1.21 2022/02/17 22:35:50 raeburn Exp $
+# $Id: Lond.pm,v 1.22 2023/05/22 21:10:56 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -43,6 +43,9 @@
 use Crypt::PKCS10;
 use Net::OAuth;
 use Crypt::CBC;
+use Net::OAuth;
+use Digest::SHA;
+use Digest::MD5 qw(md5_hex);
 
 sub dump_with_regexp {
     my ( $tail, $clientversion ) = @_;
@@ -1254,6 +1257,122 @@
     return $itemid;
 }
 
+sub sign_params {
+    my ($cdom,$cnum,$crstool,$url,$idx,$keynum,$post,$loncaparev,$paramsref,$inforef) = @_;
+    return unless (ref($paramsref) eq 'HASH');
+    my ($sigmethod,$type,$callback);
+    if (ref($inforef) eq 'HASH') {
+        if (exists($inforef->{'method'})) {
+            $sigmethod = $inforef->{'method'};
+        }
+        if (exists($inforef->{'cb'})) {
+            $callback = $inforef->{'cb'};
+        }
+        if (exists($inforef->{'type'})) {
+            $type = $inforef->{'type'};
+        }
+    }
+    my ($cachename,$hashid,$key,$secret,%ltitoolsenc);
+    if ($crstool) {
+        $cachename = 'crsltitoolsenc';
+        $hashid = $cdom.'_'.$cnum;
+    } else {
+        $cachename = 'ltitoolsenc';
+        $hashid = $cdom;
+    }
+    my ($encresult,$enccached)=&Apache::lonnet::is_cached_new($cachename,$hashid);
+    if (defined($enccached)) {
+        if (ref($encresult) eq 'HASH') {
+            %ltitoolsenc = %{$encresult};
+        }
+    } else {
+        if ($crstool) {
+            my $reply = &dump_with_regexp(join(":",($cdom,$cnum,'nohist_toolsenc','','')),$loncaparev);
+            %ltitoolsenc = %{&Apache::lonnet::unserialize($reply)};
+        } else {
+            my $reply = &get_dom("getdom:$cdom:encconfig:ltitools");
+            my $ltitoolsencref = &Apache::lonnet::thaw_unescape($reply);
+            if (ref($ltitoolsencref) eq 'HASH') {
+                %ltitoolsenc = %{$ltitoolsencref};
+            }
+        }
+        my $cachetime = 24*60*60;
+        &Apache::lonnet::do_cache_new($cachename,$hashid,\%ltitoolsenc,$cachetime);
+    }
+    if (!keys(%ltitoolsenc)) {
+         return;
+    } elsif (exists($ltitoolsenc{$idx})) {
+        if (ref($ltitoolsenc{$idx}) eq 'HASH') {
+            if (exists($ltitoolsenc{$idx}{'key'})) {
+                $key = $ltitoolsenc{$idx}{'key'};
+            }
+            if (exists($ltitoolsenc{$idx}{'secret'})) {
+                $secret = $ltitoolsenc{$idx}{'secret'};
+                my $privhost;
+                if ($keynum =~ /^\d+$/) {
+                    if ($crstool) {
+                        my $primary = &Apache::lonnet::domain($cdom,'primary');
+                        my @ids = &Apache::lonnet::current_machine_ids();
+                        unless (grep(/^\Q$primary\E$/, at ids)) {
+                            $privhost = $primary;
+                            my ($result,$plainsecret) = &decrypt_secret($privhost,$secret,$keynum,'ltitools');
+                            if ($result eq 'ok') {
+                                $secret = $plainsecret;
+                            } else {
+                                undef($secret);
+                            }
+                        }
+                    }
+                    unless ($privhost) {
+                        my $privkey = &get_dom("getdom:$cdom:private:$keynum:ltitools:key");
+                        if (($privkey ne '') && ($secret ne '')) {
+                            my $cipher = new Crypt::CBC($privkey);
+                            $secret = $cipher->decrypt_hex($secret);
+                        } else {
+                            undef($secret);
+                        }
+                    }
+                }
+            }
+        }
+    }
+    return if (($key eq '') || ($secret eq ''));
+    if ($sigmethod eq '') {
+        $sigmethod = 'HMAC-SHA1';
+    }
+    if ($type eq '') {
+        $type = 'request token';
+    }
+    if ($callback eq '') {
+        $callback = 'about:blank',
+    }
+    srand( time() ^ ($$ + ($$ << 15))  ); # Seed rand.
+    my $nonce = Digest::SHA::sha1_hex(sprintf("%06x%06x",rand(0xfffff0),rand(0xfffff0)));
+    my $request = Net::OAuth->request($type)->new(
+            consumer_key => $key,
+            consumer_secret => $secret,
+            request_url => $url,
+            request_method => 'POST',
+            signature_method => $sigmethod,
+            timestamp => time,
+            nonce => $nonce,
+            callback => $callback,
+            extra_params => $paramsref,
+            version      => '1.0',
+            );
+    $request->sign();
+    if ($post) {
+        return $request->to_post_body();
+    } else {
+        return $request->to_hash();
+    }
+}
+
+sub decrypt_secret {
+    my ($privhost,$secret,$keynum,$type) = @_;
+    return;
+}
+
 1;
 
 __END__
Index: loncom/lond
diff -u loncom/lond:1.576 loncom/lond:1.577
--- loncom/lond:1.576	Mon Jul 25 23:31:40 2022
+++ loncom/lond	Mon May 22 21:10:56 2023
@@ -2,7 +2,7 @@
 # The LearningOnline Network
 # lond "LON Daemon" Server (port "LOND" 5663)
 #
-# $Id: lond,v 1.576 2022/07/25 23:31:40 raeburn Exp $
+# $Id: lond,v 1.577 2023/05/22 21:10:56 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -65,7 +65,7 @@
 my $status='';
 my $lastlog='';
 
-my $VERSION='$Revision: 1.576 $'; #' stupid emacs
+my $VERSION='$Revision: 1.577 $'; #' stupid emacs
 my $remoteVERSION;
 my $currenthostid="default";
 my $currentdomainid;
@@ -306,6 +306,7 @@
                servertimezone => {remote => 1, enroll => 1},
                setannounce => {remote => 1, domroles => 1},
                sethost => {anywhere => 1},
+               signlti => {remote => 1},
                store => {remote => 1, enroll => 1, reqcrs => 1,},
                studentphoto => {remote => 1, enroll => 1},
                sub => {content => 1,},
@@ -5307,6 +5308,76 @@
 &register_handler("lti", \&lti_handler, 1, 1, 0);
 
 #
+# LTI data for launch payload (received encrypted) are unencrypted and
+# then signed with the appropriate key and secret, before re-encrypting
+# for sending as the signed payload to the client (caller lonnet::sign_lti()).
+#
+# Parameters:
+#   $cmd             - Command request keyword (signlti).
+#   $tail            - Tail of the command.  This is a colon-separated list
+#                      consisting of the domain, coursenum (if for an External
+#                      Tool defined in a course), crstool (true if defined in
+#                      a course), escaped launch URL, numeric ID of external tool
+#                      version number for encryption key (if tool's LTI secret was
+#                      encrypted before storing), post (true if signed data are
+#                      to be returned from Net::OAuth, as a post_body),
+#                      a frozen hash of LTI launch parameters, and a frozen hash
+#                      of LTI config data (i.e., method => signature method).
+#   $client          - File descriptor open on the client.
+# Returns:
+#   1       - Continue processing.
+#   0       - Exit.
+#  Side effects:
+#     The reply will contain the LTI payload, as & separated key=value pairs,
+#     where value is itself a frozen hash, if the required key and secret
+#     for the apecific tool ID are available. The payload data are retrived from
+#     a call to Lond::sign_params(), and the reply is encrypted before being
+#     written to $client.
+#
+sub sign_lti_handler {
+    my ($cmd, $tail, $client) = @_;
+
+    my $userinput = "$cmd:$tail";
+
+    my ($cdom,$cnum,$crstool,$escurl,$idx,$keynum,$post,$paramsref,$inforef) = split(/:/,$tail);
+    my $url = &unescape($escurl);
+    my $params = &Apache::lonnet::thaw_unescape($paramsref);
+    my $info = &Apache::lonnet::thaw_unescape($inforef);
+    my $res =
+        &LONCAPA::Lond::sign_params($cdom,$cnum,$crstool,$url,$idx,$keynum,
+                                    $post,$perlvar{'lonVersion'},$params,$info);
+    my $result;
+    if (ref($res) eq 'HASH') {
+        foreach my $key (keys(%{$res})) {
+            $result .= &escape($key).'='.&Apache::lonnet::freeze_escape($res->{$key}).'&';
+        }
+        $result =~ s/\&$//;
+    } else {
+        $result = $res;
+    }
+    if ($result =~ /^error:/) {
+        &Failure($client, \$result, $userinput);
+    } else {
+        if ($cipher) {
+            my $cmdlength=length($result);
+            $result.="         ";
+            my $encres='';
+            for (my $encidx=0;$encidx<=$cmdlength;$encidx+=8) {
+                $encres.= unpack("H16",
+                                 $cipher->encrypt(substr($result,
+                                                         $encidx,
+                                                         8)));
+            }
+            &Reply( $client,"enc:$cmdlength:$encres\n",$userinput);
+        } else {
+            &Failure( $client, "error:no_key\n",$userinput);
+        }
+    }
+    return 1;
+}
+&register_handler("signlti", \&sign_lti_handler, 1, 1, 0);
+
+#
 #  Puts an id to a domains id database. 
 #
 #  Parameters:


More information about the LON-CAPA-cvs mailing list