[LON-CAPA-cvs] cvs: doc /loncapafiles loncapafiles.lpml loncom/interface domainprefs.pm londocs.pm lonextresedit.pm lonexttool.pm lonhtmlcommon.pm lonmenu.pm lonsyllabus.pm loncom/rewrites loncapa_rewrite_off.conf loncapa_rewrite_on.conf rat lonuserstate.pm lonwrapper.pm

raeburn raeburn at source.lon-capa.org
Tue Jan 26 09:30:52 EST 2016


raeburn		Tue Jan 26 14:30:52 2016 EDT

  Modified files:              
    /loncom/interface	domainprefs.pm londocs.pm lonextresedit.pm 
                     	lonexttool.pm lonhtmlcommon.pm lonmenu.pm 
                     	lonsyllabus.pm 
    /loncom/rewrites	loncapa_rewrite_off.conf loncapa_rewrite_on.conf 
    /rat	lonuserstate.pm lonwrapper.pm 
    /doc/loncapafiles	loncapafiles.lpml 
  Log:
  - Bug 6754. Make LON-CAPA an LTI Tool Consumer (LTI 1.1). Work in progress.  
  
  
-------------- next part --------------
Index: loncom/interface/domainprefs.pm
diff -u loncom/interface/domainprefs.pm:1.266 loncom/interface/domainprefs.pm:1.267
--- loncom/interface/domainprefs.pm:1.266	Mon Jun 15 20:11:56 2015
+++ loncom/interface/domainprefs.pm	Tue Jan 26 14:30:24 2016
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # Handler to set domain-wide configuration settings
 #
-# $Id: domainprefs.pm,v 1.266 2015/06/15 20:11:56 raeburn Exp $
+# $Id: domainprefs.pm,v 1.267 2016/01/26 14:30:24 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -173,6 +173,7 @@
 use Locale::Language;
 use DateTime::TimeZone;
 use DateTime::Locale;
+use Time::HiRes qw( sleep );
 
 my $registered_cleanup;
 my $modified_urls;
@@ -215,13 +216,14 @@
                 'contacts','defaults','scantron','coursecategories',
                 'serverstatuses','requestcourses','helpsettings',
                 'coursedefaults','usersessions','loadbalancing',
-                'requestauthor','selfenrollment','inststatus'],$dom);
+                'requestauthor','selfenrollment','inststatus',
+                'ltitools'],$dom);
     my @prefs_order = ('rolecolors','login','defaults','quotas','autoenroll',
                        'autoupdate','autocreate','directorysrch','contacts',
                        'usercreation','selfcreation','usermodification','scantron',
                        'requestcourses','requestauthor','coursecategories',
-                       'serverstatuses','helpsettings',
-                       'coursedefaults','selfenrollment','usersessions');
+                       'serverstatuses','helpsettings','coursedefaults',
+                       'ltitools','selfenrollment','usersessions');
     my %existing;
     if (ref($domconfig{'loadbalancing'}) eq 'HASH') {
         %existing = %{$domconfig{'loadbalancing'}};
@@ -472,6 +474,15 @@
                   print => \&print_loadbalancing,
                   modify => \&modify_loadbalancing,
                  },
+         'ltitools' => 
+                 {text => 'External Tools (LTI)',
+                  help => 'Domain_configuration_LTI_Tools',
+                  header => [{col1 => 'Setting',
+                              col2 => 'Value',}],
+                  print => \&print_ltitools,
+                  modify => \&modify_ltitools,
+                 },
+ 
     );
     if (keys(%servers) > 1) {
         $prefs{'login'}  = { text   => 'Log-in page options',
@@ -645,6 +656,8 @@
         $output = &modify_usersessions($dom,$lastactref,%domconfig);
     } elsif ($action eq 'loadbalancing') {
         $output = &modify_loadbalancing($dom,%domconfig);
+    } elsif ($action eq 'ltitools') {
+        $output = &modify_ltitools($r,$dom,$action,$lastactref,%domconfig);
     }
     return $output;
 }
