[LON-CAPA-cvs] cvs: doc /loncapafiles loncapafiles.lpml loncom loncapa_apache.conf lontrans.pm loncom/auth lonlti.pm migrateuser.pm switchserver.pm loncom/interface domainprefs.pm lonconfigsettings.pm loncom/lonnet/perl lonnet.pm

raeburn raeburn at source.lon-capa.org
Thu Nov 30 10:15:23 EST 2017


raeburn		Thu Nov 30 15:15:23 2017 EDT

  Added files:                 
    /loncom/auth	lonlti.pm 

  Modified files:              
    /loncom/auth	migrateuser.pm switchserver.pm 
    /loncom	loncapa_apache.conf lontrans.pm 
    /loncom/lonnet/perl	lonnet.pm 
    /loncom/interface	domainprefs.pm lonconfigsettings.pm 
    /doc/loncapafiles	loncapafiles.lpml 
  Log:
  - Bug 6754 LTI Integration: LON-CAPA as LTI Provider
    - Work in progress
  
  
-------------- next part --------------
Index: loncom/auth/migrateuser.pm
diff -u loncom/auth/migrateuser.pm:1.25 loncom/auth/migrateuser.pm:1.26
--- loncom/auth/migrateuser.pm:1.25	Mon Dec  5 00:51:43 2016
+++ loncom/auth/migrateuser.pm	Thu Nov 30 15:14:51 2017
@@ -1,7 +1,7 @@
 # The LearningOnline Network
 # Starts a user off based of an existing token.
 #
-# $Id: migrateuser.pm,v 1.25 2016/12/05 00:51:43 raeburn Exp $
+# $Id: migrateuser.pm,v 1.26 2017/11/30 15:14:51 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -53,7 +53,7 @@
 }
 
 
-sub sso_check {
+sub sso_lti_check {
     my ($data) = @_;
     my %extra_env;
     if (ref($data) eq 'HASH') {
@@ -64,6 +64,21 @@
             $extra_env{'request.sso.reloginserver'} = 
                 $data->{'sso.reloginserver'};
         }
+        if ($data->{'lti.login'}) {
+            $extra_env{'request.lti.login'} = $data->{'lti.login'};
+        }
+        if ($data->{'lti.passbackid'}) {
+            $extra_env{'request.lti.passbackid'} = $data->{'lti.passbackid'};
+        }
+        if ($data->{'lti.passbackurl'}) {
+            $extra_env{'request.lti.passbackurl'} = $data->{'lti.passbackurl'};
+        }
+        if ($data->{'lti.rosterid'}) {
+            $extra_env{'request.lti.rosterid'} = $data->{'lti.rosterid'};
+        }
+        if ($data->{'lti.rosterurl'}) {
+            $extra_env{'request.lti.rosterurl'} = $data->{'lti.rosterurl'};
+        }
     }
     return \%extra_env;
 }
@@ -243,7 +258,7 @@
     }
     if ($home =~ /(con_lost|no_such_host)/) { return &goto_login($r,$udom); }
 
-    my $extra_env = &sso_check(\%data);
+    my $extra_env = &sso_lti_check(\%data);
 
     my %form;
     if ($data{'symb'} ne '') {
Index: loncom/auth/switchserver.pm
diff -u loncom/auth/switchserver.pm:1.37 loncom/auth/switchserver.pm:1.38
--- loncom/auth/switchserver.pm:1.37	Tue Aug  8 20:10:55 2017
+++ loncom/auth/switchserver.pm	Thu Nov 30 15:14:51 2017
@@ -1,7 +1,7 @@
 # The LearningOnline Network
 # Switch Servers Handler
 #
-# $Id: switchserver.pm,v 1.37 2017/08/08 20:10:55 raeburn Exp $
+# $Id: switchserver.pm,v 1.38 2017/11/30 15:14:51 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -228,6 +228,21 @@
     if ($env{'request.sso.reloginserver'}) {
         $info{'sso.reloginserver'} = $env{'request.sso.reloginserver'};
     }
+    if ($env{'request.lti.login'}) {
+        $info{'lti.login'} = $env{'request.lti.login'};
+    }
+    if ($env{'request.lti.passbackid'}) {
+        $info{'lti.passbackid'} = $env{'request.lti.passbackid'};
+    }
+    if ($env{'request.lti.passbackurl'}) {
+        $info{'lti.passbackurl'} = $env{'request.lti.passbackurl'};
+    }
+    if ($env{'request.lti.rosterid'}) {
+        $info{'lti.rosterid'} = $env{'request.lti.rosterid'};
+    }
+    if ($env{'request.lti.rosterurl'}) {
+        $info{'lti.rosterurl'} = $env{'request.lti.rosterurl'};
+    }
     my $token = &Apache::lonnet::tmpput(\%info,$env{'form.otherserver'});
     my $url =$protocol.'://'.$switch_to.'/adm/login?'.
 	'domain='.$env{'user.domain'}.
Index: loncom/loncapa_apache.conf
diff -u loncom/loncapa_apache.conf:1.257 loncom/loncapa_apache.conf:1.258
--- loncom/loncapa_apache.conf:1.257	Tue Jun  6 13:37:04 2017
+++ loncom/loncapa_apache.conf	Thu Nov 30 15:14:58 2017
@@ -2,7 +2,7 @@
 ## loncapa_apache.conf -- Apache HTTP LON-CAPA configuration file
 ##
 
-# $Id: loncapa_apache.conf,v 1.257 2017/06/06 13:37:04 raeburn Exp $
+# $Id: loncapa_apache.conf,v 1.258 2017/11/30 15:14:58 raeburn Exp $
 
 #
 # LON-CAPA Section (extensions to httpd.conf daemon configuration)
@@ -753,6 +753,11 @@
 PerlHandler Apache::lonlogin
 </Location>
 
+<LocationMatch "^/+adm/lti($|/)">
+SetHandler perl-script
+PerlHandler Apache::lonlti
+</LocationMatch>
+
 <Location /adm/restrictedaccess>
 PerlAccessHandler      Apache::publiccheck
 AuthType LONCAPA
@@ -1725,6 +1730,7 @@
 PerlSetVar       lonZipDir    /home/httpd/zipspool
 PerlSetVar       lonCaptchaDir     /home/httpd/captchaspool
 PerlSetVar       lonCaptchaDb     /home/httpd/captchadb 
+PerlSetVar       lonLTIDir    /home/httpd/lonLTItmp
 PerlSetVar       lonFontsDir     /home/httpd/html/adm/fonts
 # & separated list of % separated fields in order of
 # - internal name to call it, 
Index: loncom/lontrans.pm
diff -u loncom/lontrans.pm:1.16 loncom/lontrans.pm:1.17
--- loncom/lontrans.pm:1.16	Tue Aug 16 20:17:54 2016
+++ loncom/lontrans.pm	Thu Nov 30 15:14:58 2017
@@ -1,7 +1,7 @@
 # The LearningOnline Network
 # URL translation for User Files
 #
-# $Id: lontrans.pm,v 1.16 2016/08/16 20:17:54 raeburn Exp $
+# $Id: lontrans.pm,v 1.17 2017/11/30 15:14:58 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -38,7 +38,34 @@
     my $r = shift;
     # FIXME line remove when mod_perl fixes BUG#4948 
     $r->notes->set('error-notes' => '');
-    if ($r->uri=~m{^/raw/}) {
+    if ($r->uri =~ m{^/adm/lti/(.+)$}) {
+        my $realuri = $1;
+        my %user;
+        my $handle = &Apache::lonnet::check_for_valid_session($r,undef,\%user);
+        if (($handle ne '') && ($user{'lti'})) {
+            if ($realuri =~ m{^uploaded/$match_domain/$match_courseid/(default|supplemental)(|_\d+)\.(?:sequence|page)___\d+___.+$}) {
+                my ($map,$resid,$url) = split(/___/,$realuri);
+                $realuri = &Apache::lonnet::clutter($url).'?symb='.$realuri;
+            } elsif ($realuri =~ m{($match_domain)/($match_courseid)$}) {
+                $realuri = '/adm/navmaps';
+            } else {
+                $realuri = '/'.$realuri;
+                if ($realuri =~ m{/default_\d+\.sequence$}) {
+                    $realuri .= (($realuri =~/\?/)?'&':'?').'navmap=1';
+                }
+            }
+            my $host = $r->headers_in->get('Host');
+            if ($host) {
+                my $protocol = 'http';
+                if ($r->get_server_port == 443) {
+                    $protocol = 'https';
+                }
+                my $location = $protocol.'://'.$host.$realuri;
+                $r->headers_out->set(Location => $location);
+                return REDIRECT;
+            }
+        }
+    } elsif ($r->uri=~m{^/raw/}) {
         my $host = $r->headers_in->get('Host');
         if ($host) {
             unless ($host =~ /^internal\-/) {
Index: loncom/lonnet/perl/lonnet.pm
diff -u loncom/lonnet/perl/lonnet.pm:1.1360 loncom/lonnet/perl/lonnet.pm:1.1361
--- loncom/lonnet/perl/lonnet.pm:1.1360	Thu Nov 30 14:41:38 2017
+++ loncom/lonnet/perl/lonnet.pm	Thu Nov 30 15:15:06 2017
@@ -1,7 +1,7 @@
 # The LearningOnline Network
 # TCP networking package
 #
-# $Id: lonnet.pm,v 1.1360 2017/11/30 14:41:38 raeburn Exp $
+# $Id: lonnet.pm,v 1.1361 2017/11/30 15:15:06 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -707,6 +707,7 @@
     if (ref($userhashref) eq 'HASH') {
         $userhashref->{'name'} = $disk_env{'user.name'};
         $userhashref->{'domain'} = $disk_env{'user.domain'};
+        $userhashref->{'lti'} = $disk_env{'request.lti.login'};
     }
 
     return $handle;
Index: loncom/interface/domainprefs.pm
diff -u loncom/interface/domainprefs.pm:1.319 loncom/interface/domainprefs.pm:1.320
--- loncom/interface/domainprefs.pm:1.319	Thu Nov 30 02:17:50 2017
+++ loncom/interface/domainprefs.pm	Thu Nov 30 15:15:14 2017
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # Handler to set domain-wide configuration settings
 #
-# $Id: domainprefs.pm,v 1.319 2017/11/30 02:17:50 raeburn Exp $
+# $Id: domainprefs.pm,v 1.320 2017/11/30 15:15:14 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -219,13 +219,14 @@
                 'serverstatuses','requestcourses','helpsettings',
                 'coursedefaults','usersessions','loadbalancing',
                 'requestauthor','selfenrollment','inststatus',
-                'ltitools','ssl','trust'],$dom);
+                'ltitools','ssl','trust','lti'],$dom);
+    my %encconfig =
+        &Apache::lonnet::get_dom('encconfig',['ltitools','lti'],$dom);
     if (ref($domconfig{'ltitools'}) eq 'HASH') {
-        my %encconfig =
-            &Apache::lonnet::get_dom('encconfig',['ltitools'],$dom);
         if (ref($encconfig{'ltitools'}) eq 'HASH') {
             foreach my $id (keys(%{$domconfig{'ltitools'}})) {
-                if (ref($domconfig{'ltitools'}{$id}) eq 'HASH') {
+                if ((ref($domconfig{'ltitools'}{$id}) eq 'HASH') &&
+                    (ref($encconfig{'ltitools'}{$id}) eq 'HASH')) {
                     foreach my $item ('key','secret') {
                         $domconfig{'ltitools'}{$id}{$item} = $encconfig{'ltitools'}{$id}{$item};
                     }
@@ -233,12 +234,24 @@
             }
         }
     }
+    if (ref($domconfig{'lti'}) eq 'HASH') {
+        if (ref($encconfig{'lti'}) eq 'HASH') {
+            foreach my $id (keys(%{$domconfig{'lti'}})) {
+                if ((ref($domconfig{'lti'}{$id}) eq 'HASH') &&
+                    (ref($encconfig{'lti'}{$id}) eq 'HASH')) {
+                    foreach my $item ('key','secret') {
+                        $domconfig{'lti'}{$id}{$item} = $encconfig{'lti'}{$id}{$item};
+                    }
+                }
+            }
+        }
+    }
     my @prefs_order = ('rolecolors','login','defaults','quotas','autoenroll',
                        'autoupdate','autocreate','directorysrch','contacts',
                        'usercreation','selfcreation','usermodification','scantron',
                        'requestcourses','requestauthor','coursecategories',
                        'serverstatuses','helpsettings','coursedefaults',
-                       'ltitools','selfenrollment','usersessions','ssl','trust');
+                       'ltitools','selfenrollment','usersessions','ssl','trust','lti');
     my %existing;
     if (ref($domconfig{'loadbalancing'}) eq 'HASH') {
         %existing = %{$domconfig{'loadbalancing'}};
@@ -545,6 +558,14 @@
                   print => \&print_trust,
                   modify => \&modify_trust,
                  },
+          'lti' =>
+                 {text => 'LTI Provider',
+                  help => 'Domain_Configuration_LTI_Provider',
+                  header => [{col1 => 'Setting',
+                              col2 => 'Value',}],
+                  print => \&print_lti,
+                  modify => \&modify_lti,
+                 },
     );
     if (keys(%servers) > 1) {
         $prefs{'login'}  = { text   => 'Log-in page options',
@@ -730,6 +751,8 @@
         $output = &modify_ssl($dom,$lastactref,%domconfig);
     } elsif ($action eq 'trust') {
         $output = &modify_trust($dom,$lastactref,%domconfig);
+    } elsif ($action eq 'lti') {
+        $output = &modify_lti($r,$dom,$action,$lastactref,%domconfig);
     }
     return $output;
 }
@@ -1065,7 +1088,7 @@
             $output .= &print_quotas($dom,$settings,\$rowtotal,$action);
         } elsif (($action eq 'autoenroll') || ($action eq 'autocreate') || 
                  ($action eq 'serverstatuses') || ($action eq 'loadbalancing') || 
-                 ($action eq 'ltitools')) {
+                 ($action eq 'ltitools') || ($action eq 'lti')) {
             $output .= $item->{'print'}->($dom,$settings,\$rowtotal);
         } elsif ($action eq 'scantron') {
             $output .= &print_scantronformat($r,$dom,$confname,$settings,\$rowtotal);
@@ -2697,6 +2720,165 @@
 ENDSCRIPT
 }
 