@@ -904,7 +917,8 @@
         if ($action eq 'quotas') {
             $output .= &print_quotas($dom,$settings,\$rowtotal,$action);
         } elsif (($action eq 'autoenroll') || ($action eq 'autocreate') || ($action eq 'directorysrch') ||
-                 ($action eq 'contacts') || ($action eq 'serverstatuses') || ($action eq 'loadbalancing')) {
+                 ($action eq 'contacts') || ($action eq 'serverstatuses') || ($action eq 'loadbalancing') ||
+                 ($action eq 'ltitools')) {
             $output .= $item->{'print'}->($dom,$settings,\$rowtotal);
         } elsif ($action eq 'scantron') {
             $output .= &print_scantronformat($r,$dom,$confname,$settings,\$rowtotal);
@@ -2258,8 +2272,7 @@
                               (' 'x2).
                               '<span class="LC_nobreak">'.&mt('Thumbnail:');
                 if ($image) {
-                    $datatable .= '<span class="LC_nobreak">'.
-                                  $imgsrc.
+                    $datatable .= $imgsrc.
                                   '<label><input type="checkbox" name="'.$type.'_image_del"'.
                                   ' value="'.$key.'" />'.&mt('Delete?').'</label></span> '.
                                   '<span class="LC_nobreak"> '.&mt('Replace:').' ';
@@ -2413,6 +2426,74 @@
 ENDSCRIPT
 }
 
+sub ltitools_javascript {
+    my ($settings) = @_;
+    return unless(ref($settings) eq 'HASH');
+    my (%ordered,$total,%jstext);
+    $total = 0;
+    foreach my $item (keys(%{$settings})) {
+        if (ref($settings->{$item}) eq 'HASH') {
+            my $num = $settings->{$item}{'order'};
+            $ordered{$num} = $item;
+        }
+    }
+    $total = scalar(keys(%{$settings}));
+    my @jsarray = ();
+    foreach my $item (sort {$a <=> $b } (keys(%ordered))) {
+        push(@jsarray,$ordered{$item});
+    }
+    my $jstext = '    var ltitools = Array('."'".join("','", at jsarray)."'".');'."\n";
+    return <<"ENDSCRIPT";
+<script type="text/javascript">
+// <![CDATA[
+function reorderLTI(form,item) {
+    var changedVal;
+$jstext
+    var newpos = 'ltitools_add_pos';
+    var maxh = 1 + $total;
+    var current = new Array;
+    var newitemVal = form.elements[newpos].options[form.elements[newpos].selectedIndex].value;
+    if (item == newpos) {
+        changedVal = newitemVal;
+    } else {
+        changedVal = form.elements[item].options[form.elements[item].selectedIndex].value;
+        current[newitemVal] = newpos;
+    }
+    for (var i=0; i<ltitools.length; i++) {
+        var elementName = 'ltitools_'+ltitools[i];
+        if (elementName != item) {
+            if (form.elements[elementName]) {
+                var currVal = form.elements[elementName].options[form.elements[elementName].selectedIndex].value;
+                current[currVal] = elementName;
+            }
+        }
+    }
+    var oldVal;
+    for (var j=0; j<maxh; j++) {
+        if (current[j] == undefined) {
+            oldVal = j;
+        }
+    }
+    if (oldVal < changedVal) {
+        for (var k=oldVal+1; k<=changedVal ; k++) {
+           var elementName = current[k];
+           form.elements[elementName].selectedIndex = form.elements[elementName].selectedIndex - 1;
+        }
+    } else {
+        for (var k=changedVal; k<oldVal; k++) {
+            var elementName = current[k];
+            form.elements[elementName].selectedIndex = form.elements[elementName].selectedIndex + 1;
+        }
+    }
+    return;
+}
+
+// ]]>
+</script>
+
+ENDSCRIPT
+}
+
 sub print_autoenroll {
     my ($dom,$settings,$rowtotal) = @_;
     my $autorun = &Apache::lonnet::auto_run(undef,$dom),
@@ -2898,6 +2979,325 @@
     return ($datatable,$itemcount);
 }
 
+sub print_ltitools {
+    my ($dom,$settings,$rowtotal) = @_;
+    my $rownum = 0;
+    my $css_class;
+    my $itemcount = 1;
+    my $maxnum = 0;
+    my %ordered;
+    if (ref($settings) eq 'HASH') {
+        foreach my $item (keys(%{$settings})) {
+            if (ref($settings->{$item}) eq 'HASH') {
+                my $num = $settings->{$item}{'order'};
+                $ordered{$num} = $item;
+            }
+        }
+    }
+    my $confname = $dom.'-domainconfig';
+    my $switchserver = &check_switchserver($dom,$confname);
+    my $maxnum = scalar(keys(%ordered));
+    my $datatable = &ltitools_javascript($settings);
+    my %lt = &ltitools_names();
+    my @courseroles = ('cc','in','ta','ep','st');
+    my @ltiroles = qw(Instructor ContentDeveloper TeachingAssistant Learner);
+    my @fields = ('fullname','firstname','lastname','email','user','roles');
+    if (keys(%ordered)) {
+        my @items = sort { $a <=> $b } keys(%ordered);
+        for (my $i=0; $i<@items; $i++) {
+            $css_class = $itemcount%2?' class="LC_odd_row"':'';
+            my $item = $ordered{$items[$i]};
+            my ($title,$key,$secret,$url,$imgsrc,$version);
+            if (ref($settings->{$item}) eq 'HASH') {
+                $title = $settings->{$item}->{'title'};
+                $url = $settings->{$item}->{'url'};
+                $key = $settings->{$item}->{'key'};
+                $secret = $settings->{$item}->{'secret'};
+                my $image = $settings->{$item}->{'image'};
+                if ($image ne '') {
+                    $imgsrc = '<img src="'.$image.'" alt="'.&mt('Tool Provider icon').'" />';
+                }
+            }
+            my $chgstr = ' onchange="javascript:reorderLTI(this.form,'."'ltitools_".$item."'".');"';
+            $datatable .= '<tr '.$css_class.'><td><span class="LC_nobreak">'
+                         .'<select name="ltitools_'.$item.'"'.$chgstr.'>';
+            for (my $k=0; $k<=$maxnum; $k++) {
+                my $vpos = $k+1;
+                my $selstr;
+                if ($k == $i) {
+                    $selstr = ' selected="selected" ';
+                }
+                $datatable .= '<option value="'.$k.'"'.$selstr.'>'.$vpos.'</option>';
+            }
+            $datatable .= '</select>'.(' 'x2).
+                '<label><input type="checkbox" name="ltitools_del" value="'.$item.'" />'.
+                &mt('Delete?').'</label></span></td>'.
+                '<td colspan="2">'.
+                '<fieldset><legend>'.&mt('Required settings').'</legend>'.
+                '<span class="LC_nobreak">'.$lt{'title'}.':<input type="text" size="30" name="ltitools_title_'.$i.'" value="'.$title.'" /></span> '.
+                (' 'x2).
+                '<span class="LC_nobreak">'.$lt{'version'}.':<select name="ltitools_version_'.$i.'">'.
+                '<option value="LTI-1p0" selected="selected">1.1</option></select></span> '.
+                (' 'x2).
+                '<span class="LC_nobreak">'.$lt{'msgtype'}.':<select name="ltitools_msgtype_'.$i.'">'.
+                '<option value="basic-lti-launch-request" selected="selected">Launch</option></select></span> '.
+                '<br /><br />'.
+                '<span class="LC_nobreak">'.$lt{'url'}.':<input type="text" size="30" name="ltitools_url_'.$i.'"'.
+                ' value="'.$url.'" /></span>'.
+                (' 'x2).
+                '<span class="LC_nobreak">'.$lt{'key'}.
+                '<input type="text" size="25" name="ltitools_key_'.$i.'" value="'.$key.'" /></span> '.
+                (' 'x2).
+                '<span class="LC_nobreak">'.$lt{'secret'}.':'.
+                '<input type="password" size="20" name="ltitools_secret_'.$i.'" value="'.$secret.'" />'.
+                '<label><input type="checkbox" name="visible" onclick="if (this.checked) { this.form.ltitools_secret_'.$i.'.type='."'text'".' } else { this.form.ltitools_secret_'.$i.'.type='."'password'".' }" />'.&mt('Visible input').'</label>'.
+                '<input type="hidden" name="ltitools_id_'.$i.'" value="'.$item.'" /></span>'.
+                '</fieldset>'.
+                '<fieldset><legend>'.&mt('Optional settings').'</legend>'.
+                '<span class="LC_nobreak">'.&mt('Display target:');
+            my %currdisp;
+            if (ref($settings->{$item}->{'display'}) eq 'HASH') {
+                if ($settings->{$item}->{'display'}->{'target'} eq 'window') {
+                    $currdisp{'window'} = ' checked="checked"';
+                } else {
+                    $currdisp{'iframe'} = ' checked="checked"';
+                }
+                if ($settings->{$item}->{'display'}->{'width'} =~ /^(\d+)$/) {
+                    $currdisp{'width'} = $1;
+                }
+                if ($settings->{$item}->{'display'}->{'height'} =~ /^(\d+)$/) {
+                     $currdisp{'height'} = $1;
+                }
+            } else {
+                $currdisp{'iframe'} = ' checked="checked"';
+            }
+            foreach my $disp ('iframe','window') {
+                $datatable .= '<label><input type="radio" name="ltitools_target_'.$i.'" value="'.$disp.'"'.$currdisp{$disp}.' />'.
+                              $lt{$disp}.'</label>'.(' 'x2);
+            }
+            $datatable .= (' 'x4);
+            foreach my $dimen ('width','height') {
+                $datatable .= '<label>'.$lt{$dimen}.' '.
+                              '<input type="text" name="ltitools_'.$dimen.'_'.$i.'" size="5" value="'.$currdisp{$dimen}.'" /></label>'.
+                              (' 'x2);
+            }
+            $datatable .= '<br />';
+            foreach my $extra ('passback','roster') {
+                my $checkedon = '';
+                my $checkedoff = ' checked="checked"';
+                if ($settings->{$item}->{$extra}) {
+                    $checkedon = $checkedoff;
+                    $checkedoff = '';
+                }
+                $datatable .= $lt{$extra}.' '.
+                              '<label><input type="radio" name="ltitools_'.$extra.'_'.$i.'" value="1"'.$checkedon.' />'.
+                              &mt('Yes').'</label>'.(' 'x2).
+                              '<label><input type="radio" name="ltitools_'.$extra.'_'.$i.'" value="0"'.$checkedoff.' />'.
+                              &mt('No').'</label>'.(' 'x4);
+            }
+            $datatable .= '<br /><br /><span class="LC_nobreak">'.$lt{'icon'}.': ';
+            if ($imgsrc) {
+                $datatable .= $imgsrc.
+                              '<label><input type="checkbox" name="ltitools_image_del"'.
+                              ' value="'.$item.'" />'.&mt('Delete?').'</label></span> '.
+                              '<span class="LC_nobreak"> '.&mt('Replace:').' ';
+            } else {
+                $datatable .= '('.&mt('if larger than 21x21 pixels, image will be scaled').') ';
+            }
+            if ($switchserver) {
+                $datatable .= &mt('Upload to library server: [_1]',$switchserver);
+            } else {
+                $datatable .= '<input type="file" name="ltitools_image_'.$i.'" value="" />';
+            }
+            $datatable .= '</span></fieldset>';
+            my (%checkedfields,%rolemaps);
+            if (ref($settings->{$item}) eq 'HASH') {
+                if (ref($settings->{$item}->{'fields'}) eq 'HASH') {
+                    %checkedfields = %{$settings->{$item}->{'fields'}};
+                }
+                if (ref($settings->{$item}->{'roles'}) eq 'HASH') {
+                    %rolemaps = %{$settings->{$item}->{'roles'}};
+                    $checkedfields{'roles'} = 1;
+                }
+            }
+            $datatable .= '<fieldset><legend>'.&mt('User data sent on launch').'</legend>'.
+                          '<span class="LC_nobreak">';
+            foreach my $field (@fields) {
+                my $checked;
+                if ($checkedfields{$field}) {
+                    $checked = ' checked="checked"';
+                }
+                $datatable .= '<label>'.
+                              '<input type="checkbox" name="ltitools_fields_'.$i.'" value="'.$field.'"'.$checked.' />'.
+                              $lt{$field}.'</label>'.(' ' x2);
+            }
+            $datatable .= '</span></fieldset>'.
+                          '<fieldset><legend>'.&mt('Role mapping').'</legend><table><tr>';
+            foreach my $role (@courseroles) {
+                my ($selected,$selectnone);
+                if (!$rolemaps{$role}) {
+                    $selectnone = ' selected="selected"';
+                }
+                $datatable .= '<td align="center">'. 
+                              &Apache::lonnet::plaintext($role,'Course').'<br />'.
+                              '<select name="ltitools_roles_'.$role.'_'.$i.'">'.
+                              '<option value=""'.$selectnone.'>'.&mt('Select').'</option>';
+                foreach my $ltirole (@ltiroles) {
+                    unless ($selectnone) {
+                        if ($rolemaps{$role} eq $ltirole) {
+                            $selected = ' selected="selected"';
+                        } else {
+                            $selected = '';
+                        }
+                    }
+                    $datatable .= '<option value="'.$ltirole.'"'.$selected.'>'.$ltirole.'</option>';
+                }
+                $datatable .= '</select></td>';
+            }
+            $datatable .= '</tr></table></fieldset>'.
+                          '<fieldset><legend>'.&mt('Custom items sent on launch').'</legend>'.
+                          '<table><tr><th>'.&mt('Action').'</th><th>'.&mt('Name').'</th><th>'.&mt('Value').'</th></tr>';
+            if (ref($settings->{$item}->{'custom'}) eq 'HASH') {
+                my %custom = %{$settings->{$item}->{'custom'}};
+                if (keys(%custom) > 0) {
+                    foreach my $key (sort(keys(%custom))) {
+                        $datatable .= '<tr><td><span class="LC_nobreak">'.
+                                      '<label><input type="checkbox" name="ltitools_customdel_'.$i.'" value="'.
+                                      $key.'" />'.&mt('Delete').'</label></span></td><td>'.$key.'</td>'.
+                                      '<td><input type="text" name="ltitools_customval_'.$key.'_'.$i.'"'.
+                                      ' value="'.$custom{$key}.'" /></td></tr>';
+                    }
+                }
+            }
+            $datatable .= '<tr><td><span class="LC_nobreak">'.
+                          '<label><input type="checkbox" name="ltitools_customadd" value="'.$i.'" />'.
+                          &mt('Add').'</label></span></td><td><input type="text" name="ltitools_custom_name_'.$i.'" />'.
+                          '</td><td><input type="text" name="ltitools_custom_value_'.$i.'" /></td></tr>';
+            $datatable .= '</table></fieldset></td></tr>'."\n";
+            $itemcount ++;
+        }
+    }
+    $css_class = $itemcount%2?' class="LC_odd_row"':'';
+    my $chgstr = ' onchange="javascript:reorderLTI(this.form,'."'ltitools_add_pos'".');"';
+    $datatable .= '<tr '.$css_class.'><td><span class="LC_nobreak">'."\n".
+                  '<input type="hidden" name="ltitools_maxnum" value="'.$maxnum.'" />'."\n".
+                  '<select name="ltitools_add_pos"'.$chgstr.'>';
+    for (my $k=0; $k<$maxnum+1; $k++) {
+        my $vpos = $k+1;
+        my $selstr;
+        if ($k == $maxnum) {
+            $selstr = ' selected="selected" ';
+        }
+        $datatable .= '<option value="'.$k.'"'.$selstr.'>'.$vpos.'</option>';
+    }
+    $datatable .= '</select> '."\n".
+                  '<input type="checkbox" name="ltitools_add" value="1" />'.&mt('Add').'</td>'."\n".
+                  '<td colspan="2">'.
+                  '<fieldset><legend>'.&mt('Required settings').'</legend>'.
+                  '<span class="LC_nobreak">'.$lt{'title'}.':<input type="text" size="30" name="ltitools_add_title" value="" /></span> '."\n".
+                  (' 'x2).
+                  '<span class="LC_nobreak">'.$lt{'version'}.':<select name="ltitools_add_version">'.
+                  '<option value="LTI-1p0" selected="selected">1.1</option></select></span> '."\n".
+                  (' 'x2).
+                  '<span class="LC_nobreak">'.$lt{'msgtype'}.':<select name="ltitools_add_msgtype">'.
+                  '<option value="basic-lti-launch-request" selected="selected">Launch</option></select></span> '.
+                  '<br />'.
+                  '<span class="LC_nobreak">'.$lt{'url'}.':<input type="text" size="30" name="ltitools_add_url" value="" /></span> '."\n".
+                  (' 'x2).
+                  '<span class="LC_nobreak">'.$lt{'key'}.':<input type="text" size="25" name="ltitools_add_key" value="" /></span> '."\n".
+                  (' 'x2).
+                  '<span class="LC_nobreak">'.$lt{'secret'}.':<input type="password" size="20" name="ltitools_add_secret" value="" />'.
+                  '<label><input type="checkbox" name="visible" onclick="if (this.checked) { this.form.ltitools_add_secret.type='."'text'".' } else { this.form.ltitools_add_secret.type='."'password'".' }" />'.&mt('Visible input').'</label></span> '."\n".
+                  '</fieldset>'.
+                  '<fieldset><legend>'.&mt('Optional settings').'</legend>'.
+                  '<span class="LC_nobreak">'.&mt('Display target:');
+    my %defaultdisp;
+    $defaultdisp{'iframe'} = ' checked="checked"';
+    foreach my $disp ('iframe','window') {
+        $datatable .= '<label><input type="radio" name="ltitools_add_target" value="'.$disp.'"'.$defaultdisp{$disp}.' />'.
+                      $lt{$disp}.'</label>'.(' 'x2);
+    }
+    $datatable .= (' 'x4);
+    foreach my $dimen ('width','height') {
+        $datatable .= '<label>'.$lt{$dimen}.' '.
+                      '<input type="text" name="ltitools_add_'.$dimen.'" size="5" /></label>'.
+                      (' 'x2);
+    }
+    $datatable .= '<br />';
+    foreach my $extra ('passback','roster') {
+        $datatable .= $lt{$extra}.' '.
+                      '<label><input type="radio" name="ltitools_add_'.$extra.'" value="1" />'.
+                      &mt('Yes').'</label>'.(' 'x2).
+                      '<label><input type="radio" name="ltitools_add_'.$extra.'" value="0" checked="checked" />'.
+                      &mt('No').'</label>'.(' 'x4);
+    }
+    $datatable .= '<br /><br /><span class="LC_nobreak">'.$lt{'icon'}.': '.
+                  '('.&mt('if larger than 21x21 pixels, image will be scaled').') ';
+    if ($switchserver) {
+        $datatable .= &mt('Upload to library server: [_1]',$switchserver);
+    } else {
+        $datatable .= '<input type="file" name="ltitools_add_image" value="" />';
+    }
+    $datatable .= '</span></fieldset>'.
+                  '<fieldset><legend>'.&mt('User data sent on launch').'</legend>'.
+                  '<span class="LC_nobreak">';
+    foreach my $field (@fields) {
+        $datatable .= '<label>'.
+                      '<input type="checkbox" name="ltitools_add_fields" value="'.$field.'" />'.
+                      $lt{$field}.'</label>'.(' ' x2);
+    }
+    $datatable .= '</span></fieldset>'.
+                  '<fieldset><legend>'.&mt('Role mapping').'</legend><table><tr>';
+    foreach my $role (@courseroles) {
+        my ($checked,$checkednone);
+        $datatable .= '<td align="center">'.
+                      &Apache::lonnet::plaintext($role,'Course').'<br />'.
+                      '<select name="ltitools_add_roles_'.$role.'">'.
+                      '<option value="" selected="selected">'.&mt('Select').'</option>';
+        foreach my $ltirole (@ltiroles) {
+            $datatable .= '<option value="'.$ltirole.'">'.$ltirole.'</option>';
+        }
+        $datatable .= '</select></td>';
+    }
+    $datatable .= '</tr></table></fieldset>'.
+                  '<fieldset><legend>'.&mt('Custom items sent on launch').'</legend>'.
+                  '<table><tr><th>'.&mt('Action').'</th><th>'.&mt('Name').'</th><th>'.&mt('Value').'</th></tr>'.
+                  '<tr><td><span class="LC_nobreak">'.
+                  '<label><input type="checkbox" name="ltitools_add_custom" value="1" />'.
+                  &mt('Add').'</label></span></td><td><input type="text" name="ltitools_add_custom_name" />'.
+                  '</td><td><input type="text" name="ltitools_add_custom_value" /></td></tr>'.
+                  '</table></fieldset></td></tr>'."\n".
+                  '</td>'."\n".
+                  '</tr>'."\n";
+    $itemcount ++;
+    return $datatable;
+}
+
+sub ltitools_names {
+    my %lt = &Apache::lonlocal::texthash(
+                                          'title'     => 'Title',
+                                          'version'   => 'Version',
+                                          'msgtype'   => 'Message Type',
+                                          'url'       => 'URL',
+                                          'key'       => 'Key',
+                                          'secret'    => 'Secret',
+                                          'icon'      => 'Icon',   
+                                          'user'      => 'Username:domain',
+                                          'fullname'  => 'Full Name',
+                                          'firstname' => 'First Name',
+                                          'lastname'  => 'Last Name',
+                                          'email'     => 'E-mail',
+                                          'roles'     => 'Role',
+                                          'window'    => 'Window/Tab',
+                                          'iframe'    => 'iFrame',
+                                          'height'    => 'Height',
+                                          'width'     => 'Width',
+                                          'passback'  => 'Tool can return grades:',
+                                          'roster'    => 'Tool can retrieve roster:',
+                                        );
+    return %lt;
+}
+
 sub print_coursedefaults {
     my ($position,$dom,$settings,$rowtotal) = @_;
     my ($css_class,$datatable,%checkedon,%checkedoff,%defaultchecked, at toggles);
@@ -6883,7 +7283,7 @@
 
 sub publishlogo {
     my ($r,$action,$formname,$dom,$confname,$subdir,$thumbwidth,$thumbheight,$savefileas) = @_;
-    my ($output,$fname,$logourl);
+    my ($output,$fname,$logourl,$madethumb);
     if ($action eq 'upload') {
         $fname=$env{'form.'.$formname.'.filename'};
         chop($env{'form.'.$formname});
@@ -7011,6 +7411,7 @@
                                     $r->set_handlers('PerlCleanupHandler' => [\&notifysubscribed,@{$handlers}]);
                                     $registered_cleanup=1;
                                 }
+                                $madethumb = 1;
                             } else {
                                 print $logfile "\nUnable to write ".$copyfile.
                                                ':'.$!."\n";
@@ -7023,7 +7424,7 @@
             $output = $versionresult;
         }
     }
-    return ($output,$logourl);
+    return ($output,$logourl,$madethumb);
 }
 
 sub logo_versioning {
@@ -7892,6 +8293,495 @@
     return ($url,$error);
 }
 
+sub modify_ltitools {
+    my ($r,$dom,$action,$lastactref,%domconfig) = @_;
+    my %domdefaults = &Apache::lonnet::get_domain_defaults($dom,1);
+    my ($newid, at allpos,%changes,%confhash,$errors,$resulttext);
+    my $confname = $dom.'-domainconfig';
+    my $servadm = $r->dir_config('lonAdmEMail');
+    my ($configuserok,$author_ok,$switchserver) = &config_check($dom,$confname,$servadm);
+    my (%posslti,%possfield);
+    my @courseroles = ('cc','in','ta','ep','st');
+    my @ltiroles = qw(Instructor ContentDeveloper TeachingAssistant Learner);
+    map { $posslti{$_} = 1; } @ltiroles;
+    my @allfields = ('fullname','firstname','lastname','email','user','roles');
+    map { $possfield{$_} = 1; } @allfields;
+    my %lt = &ltitools_names(); 
+    if ($env{'form.ltitools_add'}) {
+        my $title = $env{'form.ltitools_add_title'};
+        $title =~ s/(`)/'/g;
+        ($newid,my $error) = &get_ltitools_id($dom,$title);
+        if ($newid) {
+            my $position = $env{'form.ltitools_add_pos'};
+            $position =~ s/\D+//g;
+            if ($position ne '') {
+                $allpos[$position] = $newid;
+            }
+            $changes{$newid} = 1;
+            foreach my $item ('title','url','key','secret') {
+                $env{'form.ltitools_add_'.$item} =~ s/(`)/'/g;
+                if ($env{'form.ltitools_add_'.$item}) {
+                    $confhash{$newid}{$item} = $env{'form.ltitools_add_'.$item};
+                }
+            }
+            if ($env{'form.ltitools_add_version'} eq 'LTI-1p0') {
+                $confhash{$newid}{'version'} = $env{'form.ltitools_add_version'};
+            }
+            if ($env{'form.ltitools_add_msgtype'} eq 'basic-lti-launch-request') {
+                $confhash{$newid}{'msgtype'} = $env{'form.ltitools_add_msgtype'};
+            }
+            foreach my $item ('width','height') {
+                $env{'form.ltitools_add_'.$item} =~ s/^\s+//;
+                $env{'form.ltitools_add_'.$item} =~ s/\s+$//;
+                if ($env{'form.ltitools_add_'.$item} =~ /^\d+$/) {
+                    $confhash{$newid}{'display'}{$item} = $env{'form.ltitools_add_'.$item};
+                }
+            }
+            if ($env{'form.ltitools_add_target'} eq 'window') {
+                $confhash{$newid}{'display'}{'target'} = $env{'form.ltitools_add_target'};
+            } else {
+                $confhash{$newid}{'display'}{'target'} = 'iframe';
+            }
+            foreach my $item ('passback','roster') {
+                if ($env{'form.ltitools_add_'.$item}) {
+                    $confhash{$newid}{$item} = 1;
+                }
+            }
+            if ($env{'form.ltitools_add_image.filename'} ne '') {
+                my ($imageurl,$error) =
+                    &process_ltitools_image($r,$dom,$confname,'ltitools_add_image',$dom,
+                                            $configuserok,$switchserver,$author_ok);
+                if ($imageurl) {
+                    $confhash{$newid}{'image'} = $imageurl;
+                }
+                if ($error) {
+                    &Apache::lonnet::logthis($error);
+                    $errors .= '<li><span class="LC_error">'.$error.'</span></li>';
+                }
+            }
+            my @fields = &Apache::loncommon::get_env_multiple('form.ltitools_add_fields');
+            foreach my $field (@fields) {
+                if ($possfield{$field}) {
+                    if ($field eq 'roles') {
+                        foreach my $role (@courseroles) {
+                            my $choice = $env{'form.ltitools_add_roles_'.$role};
+                            if (($choice ne '') && ($posslti{$choice})) {
+                                $confhash{$newid}{'roles'}{$role} = $choice;
+                                if ($role eq 'cc') {
+                                    $confhash{$newid}{'roles'}{'co'} = $choice; 
+                                }
+                            }
+                        }
+                    } else {
+                        $confhash{$newid}{'fields'}{$field} = 1;
+                    }
+                }
+            }
+            if ($env{'form.ltitools_add_custom'}) {
+                my $name = $env{'form.ltitools_add_custom_name'};
+                my $value = $env{'form.ltitools_add_custom_value'};
+                $value =~ s/(`)/'/g;
+                $name =~ s/(`)/'/g;
+                $confhash{$newid}{'custom'}{$name} = $value;
+            }
+        } else {
+            my $error = &mt('Failed to acquire unique ID for new external tool');   
+            $errors .= '<li><span class="LC_error">'.$error.'</span></li>';
+        }
+    }
+    if (ref($domconfig{$action}) eq 'HASH') {
+        my %deletions;
+        my @todelete = &Apache::loncommon::get_env_multiple('form.ltitools_del');
+        if (@todelete) {
+            map { $deletions{$_} = 1; } @todelete;
+        }
+        my %customadds;
+        my @newcustom = &Apache::loncommon::get_env_multiple('form.ltitools_customadd');
+        if (@newcustom) {
+            map { $customadds{$_} = 1; } @newcustom;
+        } 
+        my %imgdeletions;
+        my @todeleteimages = &Apache::loncommon::get_env_multiple('form.ltitools_image_del');
+        if (@todeleteimages) {
+            map { $imgdeletions{$_} = 1; } @todeleteimages;
+        }
+        my $maxnum = $env{'form.ltitools_maxnum'};
+        for (my $i=0; $i<=$maxnum; $i++) {
+            my $itemid = $env{'form.ltitools_id_'.$i};
+            if (ref($domconfig{$action}{$itemid}) eq 'HASH') {
+                if ($deletions{$itemid}) {
+                    if ($domconfig{$action}{$itemid}{'image'}) {
+                        #FIXME need to obsolete item in RES space
+                    }
+                    $changes{$itemid} = $domconfig{$action}{$itemid}{'title'};
+                    next;
+                } else {
+                    my $newpos = $env{'form.ltitools_'.$itemid};
+                    $newpos =~ s/\D+//g;
+                    foreach my $item ('title','url','key','secret') {
+                        $confhash{$itemid}{$item} = $env{'form.ltitools_'.$item.'_'.$i};
+                        if ($domconfig{$action}{$itemid}{$item} ne $confhash{$itemid}{$item}) {
+                            $changes{$itemid} = 1;
+                        }
+                    }
+                    if ($env{'form.ltitools_version_'.$i} eq 'LTI-1p0') {
+                        $confhash{$itemid}{'version'} = $env{'form.ltitools_version_'.$i};
+                    }
+                    if ($env{'form.ltitools_msgtype_'.$i} eq 'basic-lti-launch-request') {
+                        $confhash{$itemid}{'msgtype'} = $env{'form.ltitools_msgtype_'.$i};
+                    }
+                    foreach my $size ('width','height') {
+                        $env{'form.ltitools_'.$size.'_'.$i} =~ s/^\s+//;
+                        $env{'form.ltitools_'.$size.'_'.$i} =~ s/\s+$//;
+                        if ($env{'form.ltitools_'.$size.'_'.$i} =~ /^\d+$/) {
+                            $confhash{$itemid}{'display'}{$size} = $env{'form.ltitools_'.$size.'_'.$i};
+                            if (ref($domconfig{$action}{$itemid}{'display'}) eq 'HASH') {
+                                if ($domconfig{$action}{$itemid}{'display'}{$size} ne $confhash{$itemid}{'display'}{$size}) {
+                                    $changes{$itemid} = 1;
+                                }
+                            } else {
+                                $changes{$itemid} = 1;
+                            }
+                        }
+                    }
+                    if ($env{'form.ltitools_target_'.$i} eq 'window') {
+                        $confhash{$itemid}{'display'}{'target'} = $env{'form.ltitools_target_'.$i};
+                    } else {
+                        $confhash{$itemid}{'display'}{'target'} = 'iframe';
+                    }
+                    if (ref($domconfig{$action}{$itemid}{'display'}) eq 'HASH') {
+                        if ($domconfig{$action}{$itemid}{'display'}{'target'} ne $confhash{$itemid}{'display'}{'target'}) {
+                            $changes{$itemid} = 1;
+                        }
+                    } else {
+                        $changes{$itemid} = 1;
+                    }
+                    foreach my $extra ('passback','roster') {
+                        if ($env{'form.ltitools_'.$extra.'_'.$i}) {
+                            $confhash{$itemid}{$extra} = 1;
+                        }
+                        if ($domconfig{$action}{$itemid}{$extra} ne $confhash{$itemid}{$extra}) {
+                            $changes{$itemid} = 1;
+                        }
+                    }
+                    my @fields = &Apache::loncommon::get_env_multiple('form.ltitools_fields_'.$i);
+                    foreach my $field (@fields) {
+                        if ($possfield{$field}) {
+                            if ($field eq 'roles') {
+                                foreach my $role (@courseroles) {
+                                    my $choice = $env{'form.ltitools_roles_'.$role.'_'.$i};
+                                    if (($choice ne '') && ($posslti{$choice})) {
+                                        $confhash{$itemid}{'roles'}{$role} = $choice;
+                                        if ($role eq 'cc') {
+                                            $confhash{$itemid}{'roles'}{'co'} = $choice;
+                                        }
+                                    }
+                                    if (ref($domconfig{$action}{$itemid}{'roles'}) eq 'HASH') {
+                                        if ($domconfig{$action}{$itemid}{'roles'}{$role} ne $confhash{$itemid}{'roles'}{$role}) {
+                                            $changes{$itemid} = 1;
+                                        }
+                                    } elsif ($confhash{$itemid}{'roles'}{$role}) {
+                                        $changes{$itemid} = 1;
+                                    }
+                                }
+                            } else {
+                                $confhash{$itemid}{'fields'}{$field} = 1;
+                                if (ref($domconfig{$action}{$itemid}{'fields'}) eq 'HASH') {
+                                    if ($domconfig{$action}{$itemid}{'fields'}{$field} ne $confhash{$itemid}{'fields'}{$field}) {
+                                        $changes{$itemid} = 1;
+                                    }
+                                } else {
+                                    $changes{$itemid} = 1;
+                                }
+                            }
+                        }
+                    }
+                    $allpos[$newpos] = $itemid;
+                }
+                if ($imgdeletions{$itemid}) {
+                    $changes{$itemid} = 1;
+                    #FIXME need to obsolete item in RES space
+                } elsif ($env{'form.ltitools_image_'.$i.'.filename'}) {
+                    my ($imgurl,$error) = &process_ltitools_image($r,$dom,$confname,'ltitools_image_'.$i,
+                                                                 $itemid,$configuserok,$switchserver,
+                                                                 $author_ok);
+                    if ($imgurl) {
+                        $confhash{$itemid}{'image'} = $imgurl;
+                        $changes{$itemid} = 1;
+                    }
+                    if ($error) {
+                        &Apache::lonnet::logthis($error);
+                        $errors .= '<li><span class="LC_error">'.$error.'</span></li>';
+                    }
+                } elsif ($domconfig{$action}{$itemid}{'image'}) {
+                    $confhash{$itemid}{'image'} =
+                       $domconfig{$action}{$itemid}{'image'};
+                }
+                if ($customadds{$i}) {
+                    my $name = $env{'form.ltitools_custom_name_'.$i};
+                    $name =~ s/(`)/'/g;
+                    $name =~ s/^\s+//;
+                    $name =~ s/\s+$//;
+                    my $value = $env{'form.ltitools_custom_value_'.$i};
+                    $value =~ s/(`)/'/g;
+                    $value =~ s/^\s+//;
+                    $value =~ s/\s+$//;
+                    if ($name ne '') {
+                        $confhash{$itemid}{'custom'}{$name} = $value;
+                        $changes{$itemid} = 1;
+                    }
+                }
+                my %customdels;
+                my @customdeletions = &Apache::loncommon::get_env_multiple('form.ltitools_customdel_'.$i); 
+                if (@customdeletions) {
+                    $changes{$itemid} = 1;
+                }
+                map { $customdels{$_} = 1; } @customdeletions;
+                if (ref($domconfig{$action}{$itemid}{'custom'}) eq 'HASH') {
+                    foreach my $key (keys(%{$domconfig{$action}{$itemid}{'custom'}})) {
+                        unless ($customdels{$key}) {
+                            if ($env{'form.ltitools_customval_'.$key.'_'.$i} ne '') {
+                                $confhash{$itemid}{'custom'}{$key} = $env{'form.ltitools_customval_'.$key.'_'.$i}; 
+                            }
+                            if ($domconfig{$action}{$itemid}{'custom'}{$key} ne $env{'form.ltitools_customval_'.$key.'_'.$i}) {
+                                $changes{$itemid} = 1;
+                            }
+                        }
+                    }
+                }
+                unless ($changes{$itemid}) {
+                    foreach my $key (keys(%{$domconfig{$action}{$itemid}})) {
+                        if (ref($domconfig{$action}{$itemid}{$key}) eq 'HASH') {
+                            if (ref($confhash{$itemid}{$key}) eq 'HASH') {
+                                foreach my $innerkey (keys(%{$domconfig{$action}{$itemid}{$key}})) {
+                                    unless (exists($confhash{$itemid}{$key}{$innerkey})) {
+                                        $changes{$itemid} = 1;
+                                        last;
+                                    }
+                                }
+                            } elsif (keys(%{$domconfig{$action}{$itemid}{$key}}) > 0) {
+                                $changes{$itemid} = 1;
+                            }
+                        }
+                        last if ($changes{$itemid});
+                    }
+                }
+            }
+        }
+    }
+    if (@allpos > 0) {
+        my $idx = 0;
+        foreach my $itemid (@allpos) {
+            if ($itemid ne '') {
+                $confhash{$itemid}{'order'} = $idx;
+                if (ref($domconfig{$action}) eq 'HASH') {
+                    if (ref($domconfig{$action}{$itemid}) eq 'HASH') {
+                        if ($domconfig{$action}{$itemid}{'order'} ne $idx) {
+                            $changes{$itemid} = 1;
+                        }
+                    }
+                }
+                $idx ++;
+            }
+        }
+    }
+    my %ltitoolshash = (
+                          $action => { %confhash }
+                       );
+    my $putresult = &Apache::lonnet::put_dom('configuration',\%ltitoolshash,
+                                             $dom);
+    if ($putresult eq 'ok') {
+        if (keys(%changes) > 0) {
+            my $cachetime = 24*60*60;
+            &Apache::lonnet::do_cache_new('ltitools',$dom,\%confhash,$cachetime);
+            if (ref($lastactref) eq 'HASH') {
+                $lastactref->{'ltitools'} = 1;
+            }
+            $resulttext = &mt('Changes made:').'<ul>';
+            my %bynum;
+            foreach my $itemid (sort(keys(%changes))) {
+                my $position = $confhash{$itemid}{'order'};
+                $bynum{$position} = $itemid;
+            }
+            foreach my $pos (sort { $a <=> $b } keys(%bynum)) {
+                my $itemid = $bynum{$pos}; 
+                if (ref($confhash{$itemid}) ne 'HASH') {
+                    $resulttext .= '<li>'.&mt('Deleted: [_1]',$changes{$itemid}).'</li>';
+                } else {
+                    $resulttext .= '<li><b>'.$confhash{$itemid}{'title'}.'</b>';
+                    if ($confhash{$itemid}{'image'}) {
+                        $resulttext .= ' '.
+                                       '<img src="'.$confhash{$itemid}{'image'}.'"'.
+                                       ' alt="'.&mt('Tool Provider icon').'" />';
+                    }
+                    $resulttext .= '</li><ul>';
+                    my $position = $pos + 1;
+                    $resulttext .= '<li>'.&mt('Order: [_1]',$position).'</li>';
+                    foreach my $item ('version','msgtype','url','key') {
+                        if ($confhash{$itemid}{$item} ne '') {
+                            $resulttext .= '<li>'.$lt{$item}.': '.$confhash{$itemid}{$item}.'</li>';
+                        }
+                    }
+                    if ($confhash{$itemid}{'secret'} ne '') {
+                        $resulttext .= '<li>'.$lt{'secret'}.': ';
+                        my $num = length($confhash{$itemid}{'secret'});
+                        $resulttext .= ('*'x$num).'</li>';
+                    }
+                    foreach my $item ('passback','roster') {
+                        $resulttext .= '<li>'.$lt{$item}.' ';
+                        if ($confhash{$itemid}{$item}) {
+                            $resulttext .= &mt('Yes');
+                        } else {
+                            $resulttext .= &mt('No');
+                        }
+                        $resulttext .= '</li>';
+                    }
+                    if (ref($confhash{$itemid}{'display'}) eq 'HASH') {
+                        my $displaylist;
+                        if ($confhash{$itemid}{'display'}{'target'}) {
+                            $displaylist = &mt('Display target').': '.
+                                           $confhash{$itemid}{'display'}{'target'}.',';
+                        }
+                        foreach my $size ('width','height') { 
+                            if ($confhash{$itemid}{'display'}{$size}) {
+                                $displaylist .= (' 'x2).$lt{$size}.': '.
+                                                $confhash{$itemid}{'display'}{$size}.',';
+                            }
+                        }
+                        if ($displaylist) {
+                            $displaylist =~ s/,$//;
+                            $resulttext .= '<li>'.$displaylist.'</li>';
+                        }
+                    } 
+                    if (ref($confhash{$itemid}{'fields'}) eq 'HASH') {
+                        my $fieldlist;
+                        foreach my $field (@allfields) {
+                            if ($confhash{$itemid}{'fields'}{$field}) {
+                                $fieldlist .= (' 'x2).$lt{$field}.',';
+                            }
+                        }
+                        if ($fieldlist) {
+                            $fieldlist =~ s/,$//;
+                            $resulttext .= '<li>'.&mt('Data sent').':'.$fieldlist.'</li>';
+                        }
+                    }
+                    if (ref($confhash{$itemid}{'roles'}) eq 'HASH') {
+                        my $rolemaps;
+                        foreach my $role (@courseroles) {
+                            if ($confhash{$itemid}{'roles'}{$role}) {
+                                $rolemaps .= (' 'x2).&Apache::lonnet::plaintext($role,'Course').'='.
+                                             $confhash{$itemid}{'roles'}{$role}.',';
+                            }
+                        }
+                        if ($rolemaps) {
+                            $rolemaps =~ s/,$//; 
+                            $resulttext .= '<li>'.&mt('Role mapping:').$rolemaps.'</li>';
+                        }
+                    }
+                    if (ref($confhash{$itemid}{'custom'}) eq 'HASH') {
+                        my $customlist;
+                        if (keys(%{$confhash{$itemid}{'custom'}})) {
+                            foreach my $key (sort(keys(%{$confhash{$itemid}{'custom'}}))) {
+                                $customlist .= $key.':'.$confhash{$itemid}{'custom'}{$key}.(' 'x2);
+                            } 
+                        }
+                        if ($customlist) {
+                            $resulttext .= '<li>'.&mt('Custom items').':'.$customlist.'</li>';
+                        }
+                    } 
+                    $resulttext .= '</ul></li>';
+                }
+            }
+            $resulttext .= '</ul>';
+        } else {
+            $resulttext = &mt('No changes made.');
+        }
+    } else {
+        $errors .= '<li><span class="LC_error">'.&mt('Failed to save changes').'</span></li>';
+    }
+    if ($errors) {
+        $resulttext .= &mt('The following errors occurred: ').'<ul>'.
+                       $errors.'</ul>';
+    }
+    return $resulttext;
+}
+
+sub process_ltitools_image {
+    my ($r,$dom,$confname,$caller,$itemid,$configuserok,$switchserver,$author_ok) = @_;
+    my $filename = $env{'form.'.$caller.'.filename'};
+    my ($error,$url);
+    my ($width,$height) = (21,21);
+    if ($configuserok eq 'ok') {
+        if ($switchserver) {
+            $error = &mt('Upload of Tool Provider (LTI) icon is not permitted to this server: [_1]',
+                         $switchserver);
+        } elsif ($author_ok eq 'ok') {
+            my ($result,$imageurl,$madethumb) =
+                &publishlogo($r,'upload',$caller,$dom,$confname,
+                             "ltitools/$itemid/icon",$width,$height);
+            if ($result eq 'ok') {
+                if ($madethumb) {
+                    my ($path,$imagefile) = ($imageurl =~ m{^(.+)/([^/]+)$});
+                    my $imagethumb = "$path/tn-".$imagefile;
+                    $url = $imagethumb;
+                } else {
+                    $url = $imageurl;
+                }
+            } else {
+                $error = &mt("Upload of [_1] failed because an error occurred publishing the file in RES space. Error was: [_2].",$filename,$result);
+            }
+        } else {
+            $error = &mt("Upload of [_1] failed because an author role could not be assigned to a Domain Configuration user ([_2]) in domain: [_3].  Error was: [_4].",$filename,$confname,$dom,$author_ok);
+        }
+    } else {
+        $error = &mt("Upload of [_1] failed because a Domain Configuration user ([_2]) could not be created in domain: [_3].  Error was: [_4].",$filename,$confname,$dom,$configuserok);
+    }
+    return ($url,$error);
+}
+
+sub get_ltitools_id {
+    my ($cdom,$title) = @_;
+    # get lock on ltitools db
+    my $lockhash = {
+                      lock => $env{'user.name'}.
+                              ':'.$env{'user.domain'},
+                   };
+    my $tries = 0;
+    my $gotlock = &Apache::lonnet::newput_dom('ltitools',$lockhash,$cdom);
+    my ($id,$error);
+ 
+    while (($gotlock ne 'ok') && ($tries<10)) {
+        $tries ++;
+        sleep (0.1);
+        $gotlock = &Apache::lonnet::newput_dom('ltitools',$lockhash,$cdom);
+    }
+    if ($gotlock eq 'ok') {
+        my %currids = &Apache::lonnet::dump_dom('ltitools',$cdom);
+        if ($currids{'lock'}) {
+            delete($currids{'lock'});
+            if (keys(%currids)) {
+                my @curr = sort { $a <=> $b } keys(%currids);
+                if ($curr[-1] =~ /^\d+$/) {
+                    $id = 1 + $curr[-1];
+                }
+            } else {
+                $id = 1;
+            }
+            if ($id) {
+                unless (&Apache::lonnet::newput_dom('ltitools',{ $id => $title },$cdom) eq 'ok') {
+                    $error = 'nostore';
+                }
+            } else {
+                $error = 'nonumber';
+            }
+        }
+        my $dellockoutcome = &Apache::lonnet::del_dom('ltitools',['lock'],$cdom);
+    } else {
+        $error = 'nolock';
+    }
+    return ($id,$error);
+}
+
 sub modify_autoenroll {
     my ($dom,$lastactref,%domconfig) = @_;
     my ($resulttext,%changes);
@@ -12241,7 +13131,7 @@
     my %servers = &Apache::lonnet::internet_dom_servers($dom);
     my %thismachine;
     map { $thismachine{$_} = 1; } &Apache::lonnet::current_machine_ids();
-    my @posscached = ('domainconfig','domdefaults');
+    my @posscached = ('domainconfig','domdefaults','ltitools');
     if (keys(%servers)) {
         foreach my $server (keys(%servers)) {
             next if ($thismachine{$server});
Index: loncom/interface/londocs.pm
diff -u loncom/interface/londocs.pm:1.597 loncom/interface/londocs.pm:1.598
--- loncom/interface/londocs.pm:1.597	Sat Sep 12 15:47:57 2015
+++ loncom/interface/londocs.pm	Tue Jan 26 14:30:25 2016
@@ -1,7 +1,7 @@
 # The LearningOnline Network
 # Documents
 #
-# $Id: londocs.pm,v 1.597 2015/09/12 15:47:57 raeburn Exp $
+# $Id: londocs.pm,v 1.598 2016/01/26 14:30:25 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -586,7 +586,7 @@
 }
 
 sub group_import {
-    my ($coursenum, $coursedom, $folder, $container, $caller, @files) = @_;
+    my ($coursenum, $coursedom, $folder, $container, $caller, $ltitoolsref, @files) = @_;
     my ($donechk,$allmaps,%hierarchy,%titles,%addedmaps,%removefrommap,
         %removeparam,$importuploaded,$fixuperrors);
     $allmaps = {};
@@ -615,6 +615,39 @@
             }
         }
 	if ($url) {
+            if ($url =~ m{^(/adm/$coursedom/$coursenum/(\d+)/exttool)s?\:?(.*)$}) {
+                $url = $1;
+                my $marker = $2;
+                my $info = $3;
+                my ($toolid,%toolhash);
+                my @toolinfo = split(/:/,$info);
+                if ($residx) {
+                    my %toolsettings=&Apache::lonnet::dump('exttool_'.$marker,$coursedom,$coursenum);
+                    $toolid = $toolsettings{'id'};
+                } else {
+                    $toolid = shift(@toolinfo);  
+                }
+                $toolid =~ s/\D//g;
+                ($toolhash{'target'},$toolhash{'width'},$toolhash{'height'}) = @toolinfo;
+                if (ref($ltitoolsref) eq 'HASH') {
+                    if (ref($ltitoolsref->{$toolid}) eq 'HASH') {
+                        if ($ltitoolsref->{$toolid}->{'url'} =~ m{^https://}) {
+                            $url =~ s/exttool$/exttools/;
+                        }
+                        $toolhash{'id'} = $toolid;
+                        if ($toolhash{'target'} eq 'iframe') {
+                            delete($toolhash{'width'});
+                            delete($toolhash{'height'});
+                        } elsif ($toolhash{'target'} eq 'window') {
+                            foreach my $item ('width','height') {
+                                $toolhash{$item} =~ s/^\s+//;
+                                $toolhash{$item} =~ s/\s+$//;
+                            }
+                        }
+                        my $putres = &Apache::lonnet::put('exttool_'.$marker,\%toolhash,$coursedom,$coursenum);
+                    }
+                }
+            }
             if (($caller eq 'londocs') &&
                 ($folder =~ /^default/)) {
                 if (($url =~ /\.(page|sequence)$/) && (!$donechk)) {
@@ -1150,10 +1183,12 @@
             ($url ne '')) {
             $clipboardcount ++;
             my ($is_external,$othercourse,$fromsupp,$is_uploaded_map,$parent,
-                $canpaste,$nopaste,$othercrs,$areachange);
+                $canpaste,$nopaste,$othercrs,$areachange,$is_exttool);
             my $extension = (split(/\./,$env{'docs.markedcopy_url_'.$suffix}))[-1];
             if ($url =~ m{^(?:/adm/wrapper/ext|(?:http|https)(?::|:))//} ) {
                 $is_external = 1;
+            } elsif ($url =~ m{^/adm/$match_domain/$match_courseid/\d+/exttools?$}) {
+                $is_exttool = 1;
             }
             if ($folder =~ /^supplemental/) {
                 $canpaste = &supp_pasteable($env{'docs.markedcopy_url_'.$suffix});
@@ -1202,7 +1237,7 @@
                 }  
             }
             my $buffer;
-            if ($is_external) {
+            if (($is_external) || ($is_exttool)) {
                 $buffer = &mt('External Resource').': '.
                     &LONCAPA::map::qtescape($env{'docs.markedcopy_title_'.$suffix}).' ('.
                     &LONCAPA::map::qtescape($url).')';
@@ -1362,7 +1397,8 @@
         (($url =~ /\.sequence$/) && ($url =~ m{^/uploaded/})) ||
         ($url =~ m{^/uploaded/$match_domain/$match_courseid/(docs|supplemental)/(default|\d+)/\d+/}) ||
         ($url =~ m{^/adm/$match_domain/$match_username/aboutme}) ||
-        ($url =~ m{^/public/$match_domain/$match_courseid/syllabus})) {
+        ($url =~ m{^/public/$match_domain/$match_courseid/syllabus}) ||
+        ($url =~ m{^/adm/$match_domain/$match_courseid/\d+/exttools?$})) {
         return 1;
     }
     return;
@@ -2880,7 +2916,7 @@
 
 sub editor {
     my ($r,$coursenum,$coursedom,$folder,$allowed,$upload_output,$crstype,
-        $supplementalflag,$orderhash,$iconpath,$pathitem)=@_;
+        $supplementalflag,$orderhash,$iconpath,$pathitem,$ltitoolsref)=@_;
     my ($randompick,$ishidden,$isencrypted,$plain,$is_random_order,$container);
     if ($allowed) {
         (my $breadcrumbtrail,$randompick,$ishidden,$isencrypted,$plain,
@@ -3079,6 +3115,17 @@
                         } else {
                             return $errortxt;
                         }
+                    } elsif ($url =~ m{^/adm/$coursedom/$coursenum/new/exttool}) {
+                        my ($suffix,$errortxt,$locknotfreed) =
+                            &new_timebased_suffix($coursedom,$coursenum,'exttool');
+                        if ($locknotfreed) {
+                            $r->print($locknotfreed);
+                        }
+                        if ($suffix) {
+                            $url =~ s{^(/adm/$coursedom/$coursenum)/new}{$1/$suffix};
+                        } else {
+                            return $errortxt;
+                        }
                     } elsif ($url =~ m{^/uploaded/$coursedom/$coursenum/(docs|supplemental)/(default|\d+)/new.html$}) {
                         if ($supplementalflag) {
                             next unless ($1 eq 'supplemental');
@@ -3101,7 +3148,7 @@
 	    }
             ($errtext,$fatal,my $fixuperrors) =
                 &group_import($coursenum, $coursedom, $folder,$container,
-                              'londocs', at imports);
+                              'londocs',$ltitoolsref, at imports);
 	    return $errtext if ($fatal);
             if ($fixuperrors) {
                 $r->print($fixuperrors);
@@ -3186,7 +3233,7 @@
         $output .= &entryline($idx,$name,$url,$folder,$allowed,$res,
                               $coursenum,$coursedom,$crstype,
                               $pathitem,$supplementalflag,$container,
-                              \%filters,\%curr_groups);
+                              \%filters,\%curr_groups,$ltitoolsref);
         $idx++;
         $shown++;
     }
@@ -3552,7 +3599,8 @@
 
 sub entryline {
     my ($index,$title,$url,$folder,$allowed,$residx,$coursenum,$coursedom,
-        $crstype,$pathitem,$supplementalflag,$container,$filtersref,$currgroups)=@_;
+        $crstype,$pathitem,$supplementalflag,$container,$filtersref,$currgroups,
+        $ltitoolsref)=@_;
     my ($foldertitle,$renametitle,$oldtitle);
     if (&is_supplemental_title($title)) {
 	($title,$foldertitle,$renametitle) = &Apache::loncommon::parse_supplemental_title($title);
@@ -3643,6 +3691,7 @@
 		'rn' => 'Rename',
 		'cp' => 'Copy',
                 'ex' => 'External Resource',
+                'et' => 'External Tool',
                 'ed' => 'Edit',
                 'pr' => 'Preview',
                 'sv' => 'Save',
@@ -3660,6 +3709,7 @@
 			    |/aboutme$
 			    |/navmaps$
 			    |/bulletinboard$
+                            |/exttools?$
 			    |\.html$)}x)
              || $isexternal) {
 	    $skip_confirm = 1;
@@ -3796,7 +3846,9 @@
 	        }
 	    } elsif ($url=~m|^/ext/|) {
 	        $url='/adm/wrapper'.$url;
-	    }
+	    } elsif ($url=~m{^/adm/$coursedom/$coursenum/\d+/exttools?$}) {
+		$url='/adm/wrapper'.$url;
+            }
             if (&Apache::lonnet::symbverify($symb,$url)) {
 	        $url.=(($url=~/\?/)?'&':'?').'symb='.&escape($symb);
             } else {
@@ -3862,12 +3914,17 @@
 $form_end; 
         }
     } elsif ($supplementalflag && !$allowed) {
+        my $isexttool;
+        if ($url=~m{^/adm/$coursedom/$coursenum/\d+/exttools?$}) {
+            $url='/adm/wrapper'.$url;
+            $isexttool = 1;
+        }
         $url .= ($url =~ /\?/) ? '&':'?';
         $url .= 'folderpath='.&HTML::Entities::encode($esc_path,'<>&"');
         if ($title) {
             $url .= '&title='.&HTML::Entities::encode($renametitle,'<>&"');
         }
-        if ($isexternal && $orderidx) {
+        if ((($isexternal) || ($isexttool)) && $orderidx) {
             $url .= '&idx='.$orderidx;
         }
     }
@@ -3878,6 +3935,11 @@
         if ($isexternal) {
             ($editlink,$extresform) = 
                 &Apache::lonextresedit::extedit_form(0,$residx,$orig_url,$title,$pathitem);
+        } elsif ($orig_url =~ m{^/adm/$coursedom/$coursenum/\d+/exttools?$}) {
+            ($editlink,$extresform) =
+                &Apache::lonextresedit::extedit_form(0,$residx,$orig_url,$title,$pathitem,
+                                                     undef,undef,undef,'tool',$coursedom,
+                                                     $coursenum,$ltitoolsref);
         } elsif (!$isfolder && !$ispage) {
             my ($cfile,$home,$switchserver,$forceedit,$forceview) = 
                 &Apache::lonnet::can_edit_resource($fileloc,$coursenum,$coursedom,$orig_url);
@@ -4789,6 +4851,7 @@
     my $container;
     my $containertag;
     my $pathitem;
+    my %ltitools;
 
 # Do we directly jump somewhere?
 
@@ -4926,11 +4989,13 @@
                 }
             }
             my $tabidstr = join("','", at tabids);