+sub lti_javascript {
+    my ($settings) = @_;
+    my $togglejs = &lti_toggle_js();
+    unless (ref($settings) eq 'HASH') {
+        return $togglejs;
+    }
+    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 lti = Array('."'".join("','", at jsarray)."'".');'."\n";
+    return <<"ENDSCRIPT";
+<script type="text/javascript">
+// <![CDATA[
+function reorderLTI(form,item) {
+    var changedVal;
+$jstext
+    var newpos = 'lti_pos_add';
+    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<lti.length; i++) {
+        var elementName = 'lti_pos_'+lti[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>
+
+$togglejs
+
+ENDSCRIPT
+}
+
+sub lti_toggle_js {
+    return <<"ENDSCRIPT";
+<script type="text/javascript">
+// <![CDATA[
+
+function toggleLTI(form,setting,item) {
+    if ((setting == 'user') || (setting == 'crs')) {
+        var radioname = '';
+        var divid = '';
+        if (setting == 'user') {
+            radioname = 'lti_mapuser_'+item;
+            divid = 'lti_userfield_'+item;
+        } else {
+            radioname = 'lti_mapcrs_'+item;
+            divid = 'lti_crsfield_'+item;
+        }
+        var num = form.elements[radioname].length;
+        if (num) {
+            var setvis = '';
+            for (var i=0; i<num; i++) {
+               if (form.elements[radioname][i].checked) {
+                   if (form.elements[radioname][i].value == 'other') {
+                       if (document.getElementById(divid)) {
+                           document.getElementById(divid).style.display = 'inline-block';
+                       }
+                       setvis = 1;
+                       break;
+                   }
+               } 
+            }
+            if (!setvis) {
+                if (document.getElementById(divid)) {
+                    document.getElementById(divid).style.display = 'none';
+                }
+            }
+        }
+    } else if ((setting == 'sec') || (setting == 'secsrc')) {
+        var numsec = form.elements['lti_crssec_'+item].length;
+        if (numsec) {
+            var setvis = '';
+            for (var i=0; i<numsec; i++) {
+                if (form.elements['lti_crssec_'+item][i].checked) {
+                    if (form.elements['lti_crssec_'+item][i].value == '1') {
+                        if (document.getElementById('lti_crssecfield_'+item)) {
+                            document.getElementById('lti_crssecfield_'+item).style.display = 'inline-block';
+                            setvis = 1;
+                            var numsrcsec = form.elements['lti_crssecsrc_'+item].length;
+                            if (numsrcsec) {
+                                var setsrcvis = '';
+                                for (var j=0; j<numsrcsec; j++) {
+                                    if (form.elements['lti_crssecsrc_'+item][j].checked) {
+                                        if (form.elements['lti_crssecsrc_'+item][j].value == 'other') {
+                                            if (document.getElementById('lti_secsrcfield_'+item)) {
+                                                document.getElementById('lti_secsrcfield_'+item).style.display = 'inline-block';
+                                                setsrcvis = 1;
+                                            }
+                                        }
+                                    }
+                                }
+                                if (!setsrcvis) {
+                                    if (document.getElementById('lti_secsrcfield_'+item)) {
+                                        document.getElementById('lti_secsrcfield_'+item).style.display = 'none';
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            if (!setvis) {
+                if (document.getElementById('lti_crssecfield_'+item)) {
+                    document.getElementById('lti_crssecfield_'+item).style.display = 'none';
+                }
+                if (document.getElementById('lti_secsrcfield_'+item)) {
+                    document.getElementById('lti_secsrcfield_'+item).style.display = 'none';
+                }
+            }
+        }
+    }
+    return;
+}
+// ]]>
+</script>
+
+ENDSCRIPT
+}
+
 sub print_autoenroll {
     my ($dom,$settings,$rowtotal) = @_;
     my $autorun = &Apache::lonnet::auto_run(undef,$dom),
@@ -4298,6 +4480,327 @@
     return %lt;
 }
 
+sub print_lti {
+    my ($dom,$settings,$rowtotal) = @_;
+    my $itemcount = 1;
+    my $maxnum = 0;
+    my $css_class;
+    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 $maxnum = scalar(keys(%ordered));
+    my $datatable = &lti_javascript($settings);
+    my %lt = &lti_names();
+    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 ($key,$secret,$lifetime,$consumer,$current);
+            if (ref($settings->{$item}) eq 'HASH') {
+                $key = $settings->{$item}->{'key'};
+                $secret = $settings->{$item}->{'secret'};
+                $lifetime = $settings->{$item}->{'lifetime'};
+                $consumer = $settings->{$item}->{'consumer'};
+                $current = $settings->{$item};
+            }
+            my $chgstr = ' onchange="javascript:reorderLTI(this.form,'."'lti_pos_".$item."'".');"';
+            $datatable .= '<tr '.$css_class.'><td><span class="LC_nobreak">'
+                         .'<select name="lti_pos_'.$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="lti_del" value="'.$item.'" />'.
+                &mt('Delete?').'</label></span></td>'.
+                '<td colspan="2">'.
+                '<fieldset><legend>'.&mt('Required settings').'</legend>'.
+                '<span class="LC_nobreak">'.$lt{'consumer'}.
+                ':<input type="text" size="20" name="lti_consumer_'.$i.'" value="'.$consumer.'" /></span> '.
+                (' 'x2).
+                '<span class="LC_nobreak">'.$lt{'version'}.':<select name="lti_version_'.$i.'">'.
+                '<option value="LTI-1p0" selected="selected">1.1</option></select></span> '.
+                (' 'x2).
+                '<span class="LC_nobreak">'.$lt{'lifetime'}.':<input type="text" name="lti_lifetime_'.$i.'"'.
+                'value="'.$lifetime.'" size="5" /></span>'.
+                '<br /><br />'.
+                '<span class="LC_nobreak">'.$lt{'key'}.
+                ':<input type="text" size="25" name="lti_key_'.$i.'" value="'.$key.'" /></span> '.
+                (' 'x2).
+                '<span class="LC_nobreak">'.$lt{'secret'}.':'.
+                '<input type="password" size="20" name="lti_secret_'.$i.'" value="'.$secret.'" />'.
+                '<label><input type="checkbox" name="visible" onclick="if (this.checked) { this.form.lti_secret_'.$i.'.type='."'text'".' } else { this.form.lti_secret_'.$i.'.type='."'password'".' }" />'.&mt('Visible input').'</label>'.
+                '<input type="hidden" name="lti_id_'.$i.'" value="'.$item.'" /></span>'.
+                '</fieldset>'.&lti_options($i,$current,%lt).'</td></tr>';
+            $itemcount ++;
+        }
+    }
+    $css_class = $itemcount%2?' class="LC_odd_row"':'';
+    my $chgstr = ' onchange="javascript:reorderLTI(this.form,'."'lti_pos_add'".');"';
+    $datatable .= '<tr '.$css_class.'><td><span class="LC_nobreak">'."\n".
+                  '<input type="hidden" name="lti_maxnum" value="'.$maxnum.'" />'."\n".
+                  '<select name="lti_pos_add"'.$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="lti_add" value="1" />'.&mt('Add').'</td>'."\n".
+                  '<td colspan="2">'.
+                  '<fieldset><legend>'.&mt('Required settings').'</legend>'.
+                  '<span class="LC_nobreak">'.$lt{'consumer'}.
+                  ':<input type="text" size="20" name="lti_consumer_add" value="" /></span> '."\n".
+                  (' 'x2).
+                  '<span class="LC_nobreak">'.$lt{'version'}.':<select name="lti_version_add">'.
+                  '<option value="LTI-1p0" selected="selected">1.1</option></select></span> '."\n".
+                  (' 'x2).
+                  '<span class="LC_nobreak">'.$lt{'lifetime'}.':<input type="text" size="5" name="lti_lifetime_add" value="" /></span> '."\n".
+                  '<br /><br />'.
+                  '<span class="LC_nobreak">'.$lt{'key'}.':<input type="text" size="25" name="lti_key_add" value="" /></span> '."\n".
+                  (' 'x2).
+                  '<span class="LC_nobreak">'.$lt{'secret'}.':<input type="password" size="20" name="lti_secret_add" value="" />'.
+                  '<label><input type="checkbox" name="visible" onclick="if (this.checked) { this.form.lti_secret_add.type='."'text'".' } else { this.form.lti_secret_add.type='."'password'".' }" />'.&mt('Visible input').'</label></span> '."\n".
+                  '</fieldset>'.&lti_options('add',undef,%lt).
+                  '</td>'."\n".
+                  '</tr>'."\n";
+    $$rowtotal ++;
+    return $datatable;;
+}
+
+sub lti_names {
+    my %lt = &Apache::lonlocal::texthash(
+                                          'version'   => 'LTI Version',
+                                          'url'       => 'URL',
+                                          'key'       => 'Key',
+                                          'lifetime'  => 'Nonce lifetime (seconds)',
+                                          'consumer'  => 'LTI Consumer', 
+                                          'secret'    => 'Secret',
+                                          'email'     => 'Email address',
+                                          'sourcedid' => 'User ID',
+                                          'other'     => 'Other',
+                                          'passback'  => 'Can return grades to Consumer:',
+                                          'roster'    => 'Can retrieve roster from Consumer:',
+                                        );
+    return %lt;
+}
+
+sub lti_options {
+    my ($num,$current,%lt) = @_;
+    my (%checked,%rolemaps,$crssecsrc,$userfield,$cidfield);
+    $checked{'mapuser'}{'sourcedid'} = ' checked="checked"';
+    $checked{'mapcrs'}{'course_offering_sourcedid'} = ' checked="checked"';
+    $checked{'makecrs'}{'N'} = '  checked="checked"';
+    $checked{'mapcrstype'} = {};
+    $checked{'makeuser'} = {};
+    $checked{'selfenroll'} = {};
+    $checked{'crssec'} = {};
+    $checked{'crssecsrc'} = {};
+
+    my $userfieldsty = 'none';
+    my $crsfieldsty = 'none';
+    my $crssecfieldsty = 'none';
+    my $secsrcfieldsty = 'none';
+
+    if (ref($current) eq 'HASH') {
+        if (($current->{'mapuser'} ne '') && ($current->{'mapuser'} ne 'lis_person_sourcedid')) {
+            $checked{'mapuser'}{'sourcedid'} = '';
+            if ($current->{'mapuser'} eq 'lis_person_contact_email_primary') {
+                $checked{'mapuser'}{'email'} = ' checked="checked"'; 
+            } else {
+                $checked{'mapuser'}{'other'} = ' checked="checked"';
+                $userfield = $current->{'mapuser'};
+                $userfieldsty = 'inline-block';
+            }
+        }
+        if (($current->{'mapcrs'} ne '') && ($current->{'mapcrs'} ne 'course_offering_sourcedid')) {
+            $checked{'mapcrs'}{'course_offering_sourcedid'} = '';
+            if ($current->{'mapcrs'} eq 'context_id') {
+                $checked{'mapcrs'}{'context_id'} = ' checked="checked"'; 
+            } else {
+                $checked{'mapcrs'}{'other'} = ' checked="checked"';
+                $cidfield = $current->{'mapcrs'};
+                $crsfieldsty = 'inline-block';
+            }
+        }
+        if (ref($current->{'mapcrstype'}) eq 'ARRAY') {
+            foreach my $type (@{$current->{'mapcrstype'}}) {
+                $checked{'mapcrstype'}{$type} = ' checked="checked"';
+            }
+        }
+        if ($current->{'makecrs'}) { 
+            $checked{'makecrs'}{'Y'} = '  checked="checked"';
+        } 
+        if (ref($current->{'makeuser'}) eq 'ARRAY') {
+            foreach my $role (@{$current->{'makeuser'}}) {
+                $checked{'makeuser'}{$role} = ' checked="checked"';
+            }
+        }
+        if (ref($current->{'selfenroll'}) eq 'ARRAY') {
+            foreach my $role (@{$current->{'selfenroll'}}) {
+                $checked{'selfenroll'}{$role} = ' checked="checked"';
+            }
+        }
+        if (ref($current->{'maproles'}) eq 'HASH') {
+            %rolemaps = %{$current->{'maproles'}};
+        }
+        if ($current->{'section'} ne '') {
+            $checked{'crssec'}{'Y'} = '  checked="checked"'; 
+            $crssecfieldsty = 'inline-block';
+            if ($current->{'section'} eq 'course_section_sourcedid') {
+                $checked{'crssecsrc'}{'sourcedid'} = ' checked="checked"';
+            } else {
+                $checked{'crssecsrc'}{'other'} = ' checked="checked"';
+                $crssecsrc = $current->{'section'};
+                $secsrcfieldsty = 'inline-block';
+            }
+        } else {
+            $checked{'crssec'}{'N'} = ' checked="checked"';
+        }
+    } else {
+        $checked{'makecrs'}{'N'} = ' checked="checked"';
+        $checked{'crssec'}{'N'} = ' checked="checked"';
+    }
+    my @coursetypes = ('official','unofficial','community','textbook','placement');
+    my %coursetypetitles = &Apache::lonlocal::texthash (
+                               official   => 'Official',
+                               unofficial => 'Unofficial',
+                               community  => 'Community',
+                               textbook   => 'Textbook',
+                               placement  => 'Placement Test',
+    );
+    my @ltiroles = qw(Learner Instructor ContentDeveloper TeachingAssistant Mentor Member Manager Administrator);
+    my @lticourseroles = qw(Learner Instructor TeachingAssistant Mentor);
+    my @courseroles = ('cc','in','ta','ep','st');
+    my $onclickuser = ' onclick="toggleLTI(this.form,'."'user','$num'".');"';
+    my $onclickcrs = ' onclick="toggleLTI(this.form,'."'crs','$num'".');"';
+    my $onclicksec = ' onclick="toggleLTI(this.form,'."'sec','$num'".');"';
+    my $onclicksecsrc = ' onclick="toggleLTI(this.form,'."'secsrc','$num'".')"';
+    my $output = '<fieldset><legend>'.&mt('Mapping users').'</legend>'.
+                 '<div class="LC_floatleft"><span class="LC_nobreak">'.&mt('LON-CAPA username').': ';
+    foreach my $option ('sourcedid','email','other') {
+        $output .= '<label><input type="radio" name="lti_mapuser_'.$num.'" value="'.$option.'"'.
+                   $checked{'mapuser'}{$option}.$onclickuser.' />'.$lt{$option}.'</label>'.
+                   ($option eq 'other' ? '' : (' 'x2) );
+    }
+    $output .= '</span></div>'.
+               '<div class="LC_floatleft" style="display:'.$userfieldsty.';" id="lti_userfield_'.$num.'">'.
+               '<input type="text" name="lti_customuser_'.$num.'" '.
+               'value="'.$userfield.'" /></div></fieldset>'. 
+               '<fieldset><legend>'.&mt('Mapping course roles').'</legend><table><tr>';
+    foreach my $ltirole (@lticourseroles) {
+        my ($selected,$selectnone);
+        if ($rolemaps{$ltirole} eq '') {
+            $selectnone = ' selected="selected"';
+        }
+        $output .= '<td style="text-align: center">'.$ltirole.'<br />'.
+                   '<select name="lti_maprole_'.$ltirole.'_'.$num.'">'.
+                   '<option value=""'.$selectnone.'>'.&mt('Select').'</option>';
+        foreach my $role (@courseroles) {
+            unless ($selectnone) {
+                if ($rolemaps{$ltirole} eq $role) {
+                    $selected = ' selected="selected"';
+                } else {
+                    $selected = '';
+                }
+            }
+            $output .= '<option value="'.$role.'"'.$selected.'>'.
+                       &Apache::lonnet::plaintext($role,'Course').
+                       '</option>';
+        }
+        $output .= '</select></td>';
+    }
+    $output .= '</tr></table></fieldset>'.
+               '<fieldset><legend>'.&mt('Roles which may create user accounts').'</legend>';
+    foreach my $ltirole (@ltiroles) {
+        $output .= '<span class="LC_nobreak"><label><input type="checkbox" name="lti_makeuser_'.$num.'" value="'.$ltirole.'"'.
+                   $checked{'makeuser'}{$ltirole}.' />'.$ltirole.'</label> </span> ';     
+    }
+    $output .= '</fieldset>'.
+               '<fieldset><legend>'.&mt('Mapping courses').'</legend>'.
+               '<div class="LC_floatleft"><span class="LC_nobreak">'.
+               &mt('Unique course identifier').': ';
+    foreach my $option ('course_offering_sourcedid','context_id','other') {
+        $output .= '<label><input type="radio" name="lti_mapcrs_'.$num.'" value="'.$option.'"'.
+                   $checked{'mapcrs'}{$option}.$onclickcrs.' />'.$option.'</label>'.
+                   ($option eq 'other' ? '' : (' 'x2) );
+    }
+    $output .= '</div><div class="LC_floatleft" style="display:'.$crsfieldsty.';" id="lti_crsfield_'.$num.'".>'.
+               '<input type="text" name="lti_mapcrsfield_'.$num.'" value="'.$cidfield.'" />'.
+               '</div><div style="padding:0;clear:both;margin:0;border:0"></div>'.
+               '<span class="LC_nobreak">'.&mt('LON-CAPA course type(s)').': ';
+    foreach my $type (@coursetypes) {
+        $output .= '<label><input type="checkbox" name="lti_mapcrstype_'.$num.'" value="'.$type.'"'.
+                   $checked{'mapcrstype'}{$type}.' />'.$coursetypetitles{$type}.'</label>'.
+                   (' 'x2);
+    }
+    $output .= '</span></fieldset>'.
+               '<fieldset><legend>'.&mt('Creating courses').'</legend>'.
+               '<span class="LC_nobreak">'.&mt('Course created (if absent) on Instructor access').': '.
+               '<label><input type="radio" name="lti_makecrs_'.$num.'" value="0"'.
+               $checked{'makecrs'}{'N'}.' />'.&mt('No').'</label>'.(' 'x2).
+               '<label><input type="radio" name="lti_makecrs_'.$num.'" value="1"'.
+               $checked{'makecrs'}{'Y'}.' />'.&mt('Yes').'</label></span>'.
+               '</fieldset>'.
+               '<fieldset><legend>'.&mt('Roles which may self-enroll').'</legend>';
+    foreach my $lticrsrole (@lticourseroles) {
+        $output .= '<span class="LC_nobreak"><label><input type="checkbox" name="lti_selfenroll_'.$num.'" value="'.$lticrsrole.'"'.
+                   $checked{'selfenroll'}{$lticrsrole}.' />'.$lticrsrole.'</label> </span> ';
+    }
+    $output .= '</fieldset>'.
+               '<fieldset><legend>'.&mt('Course options').'</legend>'.
+               '<div class="LC_floatleft"><span class="LC_nobreak">'.&mt('Assign users to sections').': '.
+               '<label><input type="radio" name="lti_crssec_'.$num.'" value="0"'.
+               $checked{'crssec'}{'N'}.$onclicksec.' />'.&mt('No').'</label>'.(' 'x2).
+               '<label><input type="radio" name="lti_crssec_'.$num.'" value="1"'.
+               $checked{'crssec'}{'Y'}.$onclicksec.' />'.&mt('Yes').'</label><span></div>'.
+               '<div class="LC_floatleft" style="display:'.$crssecfieldsty.';" id="lti_crssecfield_'.$num.'">'.
+               '<span class="LC_nobreak">'.&mt('From').':<label>'.
+               '<input type="radio" name="lti_crssecsrc_'.$num.'" value="course_section_sourcedid"'.
+               $checked{'crssecsrc'}{'sourcedid'}.$onclicksecsrc.' />'.
+               &mt('Standard field').'</label>'.(' 'x2).
+               '<label><input type="radio" name="lti_crssecsrc_'.$num.'" value="other"'.
+               $checked{'crssecsrc'}{'other'}.$onclicksecsrc.' />'.&mt('Other').
+               '</label></div><div class="LC_floatleft" style="display:'.$secsrcfieldsty.';" id="lti_secsrcfield_'.$num.'">'.
+               '<input type="text" name="lti_customsection_'.$num.'" value="'.$crssecsrc.'" />'.
+               '</div><div style="padding:0;clear:both;margin:0;border:0"></div>'; 
+    foreach my $extra ('passback','roster') {
+        my $checkedon = '';
+        my $checkedoff = ' checked="checked"';
+        if (ref($current) eq 'HASH') {
+            if (($current->{$extra})) {
+                $checkedon = $checkedoff;
+                $checkedoff = '';
+            }
+        }
+        $output .= $lt{$extra}.' '.
+                   '<label><input type="radio" name="lti_'.$extra.'_'.$num.'" value="0"'.$checkedoff.' />'.
+                   &mt('No').'</label>'.(' 'x2).
+                   '<label><input type="radio" name="lti_'.$extra.'_'.$num.'" value="1"'.$checkedon.' />'.
+                   &mt('Yes').'</label><br />';
+    }
+    $output .= '</span></fieldset>';
+#        '<fieldset><legend>'.&mt('Assigning author roles').'</legend>';
+#
+#    $output .= '</fieldset>'.
+#        '<fieldset><legend>'.&mt('Assigning domain roles').'</legend>';
+    return $output;
+}
+
 sub print_coursedefaults {
     my ($position,$dom,$settings,$rowtotal) = @_;
     my ($css_class,$datatable,%checkedon,%checkedoff,%defaultchecked, at toggles);
@@ -10726,6 +11229,422 @@
     return ($id,$error);
 }
 
+sub modify_lti {
+    my ($r,$dom,$action,$lastactref,%domconfig) = @_;
+    my %domdefaults = &Apache::lonnet::get_domain_defaults($dom,1);
+    my ($newid, at allpos,%changes,%confhash,%encconfig,$errors,$resulttext);
+    my (%posslti,%posslticrs,%posscrstype);
+    my @courseroles = ('cc','in','ta','ep','st');
+    my @ltiroles = qw(Learner Instructor ContentDeveloper TeachingAssistant Mentor Member Manager Administrator);
+    my @lticourseroles = qw(Instructor TeachingAssistant Mentor Learner);
+    my @coursetypes = ('official','unofficial','community','textbook','placement');
+    my %coursetypetitles = &Apache::lonlocal::texthash (
+                               official   => 'Official',
+                               unofficial => 'Unofficial',
+                               community  => 'Community',
+                               textbook   => 'Textbook',
+                               placement  => 'Placement Test',
+    );
+    my %lt = &lti_names();
+    map { $posslti{$_} = 1; } @ltiroles;
+    map { $posslticrs{$_} = 1; } @lticourseroles;
+    map { $posscrstype{$_} = 1; } @coursetypes;
+    
+    my (@items,%deletions,%itemids);
+    if ($env{'form.lti_add'}) {
+        my $consumer = $env{'form.lti_consumer_add'};
+        $consumer =~ s/(`)/'/g;
+        ($newid,my $error) = &get_lti_id($dom,$consumer);
+        if ($newid) {
+            $itemids{'add'} = $newid;
+            push(@items,'add');
+            $changes{$newid} = 1;
+        } else {
+            my $error = &mt('Failed to acquire unique ID for new LTI configuration');
+            $errors .= '<li><span class="LC_error">'.$error.'</span></li>';
+        }
+    }
+    if (ref($domconfig{$action}) eq 'HASH') {
+        my @todelete = &Apache::loncommon::get_env_multiple('form.lti_del');
+        if (@todelete) {
+            map { $deletions{$_} = 1; } @todelete;
+        }
+        my $maxnum = $env{'form.lti_maxnum'};
+        for (my $i=0; $i<=$maxnum; $i++) {
+            my $itemid = $env{'form.lti_id_'.$i};
+            $itemid =~ s/\D+//g;
+            if (ref($domconfig{$action}{$itemid}) eq 'HASH') {
+                if ($deletions{$itemid}) {
+                    $changes{$itemid} = $domconfig{$action}{$itemid}{'consumer'};
+                } else {
+                   push(@items,$i);
+                   $itemids{$i} = $itemid;
+                }
+            }
+        }
+    }
+    foreach my $idx (@items) {
+        my $itemid = $itemids{$idx};
+        next unless ($itemid);
+        my $position = $env{'form.lti_pos_'.$idx};
+        $position =~ s/\D+//g;
+        if ($position ne '') {
+            $allpos[$position] = $itemid;
+        }
+        foreach my $item ('consumer','key','secret','lifetime') {
+            my $formitem = 'form.lti_'.$item.'_'.$idx;
+            $env{$formitem} =~ s/(`)/'/g;
+            if ($item eq 'lifetime') {
+                $env{$formitem} =~ s/[^\d.]//g;
+            }
+            if ($env{$formitem} ne '') {
+                if (($item eq 'key') || ($item eq 'secret')) {
+                    $encconfig{$itemid}{$item} = $env{$formitem};
+                } else {
+                    $confhash{$itemid}{$item} = $env{$formitem};
+                    unless (($idx eq 'add') || ($changes{$itemid})) {
+                        if ($domconfig{$action}{$itemid}{$item} ne $confhash{$itemid}{$item}) {
+                            $changes{$itemid} = 1;
+                        }
+                    }
+                }
+            }
+        }
+        if ($env{'form.lti_version_'.$idx} eq 'LTI-1p0') {
+            $confhash{$itemid}{'version'} = $env{'form.lti_version_'.$idx};
+        }
+        if ($env{'form.lti_mapuser_'.$idx} eq 'sourcedid') {
+            $confhash{$itemid}{'mapuser'} = 'lis_person_sourcedid'; 
+        } elsif ($env{'form.lti_mapuser_'.$idx} eq 'email') {
+            $confhash{$itemid}{'mapuser'} = 'lis_person_contact_email_primary';
+        } elsif ($env{'form.lti_mapuser_'.$idx} eq 'other') {
+            my $mapuser = $env{'form.lti_customuser_'.$idx};
+            $mapuser =~ s/(`)/'/g;
+            $mapuser =~ s/^\s+|\s+$//g; 
+            $confhash{$itemid}{'mapuser'} = $mapuser; 
+        }
+        foreach my $ltirole (@lticourseroles) {
+            my $possrole = $env{'form.lti_maprole_'.$ltirole.'_'.$idx};
+            if (grep(/^\Q$possrole\E$/, at courseroles)) {
+                $confhash{$itemid}{'maproles'}{$ltirole} = $possrole;
+            }
+        }
+        my @possmakeuser = &Apache::loncommon::get_env_multiple('form.lti_makeuser_'.$idx);
+        my @makeuser;
+        foreach my $ltirole (sort(@possmakeuser)) {
+            if ($posslti{$ltirole}) {
+                push(@makeuser,$ltirole);
+            }
+        }
+        $confhash{$itemid}{'makeuser'} = \@makeuser;
+        if (($env{'form.lti_mapcrs_'.$idx} eq 'course_offering_sourcedid') ||
+            ($env{'form.lti_mapcrs_'.$idx} eq 'context_id'))  {
+            $confhash{$itemid}{'mapcrs'} = $env{'form.lti_mapcrs_'.$idx};
+        } elsif ($env{'form.lti_mapcrs_'.$idx} eq 'other') {
+            my $mapcrs = $env{'form.lti_mapcrsfield_'.$idx}; 
+            $mapcrs =~ s/(`)/'/g;
+            $mapcrs =~ s/^\s+|\s+$//g;
+            $confhash{$itemid}{'mapcrs'} = $mapcrs;
+        }
+        my @posstypes = &Apache::loncommon::get_env_multiple('form.lti_mapcrstype_'.$idx);
+        my @crstypes;
+        foreach my $type (sort(@posstypes)) {
+            if ($posscrstype{$type}) {
+                push(@crstypes,$type);
+            }
+        }
+        $confhash{$itemid}{'mapcrstype'} = \@crstypes;
+        if ($env{'form.lti_makecrs_'.$idx}) {
+            $confhash{$itemid}{'makecrs'} = 1;
+        }
+        my @possenroll = &Apache::loncommon::get_env_multiple('form.lti_selfenroll_'.$idx);
+        my @selfenroll;
+        foreach my $type (sort(@possenroll)) {
+            if ($posslticrs{$type}) {
+                push(@selfenroll,$type);
+            }
+        }
+        $confhash{$itemid}{'selfenroll'} = \@selfenroll;
+        if ($env{'form.lti_crssec_'.$idx}) {
+            if ($env{'form.lti_crssecsrc_'.$idx} eq 'course_section_sourcedid') {
+                $confhash{$itemid}{'section'} = $env{'form.lti_crssecsrc_'.$idx};
+            } elsif ($env{'form.lti_crssecsrc_'.$idx} eq 'other') {
+                my $section = $env{'form.lti_customsection_'.$idx};
+                $section =~ s/(`)/'/g;
+                $section =~ s/^\s+|\s+$//g;
+                if ($section ne '') {
+                    $confhash{$itemid}{'section'} = $section;
+                }
+            }
+        }
+        foreach my $field ('passback','roster') {
+            if ($env{'form.ltitools_'.$idx.'_'.$field}) {
+                $confhash{$itemid}{$field} = 1;
+            }
+        }
+        unless (($idx eq 'add') || ($changes{$itemid})) {
+            foreach my $field ('mapuser','mapcrs','section','passback','roster') {
+                if ($domconfig{$action}{$itemid}{$field} ne $confhash{$itemid}{$field}) {
+                    $changes{$itemid} = 1;
+                }
+            }
+            foreach my $field ('makeuser','mapcrstype','selfenroll') {
+                unless ($changes{$itemid}) {
+                    if (ref($domconfig{$action}{$itemid}{$field}) eq 'ARRAY') {
+                        if (ref($confhash{$itemid}{$field}) eq 'ARRAY') {
+                            my @diffs = &Apache::loncommon::compare_arrays($domconfig{$action}{$itemid}{$field},
+                                                                           $confhash{$itemid}{$field});
+                            if (@diffs) {
+                                $changes{$itemid} = 1;
+                            }
+                        } elsif (@{$domconfig{$action}{$itemid}{$field}} > 0) {
+                            $changes{$itemid} = 1;
+                        }
+                    } elsif (ref($confhash{$itemid}{$field}) eq 'ARRAY') {
+                        if (@{$confhash{$itemid}{$field}} > 0) {
+                            $changes{$itemid} = 1;
+                        }
+                    } 
+                }
+            }
+            unless ($changes{$itemid}) {
+                if (ref($domconfig{$action}{$itemid}{'maproles'}) eq 'HASH') {
+                    if (ref($confhash{$itemid}{'maproles'}) eq 'HASH') {
+                        foreach my $ltirole (keys(%{$domconfig{$action}{$itemid}{'maproles'}})) {
+                            if ($domconfig{$action}{$itemid}{'maproles'}{$ltirole} ne 
+                                $confhash{$itemid}{'maproles'}{$ltirole}) {
+                                $changes{$itemid} = 1;
+                                last;
+                            }
+                        }
+                        unless ($changes{$itemid}) {
+                            foreach my $ltirole (keys(%{$confhash{$itemid}{'maproles'}})) {
+                                if ($confhash{$itemid}{'maproles'}{$ltirole} ne 
+                                    $domconfig{$action}{$itemid}{'maproles'}{$ltirole}) {
+                                    $changes{$itemid} = 1;
+                                    last;
+                                }
+                            }
+                        }
+                    } elsif (keys(%{$domconfig{$action}{$itemid}{'maproles'}}) > 0) {
+                        $changes{$itemid} = 1;
+                    }
+                } elsif (ref($confhash{$itemid}{'maproles'}) eq 'HASH') {
+                    unless ($changes{$itemid}) {
+                        if (keys(%{$confhash{$itemid}{'maproles'}}) > 0) {
+                            $changes{$itemid} = 1;
+                        }
+                    }
+                }
+            }
+        }
+    }
+    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 %ltihash = (
+                          $action => { %confhash }
+                       );
+    my $putresult = &Apache::lonnet::put_dom('configuration',\%ltihash,
+                                             $dom);
+    if ($putresult eq 'ok') {
+        my %ltienchash = (
+                             $action => { %encconfig }
+                         );
+        &Apache::lonnet::put_dom('encconfig',\%ltienchash,$dom);
+        if (keys(%changes) > 0) {
+            my $cachetime = 24*60*60;
+            my %ltiall = %confhash;
+            foreach my $id (keys(%ltiall)) {
+                if (ref($encconfig{$id}) eq 'HASH') {
+                    foreach my $item ('key','secret') {
+                        $ltiall{$id}{$item} = $encconfig{$id}{$item};
+                    }
+                }
+            }
+            &Apache::lonnet::do_cache_new('lti',$dom,\%ltiall,$cachetime);
+            if (ref($lastactref) eq 'HASH') {
+                $lastactref->{'lti'} = 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}{'consumer'}.'</b></li><ul>';
+                    my $position = $pos + 1;
+                    $resulttext .= '<li>'.&mt('Order: [_1]',$position).'</li>';
+                    foreach my $item ('version','lifetime') {
+                        if ($confhash{$itemid}{$item} ne '') {
+                            $resulttext .= '<li>'.$lt{$item}.': '.$confhash{$itemid}{$item}.'</li>';
+                        }
+                    }
+                    if ($encconfig{$itemid}{'key'} ne '') {
+                        $resulttext .= '<li>'.$lt{'key'}.': '.$encconfig{$itemid}{'key'}.'</li>';
+                    }
+                    if ($encconfig{$itemid}{'secret'} ne '') {
+                        $resulttext .= '<li>'.$lt{'secret'}.': ';
+                        my $num = length($encconfig{$itemid}{'secret'});
+                        $resulttext .= ('*'x$num).'</li>';
+                    }
+                    if ($confhash{$itemid}{'mapuser'}) {
+                        my $shownmapuser;
+                        if ($confhash{$itemid}{'mapuser'} eq 'lis_person_sourcedid') {
+                            $shownmapuser = $lt{'sourcedid'}.' (lis_person_sourcedid)';
+                        } elsif ($confhash{$itemid}{'mapuser'} eq 'lis_person_contact_email_primary') {
+                            $shownmapuser = $lt{'email'}.' (lis_person_contact_email_primary)';
+                        } else {
+                            $shownmapuser = &mt('Other').' ('.$confhash{$itemid}{'mapuser'}.')';
+                        } 
+                        $resulttext .= '<li>'.&mt('LON-CAPA username').': '.$shownmapuser.'</li>';
+                    }
+                    if (ref($confhash{$itemid}{'maproles'}) eq 'HASH') {
+                        my $rolemaps;
+                        foreach my $role (@ltiroles) {
+                            if ($confhash{$itemid}{'maproles'}{$role}) {
+                                $rolemaps .= (' 'x2).$role.'='.
+                                             &Apache::lonnet::plaintext($confhash{$itemid}{'maproles'}{$role},
+                                                                        'Course').',';
+                            }
+                        }
+                        if ($rolemaps) {
+                            $rolemaps =~ s/,$//;
+                            $resulttext .= '<li>'.&mt('Role mapping:').$rolemaps.'</li>';
+                        }
+                    }
+                    if (ref($confhash{$itemid}{'makeuser'}) eq 'ARRAY') {
+                        if (@{$confhash{$itemid}{'makeuser'}} > 0) { 
+                            $resulttext .= '<li>'.&mt('Following roles may create user accounts: [_1]',
+                                                      join(', ',@{$confhash{$itemid}{'makeuser'}})).'</li>';
+                        } else {
+                            $resulttext .= '<li>'.&mt('User account creation not permitted.').'</li>';
+                        }
+                    }
+                    if ($confhash{$itemid}{'mapcrs'}) {
+                        $resulttext .= '<li>'.&mt('Unique course identifier').': '.$confhash{$itemid}{'mapcrs'}.'</li>';
+                    }
+                    if (ref($confhash{$itemid}{'mapcrstype'}) eq 'ARRAY') {
+                        if (@{$confhash{$itemid}{'mapcrstype'}} > 0) {
+                            $resulttext .= '<li>'.&mt('Mapping for the following LON-CAPA course types: [_1]',
+                                           join(', ',map { $coursetypetitles{$_}; } @coursetypes)).
+                                           '</li>';
+                        } else {
+                            $resulttext .= '<li>'.&mt('No mapping to LON-CAPA courses').'</li>';
+                        }
+                    }
+                    if ($confhash{$itemid}{'makecrs'}) {
+                        $resulttext .= '<li>'.&mt('Instructor may create course (if absent).').'</li>';
+                    } else {
+                        $resulttext .= '<li>'.&mt('Instructor may not create course (if absent).').'</li>';
+                    }
+                    if (ref($confhash{$itemid}{'selfenroll'}) eq 'ARRAY') {
+                        if (@{$confhash{$itemid}{'selfenroll'}} > 0) {
+                            $resulttext .= '<li>'.&mt('Self-enrollment for following roles: [_1]',
+                                                      join(', ',@{$confhash{$itemid}{'selfenroll'}})).
+                                           '</li>';
+                        } else {
+                            $resulttext .= '<li>'.&mt('Self-enrollment not permitted').'</li>';
+                        }
+                    }
+                    if ($confhash{$itemid}{'section'}) {
+                        if ($confhash{$itemid}{'section'} eq 'course_section_sourcedid') {
+                            $resulttext .= '<li>'.&mt('User section from standard field:').
+                                                 ' (course_section_sourcedid)'.'</li>';  
+                        } else {
+                            $resulttext .= '<li>'.&mt('User section from:').' '.
+                                                  $confhash{$itemid}{'section'}.'</li>';
+                        }
+                    } else {
+                        $resulttext .= '<li>'.&mt('No section assignment').'</li>';
+                    }
+                    foreach my $item ('passback','roster') {
+                        $resulttext .= '<li>'.$lt{$item}.' ';
+                        if ($confhash{$itemid}{$item}) {
+                            $resulttext .= &mt('Yes');
+                        } else {
+                            $resulttext .= &mt('No');
+                        }
+                        $resulttext .= '</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 get_lti_id {
+    my ($domain,$consumer) = @_;
+    # get lock on lti db
+    my $lockhash = {
+                      lock => $env{'user.name'}.
+                              ':'.$env{'user.domain'},
+                   };
+    my $tries = 0;
+    my $gotlock = &Apache::lonnet::newput_dom('lti',$lockhash,$domain);
+    my ($id,$error);
+
+    while (($gotlock ne 'ok') && ($tries<10)) {
+        $tries ++;
+        sleep (0.1);
+        $gotlock = &Apache::lonnet::newput_dom('lti',$lockhash,$domain);
+    }
+    if ($gotlock eq 'ok') {
+        my %currids = &Apache::lonnet::dump_dom('lti',$domain);
+        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('lti',{ $id => $consumer },$domain) eq 'ok') {
+                    $error = 'nostore';
+                }
+            } else {
+                $error = 'nonumber';
+            }
+        }
+        my $dellockoutcome = &Apache::lonnet::del_dom('lti',['lock'],$domain);
+    } else {
+        $error = 'nolock';
+    }
+    return ($id,$error);
+}
+
 sub modify_autoenroll {
     my ($dom,$lastactref,%domconfig) = @_;
     my ($resulttext,%changes);
Index: loncom/interface/lonconfigsettings.pm
diff -u loncom/interface/lonconfigsettings.pm:1.39 loncom/interface/lonconfigsettings.pm:1.40
--- loncom/interface/lonconfigsettings.pm:1.39	Thu Nov 30 12:45:55 2017
+++ loncom/interface/lonconfigsettings.pm	Thu Nov 30 15:15:15 2017
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # Handler to set domain-wide configuration settings
 #
-# $Id: lonconfigsettings.pm,v 1.39 2017/11/30 12:45:55 raeburn Exp $
+# $Id: lonconfigsettings.pm,v 1.40 2017/11/30 15:15:15 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -215,6 +215,21 @@
                 my $optionsprefix = 'LC_options_helpdesk_';
                 $onload .= "toggleHelpdeskRow(document.display,'overrides','$customclass','$optionsprefix');";
             }
+            if (grep(/^lti$/, at actions)) {
+                $onload .= "toggleLTI(document.display,'user','add');".
+                           "toggleLTI(document.display,'crs','add');".
+                           "toggleLTI(document.display,'sec','add');";
+                if (ref($values) eq 'HASH') {
+                    if (ref($values->{'lti'}) eq 'HASH') {
+                        my $numlti = scalar(keys(%{$values->{'lti'}}));
+                        for (my $i=0; $i<$numlti; $i++) {
+                            $onload .= "toggleLTI(document.display,'user','$i');".
+                                       "toggleLTI(document.display,'crs','$i');".
+                                       "toggleLTI(document.display,'sec','$i');";
+                        }
+                    }
+                } 
+            }
             if (grep(/^ltitools$/, at actions)) {
                 $onload .= "toggleLTITools(document.display,'passback','add');".
                            "toggleLTITools(document.display,'roster','add');";
Index: doc/loncapafiles/loncapafiles.lpml
diff -u doc/loncapafiles/loncapafiles.lpml:1.957 doc/loncapafiles/loncapafiles.lpml:1.958
--- doc/loncapafiles/loncapafiles.lpml:1.957	Mon Nov  6 03:08:36 2017
+++ doc/loncapafiles/loncapafiles.lpml	Thu Nov 30 15:15:23 2017
@@ -2,7 +2,7 @@
  "http://lpml.sourceforge.net/DTD/lpml.dtd">
 <!-- loncapafiles.lpml -->
 
-<!-- $Id: loncapafiles.lpml,v 1.957 2017/11/06 03:08:36 raeburn Exp $ -->
+<!-- $Id: loncapafiles.lpml,v 1.958 2017/11/30 15:15:23 raeburn Exp $ -->
 
 <!--
 
@@ -417,6 +417,12 @@
 </directory>
 <directory dist='default'>
   <protectionlevel>modest_delete</protectionlevel>
+  <targetdir dist='default'>home/httpd/lonLTItmp</targetdir>
+  <categoryname>server standard</categoryname>
+  <description>for temporary storage of LTI nonces</description>
+</directory>
+<directory dist='default'>
+  <protectionlevel>modest_delete</protectionlevel>
   <targetdir dist='default'>home/httpd/webdav</targetdir>
   <categoryname>server standard</categoryname>
   <description>for storage of webdav DAVLock files</description>
@@ -2530,6 +2536,15 @@
 <status>works/unverified</status>
 </file>
 <file>
+<source>loncom/auth/lonlti.pm</source>
+<target dist='default'>home/httpd/lib/perl/Apache/lonlti.pm</target>
+<categoryname>handler</categoryname>
+<description>
+Handler to allow LON-CAPA to operate as an LTI Provider
+</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>

Index: loncom/auth/lonlti.pm
+++ loncom/auth/lonlti.pm
# The LearningOnline Network
# Basic LTI Authentication Module
#
# $Id: lonlti.pm,v 1.1 2017/11/30 15:14:51 raeburn Exp $
#
# Copyright Michigan State University Board of Trustees
#
# This file is part of the LearningOnline Network with CAPA (LON-CAPA).
#
# LON-CAPA is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# LON-CAPA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with LON-CAPA; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# /home/httpd/html/adm/gpl.txt
#
# http://www.lon-capa.org/
#

package Apache::lonlti;

use strict;
use LONCAPA qw(:DEFAULT :match);
use Apache::Constants qw(:common :http);
use Net::OAuth;
use Apache::lonlocal;
use Apache::lonnet;
use Apache::loncommon;
use Apache::lonacc;

sub handler {
    my $r = shift;
    my $requri = $r->uri;
#
# Retrieve data POSTed by LTI Consumer on launch  
#
    &Apache::lonacc::get_posted_cgi($r);
    my $params = {};
    foreach my $key (sort(keys(%env))) {
        if ($key =~ /^form\.(.+)$/) {
            $params->{$1} = $env{$key};
        }
    }

    unless (keys(%{$params})) {
        &invalid_request($r,1);
        return OK;
    }

    unless ($params->{'oauth_consumer_key'} &&
            $params->{'oauth_nonce'} &&
            $params->{'oauth_timestamp'} &&
            $params->{'oauth_version'} &&
            $params->{'oauth_signature'} &&
            $params->{'oauth_signature_method'}) {
        &invalid_request($r,2);
        return OK;
    }

#
# Retrieve "internet domains" for all this institution's LON-CAPA
# nodes.
#
    my ($udom,$uname,$uhome,$cdom,$cnum,$symb,$mapurl, at intdoms);
    my $lonhost = $r->dir_config('lonHostID');
    my $internet_names = &Apache::lonnet::get_internet_names($lonhost);
    if (ref($internet_names) eq 'ARRAY') {
        @intdoms = @{$internet_names};
    }

#
# For user who launched LTI in Consumer, determine user's domain in 
# LON-CAPA.
#
# Order is:
#
# (a) from custom_userdomain item in POSTed data
# (b) from lis_person_sourcedid in POSTed data
# (c) from default "log-in" domain for node
#     (can support multidomain servers, where specific domain is 
#      first part of hostname).
#
# Note: "internet domain" for user's domain must be one of the
# "internet domain(s)" for the institution's LON-CAPA servers.
#
    if (exists($params->{'custom_userdomain'})) {
        if ($params->{'custom_userdomain'} =~ /^$match_domain$/) {
            my $uprimary_id = &Apache::lonnet::domain($params->{'custom_userdomain'},'primary');
            if ($uprimary_id ne '') {
                my $uintdom = &Apache::lonnet::internet_dom($uprimary_id);
                if (($uintdom ne '') && (grep(/^\Q$uintdom\E$/, at intdoms))) {
                    $udom = $params->{'custom_userdomain'};
                }
            }
        }
    }
    my $defdom = &Apache::lonnet::default_login_domain();
    my ($domain,$possuname,$possudom,$possmapuser);
    if ($env{'form.lis_person_sourcedid'} =~ /^($match_username)\:($match_domain)$/) {
        ($possuname,$possudom) = ($1,$2);
        if ($udom eq '') {
            my $uintdom = &Apache::lonnet::domain($possudom,'primary');
            if (($uintdom ne '') && (grep(/^\Q$uintdom\E$/, at intdoms))) {
                $udom = $possudom;
                $possmapuser = 'lis_person_sourcedid';
            } else {
                $udom = $defdom;
            }
        } elsif ($udom eq $possudom) {
            $possmapuser = 'lis_person_sourcedid';
        }
    }
    unless ($possuname) {
        if ($env{'form.lis_person_sourcedid'} =~ /^$match_username$/) {
            $possuname = $env{'form.lis_person_sourcedid'};
            $possmapuser = 'lis_person_sourcedid';
        } elsif ($env{'form.lis_person_contact_email_primary'} =~ /^$match_username$/) {
            $possuname = $env{'form.lis_person_contact_email_primary'};
            $possmapuser = 'lis_person_contact_email_primary';
        }
        unless ($udom) {
            $udom = $defdom;
        }
    }

#
# Determine course's domain in LON-CAPA
#
# Order is:
#
# (a) from custom_coursedomain item in POSTed data
# (b) from tail of requested URL (after /adm/lti) if it has format of a symb  
# (c) from tail of requested URL (after /adm/lti) if it has format of a map 
# (d) from tail of requested URL (after /adm/lti) if it has format /domain/courseID
# (e) from tail of requested URL (after /adm/lti) if it has format /tiny/domain/...
# i.e., a shortened URL (see bug #6400) -- not implemented yet.   
# (f) same as user's domain 
#
# Request invalid if custom_coursedomain is defined and is inconsistent with
# domain contained in requested URL.
#
# Note: "internet domain" for course's domain must be one of the
# internet domains for the institution's LON-CAPA servers.
#

    if (exists($params->{'custom_coursedomain'})) {
        if ($params->{'custom_coursedomain'} =~ /^$match_domain$/) {
            my $cprimary_id = &Apache::lonnet::domain($params->{'custom_coursedomain'},'primary');
            if ($cprimary_id ne '') {
                my $cintdom = &Apache::lonnet::internet_dom($cprimary_id);
                if (($cintdom ne '') && (grep(/^\Q$cintdom\E$/, at intdoms))) {
                    $cdom = $params->{'custom_coursedomain'};
                }
            }
        }
    }

    my ($tail) = ($requri =~ m{^/adm/lti(|/.*)$});
    my $urlcnum;
    if ($tail ne '') {
        my $urlcdom;
        if ($tail =~ m{^/uploaded/($match_domain)/($match_courseid)/(?:default|supplemental)(?:|_\d+)\.(?:sequence|page)(|___\d+___.+)$}) {
            ($urlcdom,$urlcnum,my $rest) = ($1,$2,$3);
            if (($cdom ne '') && ($cdom ne $urlcdom)) {
                &invalid_request($r,3);
                return OK;
            }
            if ($rest eq '') {
                $mapurl = $tail;
            } else {
                $symb = $tail;
                $symb =~ s{^/+}{};
            }
#FIXME Need to handle encrypted URLs 
        } elsif ($tail =~ m{^/($match_domain)/($match_courseid)$}) {
            ($urlcdom,$urlcnum) = ($1,$2);
            if (($cdom ne '') && ($cdom ne $urlcdom)) {
                &invalid_request($r,4);
                return OK;
            }
        }
        if (($cdom eq '') && ($urlcdom ne '')) { 
            my $cprimary_id = &Apache::lonnet::domain($urlcdom,'primary');
            if ($cprimary_id ne '') {
                my $cintdom = &Apache::lonnet::internet_dom($cprimary_id);
                if (($cintdom ne '') && (grep(/^\Q$cintdom\E$/, at intdoms))) {
                    $cdom = $urlcdom;
                }
            } else {
                $urlcnum = '';
            }
        }
    }
    if ($cdom eq '') {
        if ($udom ne '') {
            $cdom = $udom;
        } else {
            $cdom = $defdom;
        }
    }

#
# Retrieve information for LTI Consumers in course domain
# and populate hash --  %lti_by_key -- for which keys
# are those defined in domain configuration for LTI.
#
 
    my %lti = &Apache::lonnet::get_domain_lti($cdom,'provider');
    unless (keys(%lti) > 0) {
        &invalid_request($r,5);
        return OK;
    }
    my %lti_by_key;
    if (keys(%lti)) {
        foreach my $id (keys(%lti)) {
            if (ref($lti{$id}) eq 'HASH') {
                my $key = $lti{$id}{'key'};
                push(@{$lti_by_key{$key}},$id);
            }
        }
    }

#
# Verify the signed request using the secret for those
# Consumers for which the key in the POSTed data matches 
# keys in the domain configuration for LTI.
#
    my $hostname = $r->hostname;
    my $protocol = 'http';
    if ($ENV{'SERVER_PORT'} == 443) {
        $protocol = 'https';
    }

    my $itemid;
    my $key = $params->{'oauth_consumer_key'};
    my @ltiroles;
    if (ref($lti_by_key{$key}) eq 'ARRAY') {
        foreach my $id (@{$lti_by_key{$key}}) {
            if (ref($lti{$id}) eq 'HASH') {
                my $secret = $lti{$id}{'secret'};
                my $request = Net::OAuth->request('request token')->from_hash($params,
                                                   request_url => $protocol.'://'.$hostname.$requri,
                                                   request_method => $env{'request.method'},
                                                   consumer_secret => $secret,);
                if ($request->verify()) {
                    $itemid = $id;
                    last;
                }
            }
        }
    }

#
# Request is invalid if the signed request could not be verified
# for the Consumer key and Consumer secret from the domain
# configuration in LON-CAPA for that LTI Consumer.
#
    unless (($itemid) && (ref($lti{$itemid}) eq 'HASH')) {
        &invalid_request($r,6);
        return OK;
    }

#
# Determine if nonce in POSTed data has expired.
# If unexpired, confirm it has not already been used.
#
    unless (&check_nonce($r,$params->{'oauth_nonce'},$params->{'oauth_timestamp'},$lti{$itemid}{'lifetime'},$cdom)) {
        &invalid_request($r,7);
        return OK;
    }

#
# Determinine if source of username matches requirement from the 
# domain configuration for the specific LTI Consumer.
# 

    if ($lti{$itemid}{'mapuser'} eq $possmapuser) {
        $uname = $possuname;
    } elsif ($lti{$itemid}{'mapuser'} eq 'lis_person_sourcedid') {
        if ($params->{'lis_person_sourcedid'} =~ /^$match_username$/) {
            $uname = $possuname;
        }
    } elsif ($lti{$itemid}{'mapuser'} eq 'lis_person_contact_email_primary') {
        if ($params->{'lis_person_contact_email_primary'} =~ /^$match_username$/) {
            $uname = $params->{'lis_person_contact_email_primary'};
        }
    } elsif (exists($params->{$lti{$itemid}{'mapuser'}})) {
        if ($params->{$lti{$itemid}{'mapuser'}} =~ /^$match_username$/) {
            $uname = $params->{$lti{$itemid}{'mapuser'}};
        }
    }

#
# Determine the courseID of the LON-CAPA course to which the
# launch of LON-CAPA should provide access.
#
# Order is:
#
# (a) from course mapping (if the link between Consumer "course" and 
# Provider "course" has been established previously).
# (b) from tail of requested URL (after /adm/lti) if it has format of a symb
# (c) from tail of requested URL (after /adm/lti) if it has format of a map
# (d) from tail of requested URL (after /adm/lti) if it has format /domain/courseID
# (e) from tail of requested URL (after /adm/lti) if it has format /tiny/domain/...
# i.e., a shortened URL (see bug #6400) -- not implemented yet.
#
# If Consumer course included in POSTed data points as a target course which
# has a format which matches a LON-CAPA courseID, but the course does not
# exist, the request is invalid.
# 

    my ($sourcecrs,%consumers);
    if ($lti{$itemid}{'mapcrs'} eq 'course_offering_sourcedid') {
        $sourcecrs = $params->{'course_offering_sourcedid'};
    } elsif ($lti{$itemid}{'mapcrs'} eq 'context_id') {
        $sourcecrs = $params->{'context_id'};
    } elsif ($lti{$itemid}{'mapcrs'} ne '') {
        $sourcecrs = $params->{$lti{$itemid}{'mapcrs'}};
    }

    my $posscnum;
    if ($sourcecrs ne '') {
        %consumers = &Apache::lonnet::get_dom('lticonsumers',[$sourcecrs],$cdom);
        if (exists($consumers{$sourcecrs})) {
            if ($consumers{$sourcecrs} =~ /^$match_courseid$/) {
                my $crshome = &Apache::lonnet::homeserver($consumers{$sourcecrs},$cdom);
                if ($crshome =~ /(con_lost|no_host|no_such_host)/) {
                    &invalid_request($r,8);
                    return OK;
                } else {
                    $posscnum = $consumers{$sourcecrs};
                }
            }
        }
    }

    if ($urlcnum ne '') {
        if ($posscnum ne '') {
            if ($posscnum ne $urlcnum) {
                &invalid_request($r,9);
                return OK;
            } else {
                $cnum = $posscnum;
            }
        } else {
            my $crshome = &Apache::lonnet::homeserver($urlcnum,$cdom);
            if ($crshome =~ /(con_lost|no_host|no_such_host)/) {
                &invalid_request($r,10);
                return OK;
            } else {
                $cnum = $urlcnum;
            }
        }
    } elsif ($posscnum ne '') {
        $cnum = $posscnum;
    }

#
# Get LON-CAPA role to use from role-mapping of Consumer roles
# defined in domain configuration for the appropriate LTI
# Consumer.
#
# If multiple LON-CAPA roles are indicated, choose based
# on the order: cc, in, ta, ep, st
#

    my $reqrole;

    my @roleorder = ('cc','in','ta','ep','st');
    if ($params->{'roles'} =~ /,/) {
        @ltiroles = split(/\s*,\s*/,$params->{'role'});
    } else {
        my $singlerole = $params->{'roles'};
        $singlerole =~ s/^\s|\s+$//g;
        @ltiroles = ($singlerole);
    }
    if (@ltiroles) {
        if (ref($lti{$itemid}{maproles}) eq 'HASH') {
            my %possroles;
            map { $possroles{$lti{$itemid}{maproles}{$_}} = 1; } @ltiroles;
            my @possibles = keys(%possroles);
            if (@possibles == 1) {
                if (grep(/^\Q$possibles[0]\E$/, at roleorder)) {
                    $reqrole = $possibles[0];

                }
            } elsif (@possibles > 1) {
                foreach my $item (@roleorder) {
                    if ($possroles{$item}) {
                        $reqrole = $item;
                        last;
                    }
                }
            }
        }
    }

#
# If no LON-CAPA username  -- is user allowed to create one?
#

    my $selfcreate;
    if (($uname ne '') && ($udom ne '')) {
        $uhome = &Apache::lonnet::homeserver($uname,$udom);
        if ($uhome =~ /(con_lost|no_host|no_such_host)/) {
            &Apache::lonnet::logthis(" LTI authorized unknown user $uname:$udom ");
            if (ref($lti{$itemid}{'makeuser'}) eq 'ARRAY') {
                if (@{$lti{$itemid}{'makeuser'}} > 0) {
                    foreach my $ltirole (@ltiroles) {
                        if (grep(/^\Q$ltirole\E$/,@{$lti{$itemid}{'makeuser'}})) {
                            $selfcreate = 1;
                        }
                    }
                }
            }
            if ($selfcreate) {
#FIXME Do user creation here.
                return OK
            } else {
                &invalid_request($r,11);
                return OK;
            } 
        } 
    } else {
        &invalid_request($r,12);
        return OK;
    }

#
# If no LON-CAPA course available, check if domain's configuration
# for the specific LTI Consumer allows a new course to be created 
# (requires role in Consumer to be: Instructor).
#

    if ($cnum eq '') {
        if ((@ltiroles) && (grep(/^Instructor$/, at ltiroles)) &&
            ($lti{$itemid}{'mapcrs'})) {
#FIXME Create a new LON-CAPA course here.
            return OK;
        } else {
            &invalid_request($r,13);
            return OK; 
        }
    }

#
# If LON-CAPA course is a Community, and LON-CAPA role
# indicated is cc, change role indicated to co.
# 

    if ($reqrole eq 'cc') {
        if (($cdom ne '') && ($cnum ne '')) {
            my %crsenv = &Apache::lonnet::coursedescription($cnum.'_'.$cdom,{ 'one_time' => 1,});
            if ($crsenv{'type'} eq 'Community') {
                $reqrole = 'co'; 
            }
        }
    }

#
# Determine if user has required LON-CAPA role
# in the mapped LON-CAPA course.
#

    my $role;
    my %crsroles = &Apache::lonnet::get_my_roles($uname,$udom,'userroles',undef,[$reqrole],[$cdom]);
    if (exists($crsroles{$cnum.':'.$cdom.':'.$reqrole})) {
        $role = $reqrole.'./'.$cdom.'/'.$cnum;
#FIXME Need to accommodate sections
    } elsif (ref($lti{$itemid}{'selfenroll'}) eq 'ARRAY') {
        if (grep(/^\Q$reqrole\E$/,@{$lti{$itemid}{'selfenroll'}})) {
#FIXME Do self-enrollment here
            return OK;
        } else {
            &invalid_request($r,14);
        }
    }

#
# Store consumer-to-LON-CAPA course mapping
#
    if (($sourcecrs ne '')  && ($consumers{$sourcecrs} eq '') && ($cnum ne '')) {
        &Apache::lonnet::put_dom('lticonsumers',{ $sourcecrs => $cnum },$cdom);
    }

#
# Check if user should be hosted here or switched to another server.
#

    &Apache::lonnet::logthis(" LTI authorized user: $uname:$udom role: $role course: $cnum:$cdom");
    $r->user($uname);
    my ($is_balancer,$otherserver,$hosthere);
    ($is_balancer,$otherserver) =
        &Apache::lonnet::check_loadbalancing($uname,$udom,'login');
    if ($is_balancer) {
        if ($otherserver eq '') {
            my $lowest_load;
            ($otherserver,undef,undef,undef,$lowest_load) = &Apache::lonnet::choose_server($udom);
            if ($lowest_load > 100) {
                $otherserver = &Apache::lonnet::spareserver($lowest_load,$lowest_load,1,$udom);
            }
        }
        if ($otherserver ne '') {
            my @hosts = &Apache::lonnet::current_machine_ids();
            if (grep(/^\Q$otherserver\E$/, at hosts)) {
                $hosthere = $otherserver;
            }
        }
    }
    if (($is_balancer) && (!$hosthere)) {
        # login but immediately go to switch server.
        &Apache::lonauth::success($r,$uname,$udom,$uhome,'noredirect');
        if ($symb) {
            $env{'form.symb'} = $symb;
        }
        if ($role) {
            $env{'form.role'} = $role;
        }
        if ($lti{$itemid}{'passback'}) {
            if ($params->{'lis_result_sourcedid'}) {
                $env{'request.lti.passbackid'} = $params->{'lis_result_sourcedid'};
            }
            if ($params->{'lis_outcome_service_url'}) {
                $env{'request.lti.passbackurl'} = $params->{'lis_outcome_service_url'};
            }
        }
        if (($lti{$itemid}{'roster'}) && (grep(/^Instructor$/, at ltiroles))) {
            if ($params->{'ext_ims_lis_memberships_id'}) {
                $env{'request.lti.rosterid'} = $params->{'ext_ims_lis_memberships_id'}; 
            }
            if ($params->{'ext_ims_lis_memberships_url'}) {
                $env{'request.lti.rosterurl'} = $params->{'ext_ims_lis_memberships_url'};
            }
        }
        $env{'request.lti.login'} = 1;
        foreach my $key (%{$params}) {
            delete($env{'form.'.$key});
        }
        my $redirecturl = '/adm/switchserver';
        if ($otherserver ne '') {
            $redirecturl .= '?otherserver='.$otherserver;
        }
        $r->internal_redirect($redirecturl);
        $r->set_handlers('PerlHandler'=> undef);
    } else {
        # need to login them in, so generate the need data that
        # migrate expects to do login
        foreach my $key (%{$params}) {
            delete($env{'form.'.$key});
        }
        my $ip = $r->get_remote_host();
        my %info=('ip'        => $ip,
                  'domain'    => $udom,
                  'username'  => $uname,
                  'server'    => $lonhost,
                  'lti.login' => 1,
                 );
        if ($role) {
            $info{'role'} = $role;
        }
        if ($symb) {
            $info{'symb'} = $symb; 
        }
        if ($lti{$itemid}{'passback'}) {
            if ($params->{'lis_result_sourcedid'}) {
                $info{'lti.passbackid'} = $params->{'lis_result_sourcedid'}
            }
            if ($params->{'lis_outcome_service_url'}) {
                $info{'lti.passbackurl'} = $params->{'lis_outcome_service_url'}
            }
        }
        if (($lti{$itemid}{'roster'}) && (grep(/^Instructor$/, at ltiroles))) {
            if ($params->{'ext_ims_lis_memberships_id'}) {
                $info{'lti.rosterid'} = $params->{'ext_ims_lis_memberships_id'};
            }
            if ($params->{'ext_ims_lis_memberships_url'}) {
                $info{'lti.rosterurl'} = $params->{'ext_ims_lis_memberships_url'};
            }
        }
        unless ($info{'symb'}) {
            if ($mapurl) {
                $info{'origurl'} = $mapurl;
                if ($mapurl =~ m{/default_\d+\.sequence$}) {
                    $info{'origurl'} .=  (($mapurl =~/\?/)?'&':'?').'navmap=1';
                }
            } else {
                unless ($tail eq '/adm/roles') {
                    $info{'origurl'} = '/adm/navmaps';
                }
            }
        }
        if (($is_balancer) && ($hosthere)) {
            $info{'noloadbalance'} = $hosthere;
        }
        my $token = &Apache::lonnet::tmpput(\%info,$lonhost);
        $env{'form.token'} = $token;
        $r->internal_redirect('/adm/migrateuser');
        $r->set_handlers('PerlHandler'=> undef);
    }
    return OK;
}

sub check_nonce {
    my ($r,$nonce,$timestamp,$lifetime,$domain) = @_;
    if (($timestamp eq '') || ($timestamp =~ /^\D/) || ($lifetime eq '') || ($lifetime =~ /\D/) || ($domain eq '')) {
        return 0;
    }
    my $now = time;
    if (($timestamp) && ($timestamp < ($now - $lifetime))) {
        return 0;
    }
    if ($nonce eq '') {
        return 0;
    }
    my $lonltidir = $r->dir_config('lonLTIDir');
    if (-e "$lonltidir/$domain/$nonce") {
        return 0;
    } else {
        unless (-e "$lonltidir/$domain") {
            mkdir("$lonltidir/$domain",0755);
        }  
        if (open(my $fh,'>',"$lonltidir/$domain/$nonce")) {
            print $fh $now;
            close($fh);
        } else {
            return 0;
        }
    }
    return 1;
}

sub invalid_request {
    my ($r,$num) = @_;
    &Apache::loncommon::content_type($r,'text/html');
    $r->send_http_header;
    if ($r->header_only) {
        return;
    }
    &Apache::lonlocal::get_language_handle($r);
    $r->print(
        &Apache::loncommon::start_page('Invalid LTI call').
        &mt('Invalid LTI call [_1]',$num).
        &Apache::loncommon::end_page());
    return;
}

1;


More information about the LON-CAPA-cvs mailing list