+            %ltitools = &Apache::lonnet::get_domain_ltitools($coursedom);
+            my $exttoolurl = "/adm/$coursedom/$coursenum/new/exttool";
 	    $script .= &editing_js($udom,$uname,$supplementalflag).
                        &history_tab_js().
                        &inject_data_js().
                        &Apache::lonhtmlcommon::resize_scrollbox_js('docs',$tabidstr,$tid).
-                       &Apache::lonextresedit::extedit_javascript();
+                       &Apache::lonextresedit::extedit_javascript(\%ltitools);
             $addentries = {
                             onload   => "javascript:resize_scrollbox('contentscroll','1','1');",
                           };
@@ -5048,6 +5113,8 @@
                 'impo' => 'Import',
 		'lnks' => 'Import from Stored Links',
                 'impm' => 'Import from Assembled Map',
+                'extr' => 'External Resource',
+                'extt' => 'External Tool',
                 'selm' => 'Select Map',
                 'load' => 'Load Map',
                 'newf' => 'New Folder',
@@ -5191,6 +5258,11 @@
         my $extresourcesform =
             &Apache::lonextresedit::extedit_form(0,0,undef,undef,$pathitem,
                                                  $help{'Adding_External_Resource'});
+        my $exttoolform =
+            &Apache::lonextresedit::extedit_form(0,0,undef,undef,$pathitem,
+                                                 $help{'Adding_External_Tool'},undef,
+                                                 undef,'tool',$coursedom,$coursenum,
+                                                 \%ltitools);
     if ($allowed) {
         my $folder = $env{'form.folder'};
         if ($folder eq '') {
@@ -5436,6 +5508,11 @@
         my @importdoc = (
         {'<img class="LC_noBorder LC_middle" src="/res/adm/pages/extres.png" alt="'.$lt{extr}.'" onclick="toggleUpload(\'ext\');" />'=>$extresourcesform}
         );
+        if (keys(%ltitools)) {
+            push(@importdoc,
+                {'<img class="LC_noBorder LC_middle" src="/res/adm/pages/extres.png" alt="'.$lt{extt}.'" onclick="toggleUpload(\'tool\');" />'=>$exttoolform},
+        );
+        }
         unless ($container eq 'page') {
             push(@importdoc,
                 {'<img class="LC_noBorder LC_middle" src="/res/adm/pages/ims.png" alt="'.$lt{imsf}.'" onclick="javascript:toggleUpload(\'ims\');" />'=>$imspform}
@@ -5477,7 +5554,7 @@
  $hadchanges=0;
        unless (($supplementalflag || $toolsflag)) {
           my $error = &editor($r,$coursenum,$coursedom,$folder,$allowed,'',$crstype,
-                              $supplementalflag,\%orderhash,$iconpath,$pathitem);
+                              $supplementalflag,\%orderhash,$iconpath,$pathitem,\%ltitools);
           if ($error) {
              $r->print('<p><span class="LC_error">'.$error.'</span></p>');
           }
@@ -5544,6 +5621,12 @@
             &Apache::lonextresedit::extedit_form(1,0,undef,undef,$pathitem,
                                                  $help{'Adding_External_Resource'});
 
+        my $supexttoolform =
+            &Apache::lonextresedit::extedit_form(1,0,undef,undef,$pathitem,
+                                                 $help{'Adding_External_Tool'},
+                                                 undef,undef,'tool',$coursedom,
+                                                 $coursenum,\%ltitools);
+
 	my $supnewsylform=(<<SNSFORM);
 	<form action="/adm/coursedocs" method="post" name="supnewsyl">
 	<input type="hidden" name="active" value="ff" />
@@ -5597,10 +5680,16 @@
 		);
 my @supimportdoc = (
 		{'<img class="LC_noBorder LC_middle" src="/res/adm/pages/extres.png" alt="'.$lt{extr}.'" onclick="javascript:toggleUpload(\'suppext\')" />'
-            =>$supextform},
-                {'<img class="LC_noBorder LC_middle" src="/res/adm/pages/pdfupload.png" alt="'.$lt{upl}.'" onclick="javascript:toggleUpload(\'suppdoc\');" />'
+            =>$supextform});
+        if (keys(%ltitools)) {
+            push(@supimportdoc,
+                {'<img class="LC_noBorder LC_middle" src="/res/adm/pages/extres.png" alt="'.$lt{extt}.'" onclick="javascript:toggleUpload(\'supptool\')" />'
+            =>$supexttoolform});
+        }
+        push(@supimportdoc, 
+            {'<img class="LC_noBorder LC_middle" src="/res/adm/pages/pdfupload.png" alt="'.$lt{upl}.'" onclick="javascript:toggleUpload(\'suppdoc\');" />'
             =>$supupdocform},
-                   );
+        );
 
 $supupdocform =  &create_form_ul(&create_list_elements(@supimportdoc));
 my %suporderhash = (
@@ -5610,7 +5699,7 @@
                 );
         if ($supplementalflag) {
            my $error = &editor($r,$coursenum,$coursedom,$folder,$allowed,'',$crstype,
-                               $supplementalflag,\%suporderhash,$iconpath,$pathitem);
+                               $supplementalflag,\%suporderhash,$iconpath,$pathitem,\%ltitools);
            if ($error) {
               $r->print('<p><span class="LC_error">'.$error.'</span></p>');
            } else {
@@ -6072,12 +6161,12 @@
         $backtourl = '/adm/navmaps';
     }
 
-    my $fieldsets = "'ext','doc'";
+    my $fieldsets = "'ext','tool','doc'";
     unless ($main_container_page) {
         $fieldsets .=",'ims'";
     }
     if ($supplementalflag) {
-        $fieldsets = "'suppext','suppdoc'";
+        $fieldsets = "'suppext','supptool','suppdoc'";
     }
 
     return <<ENDNEWSCRIPT;
@@ -6187,6 +6276,19 @@
             }
         }
         document.getElementById('upload'+blocks[i]+'form').style.display=disp;
+        if ((caller == 'tool') || (caller == 'supptool')) {
+            if (disp == 'block') {
+                if (document.getElementById('LC_exttoolid')) { 
+                    var toolselector = document.getElementById('LC_exttoolid'); 
+                    var suppflag = 0;
+                    if (caller == 'supptool') {
+                        suppflag = 1;
+                    }
+                    currForm = document.getElementById('new'+caller);
+                    updateExttool(toolselector,currForm,suppflag); 
+                }
+            }
+        }
     }
     resize_scrollbox('contentscroll','1','1');
     return;
Index: loncom/interface/lonextresedit.pm
diff -u loncom/interface/lonextresedit.pm:1.8 loncom/interface/lonextresedit.pm:1.9
--- loncom/interface/lonextresedit.pm:1.8	Tue Jun  9 21:22:56 2015
+++ loncom/interface/lonextresedit.pm	Tue Jan 26 14:30:25 2016
@@ -1,7 +1,7 @@
 # The LearningOnline Network
 # Documents
 #
-# $Id: lonextresedit.pm,v 1.8 2015/06/09 21:22:56 damieng Exp $
+# $Id: lonextresedit.pm,v 1.9 2016/01/26 14:30:25 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -56,15 +56,19 @@
     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 ($supplementalflag,$updated,$output,$errormsg,$residx,$url,$title,$symb);
+    my ($supplementalflag,$updated,$output,$errormsg,$residx,$url,$title,
+        $symb,$type);
     if (($env{'form.folderpath'} =~ /^supplemental/) && ($env{'form.suppurl'})) {
         $supplementalflag = 1;
+        if (&unescape($env{'form.suppurl'}) =~ m{^/adm/$cdom/$cnum/\d+/exttools?$}) {
+            $type = 'tool';
+        }
     }
     if (($supplementalflag) || ($env{'form.symb'} =~ /^uploaded/)) {
         ($updated,$output,$errormsg,$residx,$url,$title,$symb) =
             &process_changes($supplementalflag,$cdom,$cnum,$chome);
         if ($supplementalflag) {
-            if ($url ne $env{'form.suppurl'}) {
+            if ($url ne &unescape($env{'form.suppurl'})) {
                  $env{'form.suppurl'} = $url;
             }
             if ($title ne $env{'form.title'}) {
@@ -75,33 +79,48 @@
             if ($symb ne $env{'form.symb'}) {
                 $env{'form.symb'} = $symb;
             }
+            if ($url =~ m{/adm/$cdom/$cnum/\d+/exttools?$}) {
+                $type = 'tool';
+            }
         }
     } else {
         $errormsg = &mt('Information about external resource to edit is missing.');
     }
     if ($updated) {
-        $output = &Apache::lonhtmlcommon::confirm_success(&mt('External Resource updated'));
+        my $msg = &mt('External Resource updated');
+        if ($type eq 'tool') {
+            $msg = &mt('External Tool updated');
+        }
+        $output = &Apache::lonhtmlcommon::confirm_success($msg);
     }
     if ($errormsg) {
         $errormsg = '<p class="LC_error">'.$errormsg.'</p>';
     }
+    my %ltitools;
+    if ($type eq 'tool') {
+        %ltitools = &Apache::lonnet::get_domain_ltitools($cdom);
+    }
     my $js = &Apache::lonhtmlcommon::scripttag(&extedit_javascript());
     my $pathitem = '<input type="hidden" name="folderpath" value="'.
                    &HTML::Entities::encode($env{'form.folderpath'},'<>&"').'" />';
-    $r->print(&Apache::loncommon::start_page('External Resource Editor',$js).
+    my $description = 'External Resource Editor';
+    if ($type eq 'tool') {
+        $description = 'External Tool Editor'; 
+    }
+    $r->print(&Apache::loncommon::start_page($description,$js).
               '<div class="LC_left_float">'.
               $output.
               $errormsg.
               &extedit_form($supplementalflag,$residx,$url,$title,$pathitem,undef,
-                           'direct',$env{'form.symb'}).
+                           'direct',$env{'form.symb'},$type,$cdom,$cnum,\%ltitools).
               '</div>'.&Apache::loncommon::end_page());
     return OK;
 }
 
 sub process_changes {
     my ($supplementalflag,$cdom,$cnum,$chome) = @_;
-    my ($folder,$container,$output,$errormsg,$updated,$symb,$oldidx,$oldurl,
-        $oldtitle,$newidx,$newurl,$newtitle,$residx,$url,$title);
+    my ($folder,$container,$output,$errormsg,$updated,$symb,$oldidx,$oldurl,$type,
+        $oldtitle,$newidx,$newurl,$newtitle,$residx,$url,$title,$marker,$args);
     if ($env{'form.symb'}) {
         $symb = $env{'form.symb'};
         (my $map,$oldidx,$oldurl)=&Apache::lonnet::decode_symb($symb);
@@ -110,19 +129,27 @@
             $container = $3;
         }
         $oldtitle = &Apache::lonnet::gettitle($env{'form.symb'});
+        if ($oldurl =~ m{^ext/(.+)$}) {
+            my $external = $1;
+            if ($external =~ m{^https://}) {
+                $oldurl = $external;
+            } else {
+                $oldurl = 'http://'.$oldurl;
+            }
+            $type = 'ext';
+        } else {
+            $type = 'tool';
+        }
     } elsif ($env{'form.folderpath'}) {
         $folder = &unescape( (split('&',$env{'form.folderpath'}))[-2] );
         $oldurl = &unescape($env{'form.suppurl'});
         $oldtitle = &unescape($env{'form.title'});
         $container = 'sequence';
         $supplementalflag = 1;
-    }
-    if ($oldurl =~ m{^ext/(.+)$}) {
-        my $external = $1; 
-        if ($external =~ m{^https://}) {
-            $oldurl = $external;
+        if ($oldurl =~ m{^/adm/$cdom/$cnum/\d+/exttools?$}) {
+            $type = 'tool';
         } else {
-            $oldurl = 'http://'.$oldurl;
+            $type = 'ext';
         }
     }
     $url = $oldurl;
@@ -130,6 +157,11 @@
     if ($env{'form.importdetail'}) {
         ($newtitle,$newurl,$newidx) =
             map {&unescape($_)} split(/\=/,$env{'form.importdetail'});
+        if ($newurl =~ m{^(/adm/$cdom/$cnum/(\d+)/exttools?)\:?(.*)$}) {
+            $newurl = $1;
+            $marker = $2;
+            $args = $3;
+        }
     }
     if ($supplementalflag) {
         $residx = $newidx;
@@ -147,7 +179,18 @@
             if ($mismatchedid) {
                 $errormsg = 'Wrong item identifier';
             } elsif (($newtitle eq $oldtitle) && ($newurl eq $oldurl)) {
-                $output = &mt('No change');
+                if ($type eq 'tool') {
+                    if ($args) {
+                        ($updated,$errormsg) = &update_exttool($marker,$cdom,$cnum,$args);
+                        unless ($updated) {
+                            $output = &mt('No change');      
+                        }
+                    } else {
+                        $output = &mt('No change');
+                    }
+                } else {
+                    $output = &mt('No change');
+                }
             } else {
                 my $map = "/uploaded/$cdom/$cnum/$folder.$container";
                 my ($errtext,$fatal) = &LONCAPA::map::mapread($map);
@@ -156,8 +199,15 @@
                 } else {
                     my $saveurl = &LONCAPA::map::qtunescape($newurl);
                     my $savetitle = &LONCAPA::map::qtunescape($newtitle);
+                    my $ext = 'true';
+                    if ($type eq 'tool') {
+                        if ($args) {
+                            ($updated,$errormsg) = &update_exttool($marker,$cdom,$cnum,$args);
+                        }
+                        $ext = 'false';
+                    }
                     $LONCAPA::map::resources[$residx] =
-                        join(':', ($savetitle,$saveurl,'true','normal','res'));
+                        join(':', ($savetitle,$saveurl,$ext,'normal','res'));
                     my ($outtext,$errtext) = &LONCAPA::map::storemap($map,1);
                     if ($errtext) {
                         $errormsg = &mt('Update failed: [_1].',$errtext);
@@ -166,8 +216,10 @@
                         $title = $newtitle;
                         if ($newurl ne $oldurl) {
                             $url = $newurl;
-                            $newurl =~ s{^http://}{};
-                            $newurl = "ext/$newurl";
+                            if ($ext eq 'true') {
+                                $newurl =~ s{^http://}{};
+                                $newurl = "ext/$newurl";
+                            }
                         }
                         if (!$supplementalflag) {
                             if ($newurl ne $oldurl) {
@@ -195,45 +247,101 @@
             $output = &mt('No change');
         }
     } else {
-        $errormsg = &mt('Information about current external resource is incomplete.');
+        if ($type eq 'tool') {
+            $errormsg = &mt('Information about current external tool is incomplete.');
+        } else {
+            $errormsg = &mt('Information about current external resource is incomplete.');
+        }
     }
     return ($updated,$output,$errormsg,$residx,$url,$title,$symb);
 }
 
+sub update_exttool {
+    my ($marker,$cdom,$cnum,$args) = @_;
+    my %toolhash=&Apache::lonnet::dump('exttool_'.$marker,$cdom,$cnum);
+    my (%newhash,$changed,$errormsg);
+    ($newhash{'target'},$newhash{'width'},$newhash{'height'}) = split(/:/,$args);
+    my %toolhash=&Apache::lonnet::dump('exttool_'.$marker,$cdom,$cnum);
+    foreach my $item ('target','width','height') {
+        $newhash{$item} =~ s/^\s+//;
+        $newhash{$item} =~ s/\s+$//;
+        unless ($item eq 'target') {
+            if ($newhash{'target'} eq 'iframe') {
+                $newhash{$item} = '';
+            }
+        }
+        if ($toolhash{$item} ne $newhash{$item}) {
+            if ($newhash{$item} eq '') {
+                delete($toolhash{$item});
+            } else {
+                $toolhash{$item} = $newhash{$item};
+            }
+            $changed = 1;
+        }
+    }
+    if ($changed) {
+        my $putres = &Apache::lonnet::put('exttool_'.$marker,\%toolhash,$cdom,$cnum);
+        unless ($putres eq 'ok') {
+             $errormsg = &mt('Failed to save updated settings.').' '.&mt('Error: [_1].',$putres);
+        }
+    }
+    return ($changed,$errormsg);
+}
+
 sub extedit_form {
-    my ($supplementalflag,$residx,$orig_url,$orig_title,$pathitem,$helpitem,$caller,$symb) = @_;
+    my ($supplementalflag,$residx,$orig_url,$orig_title,$pathitem,$helpitem,$caller,
+        $symb,$type,$cdom,$cnum,$ltitools) = @_;
+    if ($type ne 'tool') {
+        $type = 'ext';
+    }
     my %lt = &Apache::lonlocal::texthash(
         ex => 'External Resource',
+        et => 'External Tool',
         ed => 'Edit',
         ee => 'External Resource Editor',
+        te => 'External Tool Editor',
         pr => 'Preview',
         sv => 'Save',
         ul => 'URL',
         ti => 'Title',
         al => 'Add Link',
+        at => 'Add Tool',
     );
-    my $formname = 'newext';
     my $tabid = 'aa';
-    my $toggle = 'ext';
-    my $fieldsetid = 'uploadextform';
-    my $urlid = 'exturl';
     my $size = 60;
     if ($supplementalflag) {
-        $formname = 'newsuppext';
         $tabid = 'ee';
-        $toggle = 'suppext';
-        $fieldsetid = 'uploadsuppextform';
-        $urlid = 'suppexturl';
+    }
+    my ($formname,$formid,$toggle,$fieldsetid,$urlid,$dispdivstyle,$dimendivstyle,
+        $legend,$urlelem,$toolelem,%toolattr);
+    $formname = 'new'.$type;
+    $toggle = $type;
+    $fieldsetid = 'upload'.$type.'form';
+    $urlid = $type.'url';
+    map { $toolattr{$_} = $type.$_; } ('dispdiv','dimendiv','dimenwidth','dimenheight');
+    $dispdivstyle = 'display:none';
+    $dimendivstyle = 'display:none';
+    if ($supplementalflag) {
+        $formname = 'newsupp'.$type;
+        $toggle = 'supp'.$type;
+        $fieldsetid = 'uploadsupp'.$type.'form';
+        $urlid = 'supp'.$type.'url';
+        map { $toolattr{$_} = 'supp'.$toolattr{$_}; } (keys(%toolattr));
     }
     my ($link,$legend,$active,$srcclass,$extsrc,$preview,$title,$save,
-        $fieldsetstyle,$action,$hiddenelem,$form);
+        $fieldsetstyle,$action,$hiddenelem,$form,$width,$height,$tooltarget,%chkstate);
     $fieldsetstyle = 'display: none;';
     $action = '/adm/coursedocs';
     if ($residx) {
         if ($caller eq 'direct') {
             $fieldsetstyle = 'display: block;';
             $action = '/adm/extresedit';
-            $legend = "<legend>$lt{'ee'}</legend>";
+            if ($type eq 'tool') {
+                $legend = $lt{'ee'};
+            } else {
+                $legend = $lt{'te'};
+            }
+            $legend = '<legend>'.$legend.'</legend>';
             if ($symb) {
                 $hiddenelem = '<input type="hidden" name="symb" value="'.$symb.'" />';
             } elsif ($supplementalflag) {
@@ -242,50 +350,150 @@
                               '<input type="hidden" name="title" value="'.
                               &HTML::Entities::encode(&escape($orig_title),'<>&"').'" />';
             }
-        } else {        
-            $link = '<a class="LC_docs_ext_edit" href="javascript:editext('."'$residx'".');">'.$lt{'ed'}.'</a> '."\n";
+        } else {
+            $link = '<a class="LC_docs_ext_edit" href="javascript:editext('."'$residx','$type'".');">'.$lt{'ed'}.'</a> '."\n";
             $size = 40;
             $active = '<input type="hidden" name="active" value="'.$tabid.'" />';
         }
-        $formname = "editext_$residx";
-        $fieldsetid = "uploadext$residx";
-        $urlid = "exturl_$residx";
+        $formname = 'edit'.$type.'_'.$residx;
+        $fieldsetid = 'upload'.$type.$residx;
+        $urlid = $type.'url_'.$residx;
+        map { $toolattr{$_} .= '_'.$residx; } (keys(%toolattr));
         $srcclass = ' class="LC_nobreak"';
-        $extsrc = '<span class="LC_docs_ext_edit">'.$lt{'ul'}.' </span>';
-        $preview = ' <a class="LC_docs_ext_edit" href="javascript:extUrlPreview('."'$urlid'".');">'.$lt{'pr'}.'</a>';
+        if ($type eq 'ext') {
+            $extsrc = '<span class="LC_docs_ext_edit">'.$lt{'ul'}.' </span>';
+            $preview = ' <a class="LC_docs_ext_edit" href="javascript:extUrlPreview('."'$urlid'".');">'.$lt{'pr'}.'</a>';
+        }
         $title = '<span class="LC_docs_ext_edit">'.$lt{'ti'}.' </span>';
         $save = $lt{'sv'};
     } else {
-        $link = '<a class="LC_menubuttons_link" href="javascript:toggleUpload('."'$toggle'".');">'.$lt{'ex'}.'</a>'.$helpitem;
-        $legend = "<legend>$lt{'ex'}</legend>";
-        $extsrc = $lt{'ul'}.':<br />';
+        $link = $lt{'ex'};
+        if ($type eq 'tool') {
+            $link = $lt{'et'};
+        }
+        $link = '<a class="LC_menubuttons_link" href="javascript:toggleUpload('."'$toggle'".');">'.$link.'</a>'.$helpitem;
+        if ($type eq 'tool') {
+            $legend = $lt{'te'};
+        } else {
+            $legend = $lt{'ee'};
+        }
+        $legend = '<legend>'.$legend.'</legend>';
         $title = $lt{'ti'}.':<br />';
         $residx = 0;
-        $orig_url = 'http://';
-        $orig_title = $lt{'ex'};
-        $preview = '<input type="button" name="view" value="'.$lt{'pr'}.'" onclick="javascript:extUrlPreview('."'$urlid'".');" />';
-        $save = $lt{'al'};
+        if ($type eq 'ext') {
+            $orig_url = 'http://';
+            $orig_title = $lt{'ex'};
+            $extsrc = $lt{'ul'}.':<br />';
+            $preview = '<input type="button" name="view" value="'.$lt{'pr'}.'" onclick="javascript:extUrlPreview('."'$urlid'".');" />';
+            $save = $lt{'al'};
+        } else {
+            $orig_title = $lt{'et'};
+            $save = $lt{'at'};
+            $orig_url = "/adm/$cdom/$cnum/new/exttool"; 
+        }
         $pathitem .= '<br />';
     }
+    $formid = $formname;
+    if ($type eq 'ext') {
+        $urlelem = '<input type="text" size="'.$size.'" name="exturl" id="'.$urlid.'" value="'.$orig_url.'" />';
+    } else {
+        my $class = 'LC_nobreak';
+        if ($residx) {
+            $class = 'LC_docs_ext_edit LC_nobreak'; 
+            if ($orig_url =~ m{^/adm/$cdom/$cnum/(\d+)/exttools?$}) {
+                my $marker = $1;
+                my %toolhash=&Apache::lonnet::dump('exttool_'.$marker,$cdom,$cnum);
+                if ($toolhash{'id'}) {
+                    $dispdivstyle = 'display:block';
+                    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.'" />';
+                                }
+                                $tooltarget = $toolhash{'target'};
+                                if ($tooltarget eq 'window') {
+                                    $dimendivstyle = 'display:block';
+                                    $chkstate{'window'} = 'checked="checked" ';
+                                } else {
+                                    $chkstate{'iframe'} = 'checked="checked" ';
+                                }
+                                $width = $toolhash{'width'};
+                                $height = $toolhash{'height'};
+                                $toolelem = '<span class="LC_nobreak">'.$image.' '.$tooltitle.'</span><br />';
+                            }
+                        }
+                    }
+                }
+            }
+        } else {
+            $toolelem = '<span class="LC_docs_ext_edit">'."\n".
+                       '<select name="exttoolid" id="LC_exttoolid" onchange="javascript:updateExttool(this,'.
+                       'this.form,'."'$supplementalflag'".');">'."\n".
+                       '<option value="" selected="selected">'.&mt('Select').'</option>';
+            my %bynum;
+            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}];
+                    }
+                }
+            }
+            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>';
+                    }
+                }
+            }
+            $toolelem .= '</select></span>';
+        }
+        $toolelem .= '<div id="'.$toolattr{'dispdiv'}.'" style="'.$dispdivstyle.'">'.
+                    '<span class="'.$class.'">'.&mt('Display target:').' '.
+                    '<label><input type="radio" name="exttooltarget" value="iframe" '.$chkstate{'iframe'}.'onclick="updateTooldim(this.form,'.
+                    "'$toolattr{dimendiv}','$toolattr{dimenwidth}','$toolattr{dimenheight}'".');">'.&mt('iframe').'</label>'.(' 'x2).
+                    '<label><input type="radio" name="exttooltarget" value="window" '.$chkstate{'window'}.'onclick="updateTooldim(this.form,'.
+                    "'$toolattr{dimendiv}','$toolattr{dimenwidth}','$toolattr{dimenheight}'".');">'.&mt('window').'</label>'.
+                    '</span><div id="'.$toolattr{'dimendiv'}.'" style="'.$dimendivstyle.'">'. 
+                    '<span class="'.$class.'">'.
+                    &mt('Width').'<input type="text" id="'.$toolattr{'dimenwidth'}.'" name="exttoolwidth" value="'.$width.'">'.(' 'x2).
+                    &mt('Height').'<input type="text" id="'.$toolattr{'dimenheight'}.'" name="exttoolheight" value="'.$height.'"></span>'."\n".
+                    '</div></div>';
+    }
+    my $chooser = $toolelem;
+    if ($type eq 'ext') {
+        $chooser = "
+<div>
+<span$srcclass>
+$extsrc
+$urlelem
+$preview
+</span>
+</div>
+";
+    }
     $form = <<ENDFORM;
-<form action="$action" method="post" name="$formname">
+<form action="$action" method="post" name="$formname" id="$formid">
 <fieldset id="$fieldsetid" style="$fieldsetstyle">
 $legend
 $active
-<span$srcclass>
-$extsrc
-<input type="text" size="$size" name="exturl" id="$urlid" value="$orig_url" />
-$preview
-</span> 
-<br />
+$chooser
+<div>
 <span$srcclass>
 $title
 <input type="text" size="$size" name="exttitle" value="$orig_title" />
 <input type="hidden" name="importdetail" value="" />
 $pathitem
 $hiddenelem
-<input type="button" value="$save" onclick="javascript:setExternal(this.form,'$residx');" />
+<input type="button" value="$save" onclick="javascript:setExternal(this.form,'$residx','$type','$orig_url');" />
 </span>
+</div>
 </fieldset>
 </form>
 ENDFORM
@@ -297,8 +505,8 @@
 }
 
 sub display_editor {
-    my ($url,$folderpath,$symb,$idx) = @_;
-    my ($residx,$supplementalflag,$title,$pathitem,$output);
+    my ($url,$folderpath,$symb,$idx,$type,$cdom,$cnum) = @_;
+    my ($residx,$supplementalflag,$title,$pathitem,$output,$js);
     if ($folderpath =~ /^supplemental/) {
         $supplementalflag = 1;
         $residx = $idx;
@@ -311,19 +519,51 @@
         my $path = &Apache::loncommon::symb_to_docspath($symb);
         $pathitem = '<input type="hidden" name="folderpath" value="'.&HTML::Entities::encode($path,'<>&"').'" />';
     }
-    my $js = &Apache::lonhtmlcommon::scripttag(&extedit_javascript());
+    my %ltitools;
+    if ($type eq 'tool') {
+        %ltitools = &Apache::lonnet::get_domain_ltitools($cdom);
+    }
+    $js = &Apache::lonhtmlcommon::scripttag(&extedit_javascript());
     my $args = { 'force_register' => $env{'form.register'} };
-    return &Apache::loncommon::start_page('External Resource Editor',$js,$args).
+    my $description = 'External Resource Editor';
+    if ($type eq 'tool') {
+        $description = 'External Tool Editor';
+    }
+    return &Apache::loncommon::start_page($description,$js,$args).
            '<div class="LC_left_float">'.
-           &extedit_form($supplementalflag,$residx,$url,$title,$pathitem,undef,'direct',$symb).
+           &extedit_form($supplementalflag,$residx,$url,$title,$pathitem,undef,'direct',
+                         $symb,$type,$cdom,$cnum,\%ltitools).
            '</div>'.
            &Apache::loncommon::end_page();
 }
 
 sub extedit_javascript {
+    my ($toolsref) = @_;
+    my $toolsjs;
+    if (ref($toolsref) eq 'HASH') {
+        my $num = scalar(keys(%{$toolsref}));
+        $toolsjs = "        var ltitools = new Array($num);\n".
+                   "        var ltitoolsTarget = new Array($num);\n".
+                   "        var ltitoolsWidth = new Array($num);\n".
+                   "        var ltitoolsHeight = new Array($num);\n"; 
+        my $i = 0;
+        foreach my $key (sort { $a <=> $b } keys(%{$toolsref})) {
+            if (ref($toolsref->{$key})) {
+                my $target = $toolsref->{$key}->{'target'};
+                my $width = $toolsref->{$key}->{'width'};
+                my $height = $toolsref->{$key}->{'height'};
+                $toolsjs .= '        ltitools['.$i.'] = '."'$key';\n".
+                            '        ltitoolsTarget['.$i.'] = '."'$target';\n".
+                            '        ltitoolsWidth['.$i.'] = '."'$width';\n".
+                            '        ltitoolsHeight['.$i.'] = '."'$height';\n";
+                $i++;
+            }
+        }
+    }
     my %js_lt = &Apache::lonlocal::texthash(
         invurl  => 'Invalid URL',
         titbl   => 'Title is blank',
+        invtool => 'Please select an external tool',
     );
     &js_escape(\%js_lt);
 
@@ -335,40 +575,74 @@
 
 var regexp = $urlregexp;
 
-function setExternal(extform,residx) {
+function setExternal(extform,residx,type,exttoolurl) {
     var title=extform.exttitle.value;
     if (!String.trim) {
         String.prototype.trim = function() {return this.replace(\/^\\s+|\\s+$\/g, "");};    }
-    var url=extform.exturl.value;
     if (title == null || title.trim()=="") {
         alert("$js_lt{'titbl'}");
         extform.exttitle.focus();
         return;
     }
-    if (regexp.test(url)) {
-        url = escape(url);
+    if (type == 'ext') {
+        var url=extform.exturl.value;
+        if (!regexp.test(url)) {
+            alert("$js_lt{'invurl'}");
+            extform.exturl.focus();
+            return;
+        } else {
+            url = escape(url);
+            title = escape(title);
+            if (residx > 0) {
+               eval("extform.importdetail.value=title+'='+url+'='+residx;extform.submit();");
+            } else {
+               eval("extform.importdetail.value=title+'='+url;extform.submit();");
+            }
+        }
+    } else {
         title = escape(title);
+        var info = exttoolurl;
+        if (residx == 0) {
+            var toolid = parseInt(extform.exttoolid.options[extform.exttoolid.selectedIndex].value);
+            if (isNaN(toolid)) {
+                alert("$js_lt{'invtool'}");
+                return;
+            }
+            info += ':'+toolid;
+        }
+        if (extform.exttooltarget.length) {
+            for (var i=0; i<extform.exttooltarget.length; i++) {
+                if (extform.exttooltarget[i].checked) {
+                    if (extform.exttooltarget[i].value == 'window') {
+                        var width = extform.exttoolwidth.value;
+                        width.trim();
+                        var height = extform.exttoolheight.value;
+                        height.trim();
+                        info += ':window:'+width+':'+height;  
+                    } else {
+                        info += ':iframe';
+                    }
+                }
+            }
+        }
+        info=escape(info);
         if (residx > 0) {
-            eval("extform.importdetail.value=title+'='+url+'='+residx;extform.submit();");
+            eval("extform.importdetail.value=title+'='+info+'='+residx;extform.submit();");
         } else {
-            eval("extform.importdetail.value=title+'='+url;extform.submit();");
+            eval("extform.importdetail.value=title+'='+info;extform.submit();");
         }
-    } else {
-        alert("$js_lt{'invurl'}");
-        extform.exturl.focus();
-        return;
     }
 }
 
-function editext(residx) {
-    if (document.getElementById('uploadext'+residx)) {
-        var curr = document.getElementById('uploadext'+residx).style.display;
+function editext(residx,type) {
+    if (document.getElementById('upload'+type+residx)) {
+        var curr = document.getElementById('upload'+type+residx).style.display;
         if (curr == 'none') {
             disp = 'block';
         } else {
             disp = 'none';
         }
-        document.getElementById('uploadext'+residx).style.display=disp;
+        document.getElementById('upload'+type+residx).style.display=disp;
     }
     resize_scrollbox('contentscroll','1','1');
     return;
@@ -385,6 +659,90 @@
     }
 }
 
+function updateExttool(caller,form,supplementalflag) {
+    var prefix = '';
+    if (supplementalflag == 1) {
+        prefix = 'supp';
+    }
+    dispdiv = prefix+'tooldispdiv';
+    dimendiv = prefix+'tooldimendiv';
+    widthinput = prefix+'toolwidth';
+    heightinput = prefix+'toolheight';
+    if (document.getElementById(dispdiv)) {
+        var toolpick = caller.options[caller.selectedIndex].value;
+        $toolsjs
+        if (toolpick == '') {
+            if (document.getElementById(dispdiv)) {
+                document.getElementById(dispdiv).style.display = 'none';    
+            }
+            if (document.getElementById(dimendiv)) {
+                document.getElementById(dimendiv).style.display = 'none';
+            }
+        } else {
+            if (document.getElementById(dispdiv)) {
+                document.getElementById(dispdiv).style.display = 'block';
+            }
+            if (ltitools.length > 0) {
+                for (var j=0; j<ltitools.length; j++) {
+                    if (ltitools[j] == toolpick) {
+                        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;
+                                }
+                            }
+                        }
+                        if (ltitoolsTarget[j] == 'window') {
+                            dimen = 'block';
+                            dimenwidth = ltitoolsWidth[j];
+                            dimenheight = ltitoolsHeight[j];                    
+                        } else {
+                            dimen = 'none';
+                            dimenwidth = '';
+                            dimenheight = '';
+                        }
+                        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;
+                        }
+                    }
+                    break;
+                }
+            }
+        }
+    }
+}
+
+function updateTooldim(form,dimendiv,widthinput,heightinput) {
+    if (form.exttooltarget.length) {
+        for (var i=0; i<form.exttooltarget.length; i++) {
+            if (form.exttooltarget[i].checked) {
+                var dimen = 'none';
+                if (form.exttooltarget[i].value == 'window') {
+                    dimen = 'block';
+                } else {
+                    if (document.getElementById(widthinput)) {
+                        document.getElementById(widthinput).value = '';
+                    }
+                    if (document.getElementById(heightinput)) {
+                        document.getElementById(heightinput).value = '';
+                    }
+                }
+                if (document.getElementById(dimendiv)) {
+                    document.getElementById(dimendiv).style.display = dimen;
+                }
+                break;
+            }
+        }
+    }
+}
+
 ENDJS
 
 }
Index: loncom/interface/lonexttool.pm
diff -u loncom/interface/lonexttool.pm:1.2 loncom/interface/lonexttool.pm:1.3
--- loncom/interface/lonexttool.pm:1.2	Mon Jan 25 20:13:02 2016
+++ loncom/interface/lonexttool.pm	Tue Jan 26 14:30:25 2016
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # Launch External Tool Provider (LTI)
 #
-# $Id: lonexttool.pm,v 1.2 2016/01/25 20:13:02 raeburn Exp $
+# $Id: lonexttool.pm,v 1.3 2016/01/26 14:30:25 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -74,15 +74,14 @@
         return OK;
     }
 
-    my $marker = (split(m{/},$r->uri))[4];
+    my ($marker,$exttool) = (split(m{/},$r->uri))[4,5];
     $marker=~s/\D//g;
 
     if (!$marker) {
         if ($target ne 'tex') {
-            &Apache::loncommon::simple_error_page($r,'Invalid Call',
-                                                  'Invalid Call');
+            $r->print(&mt('Invalid Call'));
         } else {
-            $r->print('\textbf{Invalid call}\end{document}');
+            $r->print('\textbf{'&mt('Invalid Call').'}\end{document}');
         }
         return OK;
     }
@@ -90,46 +89,47 @@
     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 $is_tool;
 
-    if ($r->uri eq "/adm/$cdom/$cnum/$marker/exttool") {
-        my %toolhash=&Apache::lonnet::dump('exttool_'.$marker,$cdom,$cnum);
-        if ($target eq 'tex') {
-            $r->print(&mt('External Tool'));
-        } else {
-            if (($toolhash{'key'} ne '') && ($toolhash{'secret'} ne '') && ($toolhash{'url'} ne '')) {
-                my %lti = &lti_params($r,\%toolhash);
-                $r->print(&launch_html($toolhash{'url'},$toolhash{'key'},
-                                       $toolhash{'secret'},$toolhash{'title'},\%lti));
-            } else {
-                &Apache::loncommon::simple_error_page($r,'External Tool Unavailable',
-                                                      'External Tool Unavailable');
+    if ($r->uri eq "/adm/$cdom/$cnum/$marker/$exttool") {
+        my %toolsettings=&Apache::lonnet::dump('exttool_'.$marker,$cdom,$cnum);
+        if ($toolsettings{'id'}) {
+            my %ltitools = &Apache::lonnet::get_domain_ltitools($cdom);
+            if (ref($ltitools{$toolsettings{'id'}}) eq 'HASH') {
+                my %toolhash = %{$ltitools{$toolsettings{'id'}}}; 
+                $toolhash{'display'} = {
+                                           target => $toolsettings{'target'},
+                                           width  => $toolsettings{'width'},
+                                           height => $toolsettings{'height'},
+                                       };
+                $is_tool = 1;
+                if ($target eq 'tex') {
+                    $r->print(&mt('External Tool'));
+                } else {
+                    my $submittext = &mt('Launch [_1]',$toolhash{'title'});
+                    if (($toolhash{'key'} ne '') && ($toolhash{'secret'} ne '') && ($toolhash{'url'} ne '')) {
+                        my %lti = &lti_params($r,$submittext,\%toolhash);
+                        $r->print(&launch_html($toolhash{'url'},$toolhash{'key'},
+                                               $toolhash{'secret'},$submittext,\%lti));
+                    } else {
+                        $r->print('<div>'.&mt('External Tool Unavailable').'</div>');
+                    }
+                }
             }
         }
-    } else {
+    }
+    unless ($is_tool) {
         if ($target ne 'tex') {
-            &Apache::loncommon::simple_error_page($r,'Invalid Call',
-                                                  'Invalid Call');
+            $r->print('<div>'.&mt('Invalid Call').'</div>');
         } else {
-            $r->print('\textbf{Invalid call}\end{document}');
+            $r->print('\textbf{'.&mt(Invalid Call).'}\end{document}');
         }
-        return OK;
     }
-
-    &print_end_page($r,$target);
     return OK;
 }
 
-sub print_end_page {
-    my ($r,$target) = @_;
-    if ($target ne 'tex') {
-        $r->print(&Apache::loncommon::end_page());
-    } else {
-        $r->print('\end{document}');
-    }
-}
-
 sub lti_params {
-    my ($r,$toolsref) = @_;
+    my ($r,$submittext,$toolsref) = @_;
     my ($version,$context_type,$msgtype,$toolname,$passback,$roster,$locale,
         %fields,%rolesmap,%display,%custom, at userlangs);
     if (ref($toolsref) eq 'HASH') {
@@ -165,7 +165,8 @@
     my $uname = $env{'user.name'};
     my $udom = $env{'user.domain'};
     my @possroles = qw(Instructor ContentDeveloper TeachingAssistant Learner);
-    my $ltirole = $rolesmap{$env{'request.role'}};
+    my ($roleprefix) = ($env{'request.role'} =~ /^(\w+)\./);
+    my $ltirole = $rolesmap{$roleprefix};
     unless (grep(/^\Q$ltirole\E$/, at possroles)) {
         $ltirole = 'Learner';
     }
@@ -218,10 +219,10 @@
         context_title                          => $env{'course.'.$env{'request.course.id'}.'.description'},
         launch_presentation_locale             => $locale,
     );
-    my $crshostname = $env{'course.'.$env{'request.course.id'}.'.home'};
-    my $crsprotocol = $Apache::lonnet::protocol{$crshostname};
+    my $crshome = $env{'course.'.$env{'request.course.id'}.'.home'};
+    my $crshostname = &Apache::lonnet::hostname($crshome);
     if ($crshostname) {
-        my $crsprotocol = $Apache::lonnet::protocol{$crshostname};
+        my $crsprotocol = $Apache::lonnet::protocol{$crshome};
         unless ($crsprotocol eq 'https') {
             $crsprotocol = 'http';
         } 
@@ -260,9 +261,9 @@
     if ($fields{'email'}) {
         my %emails = &Apache::loncommon::getemails($uname,$udom);
         my $contact_email;
-        foreach my $email ('permanentemail','critnotification','notification') {
-            if ($email =~ /\@/) {
-                $contact_email = $email;
+        foreach my $type ('permanentemail','critnotification','notification') {
+            if ($emails{$type} =~ /\@/) {
+                $contact_email = $emails{$type};
                 last;
             }
         }
@@ -276,24 +277,27 @@
     foreach my $key (keys(%ltiparams)) {
         $ltiparams{$key} = &Encode::decode_utf8($ltiparams{$key});
     }
+    $ltiparams{'basiclti_submit'} = $submittext;
     return %ltiparams;
 }
 
 sub launch_html {
-    my ($url,$key,$secret,$toolname,$paramsref) = @_;
+    my ($url,$key,$secret,$submittext,$paramsref) = @_;
     my $hashref = &sign_params($url,$key,$secret,$paramsref);
-    my $submittext = &mt('Launch [_1]',$toolname);
     my $form = <<"END";
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 <html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
 <body>
 <div id="LCltiLaunch">
-<form name="LCltiLaunchForm" action="$url" method="post" encType="application/x-www-form-urlencoded">
-<input type="submit" name="LCbasicltiSubmit" value="$submittext" />
+<form name="LCltiLaunchForm" id="LCltiLaunchFormId" action="$url" method="post" encType="application/x-www-form-urlencoded">
 END
     if (ref($hashref) eq 'HASH') {
         foreach my $item (keys(%{$hashref})) {
-            $form .= '<input type="hidden" name="'.$item.'" value="'.$hashref->{$item}.'" id="id_'.$item.'" />'."\n";
+            my $type = 'hidden';
+            if ($item eq 'basiclti_submit') {
+                $type = 'submit';
+            }
+            $form .= '<input type="'.$type.'" name="'.$item.'" value="'.$hashref->{$item}.'" id="id_'.$item.'" />'."\n";
         }
     }
     $form .= "</form></div>\n";
@@ -302,9 +306,9 @@
     document.getElementById("LCltiLaunch").style.display = "none";
     nei = document.createElement('input');
     nei.setAttribute('type','hidden');
-    nei.setAttribute('name','LCbasicltiSubmit');
+    nei.setAttribute('name','basiclti_submit');
     nei.setAttribute('value','$submittext');
-    document.getElementById("LCltiLaunchForm").appendChild(nei);
+    document.getElementById("LCltiLaunchFormId").appendChild(nei);
     document.LCltiLaunchForm.submit();
  </script>
 ENDJS
Index: loncom/interface/lonhtmlcommon.pm
diff -u loncom/interface/lonhtmlcommon.pm:1.369 loncom/interface/lonhtmlcommon.pm:1.370
--- loncom/interface/lonhtmlcommon.pm:1.369	Sun Aug 16 20:45:41 2015
+++ loncom/interface/lonhtmlcommon.pm	Tue Jan 26 14:30:25 2016
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # a pile of common html routines
 #
-# $Id: lonhtmlcommon.pm,v 1.369 2015/08/16 20:45:41 raeburn Exp $
+# $Id: lonhtmlcommon.pm,v 1.370 2016/01/26 14:30:25 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -1718,7 +1718,7 @@
             (($env{'request.noversionuri'}=~/^\/adm\//) &&
              ($env{'request.noversionuri'}!~/^\/adm\/wrapper\//) &&
              ($env{'request.noversionuri'}!~
-              m{^/adm/.*/(smppg|bulletinboard)($|\?)})
+              m{^/adm/.*/(smppg|bulletinboard|exttools?)($|\?)})
            ));
 }
 
Index: loncom/interface/lonmenu.pm
diff -u loncom/interface/lonmenu.pm:1.437 loncom/interface/lonmenu.pm:1.438
--- loncom/interface/lonmenu.pm:1.437	Mon Sep 14 13:45:01 2015
+++ loncom/interface/lonmenu.pm	Tue Jan 26 14:30:25 2016
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # Routines to control the menu
 #
-# $Id: lonmenu.pm,v 1.437 2015/09/14 13:45:01 raeburn Exp $
+# $Id: lonmenu.pm,v 1.438 2016/01/26 14:30:25 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -847,7 +847,7 @@
     $is_mobile = 1;
 }
 
-            unless ($env{'request.noversionuri'}=~/\/(bulletinboard|smppg|navmaps|syllabus|aboutme|viewclasslist|portfolio)(\?|$)/) {
+            unless ($env{'request.noversionuri'}=~/\/(bulletinboard|smppg|navmaps|syllabus|aboutme|viewclasslist|portfolio|exttools?)(\?|$)/) {
 		if ((!$env{'request.enc'}) && ($env{'request.noversionuri'} !~ m{^/adm/wrapper/ext/}) && ($env{'request.noversionuri'} !~ m{^/uploaded/$match_domain/$match_courseid/docs/})) {
 		    $menuitems.=(<<ENDREALRES);
 s&6&3&catalog.png&Info&info[_1]&catalog_info('$is_mobile')&Show Metadata
@@ -1125,6 +1125,7 @@
         if (($env{'form.folderpath'} =~ /^supplemental/) &&
             (&Apache::lonnet::allowed('mdc',$env{'request.course.id'})) &&
             (($resurl =~ m{^/adm/wrapper/ext/}) ||
+             ($resurl =~ m{^/adm/$cdom/$cnum/\d+/exttools?$}) ||
              ($resurl =~ m{^/uploaded/$cdom/$cnum/supplemental/}) ||
              ($resurl eq '/adm/supplemental') ||
              ($resurl =~ m{^/public/$cdom/$cnum/syllabus$}) ||
Index: loncom/interface/lonsyllabus.pm
diff -u loncom/interface/lonsyllabus.pm:1.138 loncom/interface/lonsyllabus.pm:1.139
--- loncom/interface/lonsyllabus.pm:1.138	Tue Jun  9 21:22:57 2015
+++ loncom/interface/lonsyllabus.pm	Tue Jan 26 14:30:25 2016
@@ -1,7 +1,7 @@
 # The LearningOnline Network
 # Syllabus
 #
-# $Id: lonsyllabus.pm,v 1.138 2015/06/09 21:22:57 damieng Exp $
+# $Id: lonsyllabus.pm,v 1.139 2016/01/26 14:30:25 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -164,7 +164,7 @@
                               '\end{document}');
                 } else {
                     $r->print(&Apache::lonwrapper::wrapper($item,undef,$env{'request.use_absolute'},
-                                                           undef,$is_pdf,&mt('Syllabus')));
+                                                           undef,$is_pdf,undef,&mt('Syllabus')));
                 }
             }
             return OK;
@@ -183,7 +183,7 @@
                     $is_pdf = 1;
                 }
                 $r->print(&Apache::lonwrapper::wrapper($external,undef,$env{'request.use_absolute'},
-                                                       $is_ext,$is_pdf,&mt('Syllabus')));
+                                                       $is_ext,$is_pdf,undef,&mt('Syllabus')));
             }
             return OK;
         }
Index: loncom/rewrites/loncapa_rewrite_off.conf
diff -u loncom/rewrites/loncapa_rewrite_off.conf:1.4 loncom/rewrites/loncapa_rewrite_off.conf:1.5
--- loncom/rewrites/loncapa_rewrite_off.conf:1.4	Tue Jan  7 20:03:32 2014
+++ loncom/rewrites/loncapa_rewrite_off.conf	Tue Jan 26 14:30:30 2016
@@ -11,6 +11,8 @@
     RewriteRule (.*) - [L]
     RewriteCond %{REQUEST_URI} ^/adm/jsMath/
     RewriteRule (.*) - [L]
+    RewriteCond %{REQUEST_URI} ^(/adm/wrapper|)/adm/.*/exttool$
+    RewriteRule (.*) - [L]
     RewriteCond %{HTTPS} !=on
     RewriteRule ^/(.*)$ https://%{HTTP_HOST}/$1 [R,L]
   </IfModule>
Index: loncom/rewrites/loncapa_rewrite_on.conf
diff -u loncom/rewrites/loncapa_rewrite_on.conf:1.4 loncom/rewrites/loncapa_rewrite_on.conf:1.5
--- loncom/rewrites/loncapa_rewrite_on.conf:1.4	Tue Jan  7 20:03:32 2014
+++ loncom/rewrites/loncapa_rewrite_on.conf	Tue Jan 26 14:30:30 2016
@@ -11,6 +11,8 @@
     RewriteRule (.*) - [L]
     RewriteCond %{REQUEST_URI} ^/adm/jsMath/
     RewriteRule (.*) - [L]
+    RewriteCond %{REQUEST_URI} ^(/adm/wrapper|)/adm/.*/exttool$
+    RewriteRule (.*) - [L]
     RewriteCond %{HTTPS} !=on
     RewriteRule ^/(.*)$ https://%{HTTP_HOST}/$1 [R,L]
   </IfModule>
Index: rat/lonuserstate.pm
diff -u rat/lonuserstate.pm:1.149 rat/lonuserstate.pm:1.150
--- rat/lonuserstate.pm:1.149	Mon Dec 15 01:10:19 2014
+++ rat/lonuserstate.pm	Tue Jan 26 14:30:40 2016
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # Construct and maintain state and binary representation of course for user
 #
-# $Id: lonuserstate.pm,v 1.149 2014/12/15 01:10:19 raeburn Exp $
+# $Id: lonuserstate.pm,v 1.150 2016/01/26 14:30:40 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -42,7 +42,7 @@
 use Opcode;
 use Apache::lonenc;
 use Fcntl qw(:flock);
-use LONCAPA;
+use LONCAPA qw(:DEFAULT :match);
 use File::Basename;
 
  
@@ -475,6 +475,8 @@
 	    } elsif ($turi!~/\.(sequence|page)$/) {
 		$turi='/adm/coursedocs/showdoc'.$turi;
 	    }
+        } elsif ($turi=~ m{^/adm/$match_domain/$match_courseid/\d+/exttools?$}) {
+            $turi='/adm/wrapper'.$turi;
 	} elsif ($turi=~/\S/) { # normal non-empty internal resource
 	    my $mapdir=$uri;
 	    $mapdir=~s/[^\/]+$//;
Index: rat/lonwrapper.pm
diff -u rat/lonwrapper.pm:1.49 rat/lonwrapper.pm:1.50
--- rat/lonwrapper.pm:1.49	Tue Jun 17 23:22:21 2014
+++ rat/lonwrapper.pm	Tue Jan 26 14:30:40 2016
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # Wrapper for external and binary files as standalone resources
 #
-# $Id: lonwrapper.pm,v 1.49 2014/06/17 23:22:21 raeburn Exp $
+# $Id: lonwrapper.pm,v 1.50 2016/01/26 14:30:40 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -37,16 +37,17 @@
 use Apache::loncommon();
 use Apache::lonhtmlcommon();
 use Apache::lonextresedit();
+use Apache::lonexttool();
+use LONCAPA qw(:DEFAULT :match);;
 
 # ================================================================ Main Handler
 sub wrapper {
-    my ($url,$brcrum,$absolute,$is_ext,$is_pdf,$title) = @_;
+    my ($url,$brcrum,$absolute,$is_ext,$is_pdf,$exttool,$title) = @_;
 
     my $forcereg;
     unless ($env{'form.folderpath'}) {
         $forcereg = 1;
     }
-
     my %lt = &Apache::lonlocal::texthash(
                                           'noif' => 'No iframe support.',
                                           'show' => 'Show content in pop-up window',
@@ -66,8 +67,8 @@
 
     my $startpage = &Apache::loncommon::start_page('Menu',undef,$args);
     my $endpage = &Apache::loncommon::end_page();
-
-    if ($env{'browser.mobile'}) {
+    
+    if (($env{'browser.mobile'}) || ($exttool eq 'window')) {
         my $output = $startpage;
         if ($is_pdf) {
             if ($title eq '') {
@@ -83,6 +84,11 @@
                 $output .= $title.'<br />';
             }
             $output .= '<a href="'.$url.'">'.&mt('Link to PDF (for mobile devices)').'</a>';
+        } elsif ($exttool eq 'window') {
+            $output .= '<div>'.
+                       '<a href="'.$url.'" target="LC_LTI" style="padding:0;clear:both;margin:0;border:0">'.
+                       &mt('Launch External Tool').'</a>'.
+                       '</div>';
         } else {
             $output .= '<div style="overflow:scroll; -webkit-overflow-scrolling:touch;">'."\n".
                        '<iframe src="'.$url.'" height="100%" width="100%" frameborder="0">'."\n".
@@ -138,7 +144,7 @@
     return OK if $r->header_only;
 
     my $url = $r->uri;
-    my ($is_ext,$brcrum,$absolute,$is_pdf);
+    my ($is_ext,$brcrum,$absolute,$is_pdf,$exttool,$cdom,$cnum);
 
     for ($url){
         s|^/adm/wrapper||;
@@ -147,27 +153,47 @@
         s|:|:|g;              
     }
 
+
     if ($url =~ /\.pdf$/i) {
         $is_pdf = 1;
+    } elsif ($url =~ m{^/adm/($match_domain)/($match_courseid)/(\d+)/exttools?$}) {
+        $cdom = $1;
+        $cnum = $2;
+        my $marker = $3;
+        $exttool = 'iframe';
+        my %toolhash = &Apache::lonnet::get('exttool_'.$marker,['target'],$cdom,$cnum);
+        if ($toolhash{'target'} eq 'window') {
+           $exttool = 'window'; 
+        }
     }
- 
-    if ($is_ext) {
+    if (($is_ext) || ($exttool)) {
         &Apache::loncommon::get_unprocessed_cgi($ENV{'QUERY_STRING'},
             ['forceedit','register','folderpath','symb','idx','title']);
         if (($env{'form.forceedit'}) &&
             (&Apache::lonnet::allowed('mdc',$env{'request.course.id'})) &&
             (($env{'form.folderpath'} =~ /^supplemental/) ||
              ($env{'form.symb'} =~ /^uploaded/))) {
+            my $type = 'ext';
+            my %ltitools;
+            if ($exttool) {
+                $type = 'tool';
+                %ltitools = &Apache::lonnet::get_domain_ltitools($cdom);
+            }
             $r->print(
                 &Apache::lonextresedit::display_editor($url,$env{'form.folderpath'},
                                                        $env{'form.symb'},
-                                                       $env{'form.idx'}));
+                                                       $env{'form.idx'},$type,$cdom,
+                                                       $cnum,\%ltitools));
             return OK;
         } elsif ($env{'form.folderpath'} =~ /^supplemental/) {
             my $crstype = &Apache::loncommon::course_type();
             my $title = $env{'form.title'};
             if ($title eq '') {
-                $title = &mt('External Resource');
+                if ($is_ext) {
+                    $title = &mt('External Resource');
+                } else {
+                    $title = &mt('External Tool');
+                }
             }
             $brcrum =
                 &Apache::lonhtmlcommon::docs_breadcrumbs(undef,$crstype,undef,$title,1);
@@ -187,7 +213,7 @@
 #
 # This is not homework
 #
-        if ($is_ext) {
+        if (($is_ext) || ($exttool)) {
             $absolute = $env{'request.use_absolute'};
             $ENV{'QUERY_STRING'} =~ s/(^|\&)symb=[^\&]*/$1/;
             $ENV{'QUERY_STRING'} =~ s/\&$//;
@@ -198,9 +224,11 @@
         }
 
         # encrypt url if not external
-        &Apache::lonenc::check_encrypt(\$url) if $url !~ /^https?\:/ ;
+        unless ($is_ext || $exttool) {
+            &Apache::lonenc::check_encrypt(\$url);
+        }
 
-        $r->print( wrapper($url,$brcrum,$absolute,$is_ext,$is_pdf) );
+        $r->print( wrapper($url,$brcrum,$absolute,$is_ext,$is_pdf,$exttool) );
 
     } # not just the menu
     
Index: doc/loncapafiles/loncapafiles.lpml
diff -u doc/loncapafiles/loncapafiles.lpml:1.926 doc/loncapafiles/loncapafiles.lpml:1.927
--- doc/loncapafiles/loncapafiles.lpml:1.926	Tue Jan 12 19:07:12 2016
+++ doc/loncapafiles/loncapafiles.lpml	Tue Jan 26 14:30:51 2016
@@ -2,7 +2,7 @@
  "http://lpml.sourceforge.net/DTD/lpml.dtd">
 <!-- loncapafiles.lpml -->
 
-<!-- $Id: loncapafiles.lpml,v 1.926 2016/01/12 19:07:12 damieng Exp $ -->
+<!-- $Id: loncapafiles.lpml,v 1.927 2016/01/26 14:30:51 raeburn Exp $ -->
 
 <!--
 
@@ -2511,6 +2511,15 @@
 <status>works/unverified</status>
 </file>
 <file>
+<source>loncom/interface/lonexttool.pm</source>
+<target dist='default'>home/httpd/lib/perl/Apache/lonexttool.pm</target>
+<categoryname>handler</categoryname>
+<description>
+Handler to allow LON-CAPA to operate as an LTI Tool Consumer 
+</description>
+<status>works/unverified</status>
+</file>
+<file>
   <source>loncom/interface/lonpickcode.pm</source>
   <target dist='default'>home/httpd/lib/perl/Apache/lonpickcode.pm</target>
   <categoryname>handler</categoryname>


More information about the LON-CAPA-cvs mailing list