--- loncom/interface/loncommon.pm	2017/12/18 16:11:18	1.1304
+++ loncom/interface/loncommon.pm	2018/04/24 13:40:32	1.1314
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # a pile of common routines
 #
-# $Id: loncommon.pm,v 1.1304 2017/12/18 16:11:18 raeburn Exp $
+# $Id: loncommon.pm,v 1.1314 2018/04/24 13:40:32 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -86,6 +86,8 @@ use MIME::Lite;
 use MIME::Types;
 use File::Copy();
 use File::Path();
+use String::CRC32();
+use Short::URL();
 
 # ---------------------------------------------- Designs
 use vars qw(%defaultdesign);
@@ -1295,9 +1297,13 @@ sub help_open_topic {
     }
 
     # Add the text
+    my $target = ' target="_top"';
+    if (($env{'request.lti.login'}) && ($env{'request.lti.target'} eq 'iframe')) {
+        $target = '';
+    }
     if ($text ne "") {	
 	$template.='<span class="LC_help_open_topic">'
-                  .'<a target="_top" href="'.$link.'">'
+                  .'<a'.$target.' href="'.$link.'">'
                   .$text.'</a>';
     }
 
@@ -1307,7 +1313,7 @@ sub help_open_topic {
     if ($imgid ne '') {
         $imgid = ' id="'.$imgid.'"';
     }
-    $template.=' <a target="_top" href="'.$link.'" title="'.$title.'">'
+    $template.=' <a'.$target.' href="'.$link.'" title="'.$title.'">'
               .'<img src="'.$helpicon.'" border="0"'
               .' alt="'.&mt('Help: [_1]',$topic).'"'
               .' title="'.$title.'" style="vertical-align:middle;"'.$imgid 
@@ -1500,19 +1506,24 @@ sub help_open_bug {
     {
 	$link = $url;
     }
+
+    my $target = ' target="_top"';
+    if (($env{'request.lti.login'}) && ($env{'request.lti.target'} eq 'iframe')) {
+        $target = '';
+    }
     # Add the text
     if ($text ne "")
     {
 	$template .= 
   "<table bgcolor='#AA3333' cellspacing='1' cellpadding='1' border='0'><tr>".
-  "<td bgcolor='#FF5555'><a target=\"_top\" href=\"$link\"><span style=\"color:#FFFFFF;font-size:10pt;\">$text</span></a>";
+  "<td bgcolor='#FF5555'><a".$target." href=\"$link\"><span style=\"color:#FFFFFF;font-size:10pt;\">$text</span></a>";
     }
 
     # Add the graphic
     my $title = &mt('Report a Bug');
     my $bugicon=&lonhttpdurl("/adm/lonMisc/smallBug.gif");
     $template .= <<"ENDTEMPLATE";
- <a target="_top" href="$link" title="$title"><img src="$bugicon" border="0" alt="(Bug: $topic)" /></a>
+ <a$target href="$link" title="$title"><img src="$bugicon" border="0" alt="(Bug: $topic)" /></a>
 ENDTEMPLATE
     if ($text ne '') { $template.='</td></tr></table>' };
     return $template;
@@ -3025,6 +3036,8 @@ This is not an optimal method, but it wo
 
 =item * authform_filesystem
 
+=item * authform_lti
+
 =back
 
 See loncreateuser.pm for invocation and use examples.
@@ -3441,14 +3454,69 @@ sub authform_filesystem {
                     $fsyscheck.' onchange="'.$jscall.'" onclick="'.
                     $jscall.'"'.$disabled.' />';
     }
-    $autharg = '<input type="text" size="10" name="fsysarg" value=""'.
+    $autharg = '<input type="password" size="10" name="fsysarg" value=""'.
                ' onchange="'.$jscall.'"'.$disabled.' />';
     $result = &mt
         ('[_1] Filesystem Authenticated (with initial password [_2])',
-         '<label><input type="radio" name="login" value="fsys" '.
-         $fsyscheck.'onchange="'.$jscall.'" onclick="'.$jscall.'"'.$disabled.' />',
-         '</label><input type="password" size="10" name="fsysarg" value="" '.
-                  'onchange="'.$jscall.'"'.$disabled.' />');
+         '<label>'.$authtype,'</label>'.$autharg);
+    return $result;
+}
+
+sub authform_lti {
+    my %in = (
+              formname => 'document.cu',
+              kerb_def_dom => 'MSU.EDU',
+              @_,
+              );
+    my ($lticheck,$result,$authtype,$autharg,$jscall,$disabled);
+    my ($authnum,%can_assign) = &get_assignable_auth($in{'domain'});
+    if ($in{'readonly'}) {
+        $disabled = ' disabled="disabled"';
+    }
+    if (defined($in{'curr_authtype'})) {
+        if ($in{'curr_authtype'} eq 'lti') {
+            if ($can_assign{'lti'}) {
+                $lticheck = 'checked="checked" ';
+                if (defined($in{'mode'})) {
+                    if ($in{'mode'} eq 'modifyuser') {
+                        $lticheck = '';
+                    }
+                }
+            } else {
+                $result = &mt('Currently LTI Authenticated.');
+                return $result;
+            }
+        }
+    } else {
+        if ($authnum == 1) {
+            $authtype = '<input type="hidden" name="login" value="lti" />';
+        }
+    }
+    if (!$can_assign{'lti'}) {
+        return;
+    } elsif ($authtype eq '') {
+        if (defined($in{'mode'})) {
+            if ($in{'mode'} eq 'modifycourse') {
+                if ($authnum == 1) {
+                    $authtype = '<input type="radio" name="login" value="lti"'.$disabled.' />';
+                }
+            }
+        }
+    }
+    $jscall = "javascript:changed_radio('lti',$in{'formname'});";
+    if (($authtype eq '') && (($in{'mode'} eq 'modifycourse') || ($in{'curr_authtype'} ne 'lti'))) {
+        $authtype = '<input type="radio" name="login" value="lti" '.
+                    $lticheck.' onchange="'.$jscall.'" onclick="'.
+                    $jscall.'"'.$disabled.' />';
+    }
+    $autharg = '<input type="hidden" name="ltiarg" value="" />';
+    if ($authtype) {
+        $result = &mt('[_1] LTI Authenticated',
+                      '<label>'.$authtype.'</label>'.$autharg);
+    } else {
+        $result = '<b>'.&mt('LTI Authenticated').'</b>'.
+                  $autharg;
+    }
     return $result;
 }
 
@@ -3462,6 +3530,7 @@ sub get_assignable_auth {
                           krb5 => 1,
                           int  => 1,
                           loc  => 1,
+                          lti  => 1,
                      );
     my %domconfig = &Apache::lonnet::get_dom('configuration',['usercreation'],$dom);
     if (ref($domconfig{'usercreation'}) eq 'HASH') {
@@ -4579,9 +4648,15 @@ sub get_previous_attempt {
       }
       $prevattempts.= &end_data_table_row().&end_data_table();
     } else {
+      my $msg;
+      if ($symb =~ /ext\.tool$/) {
+          $msg = &mt('No grade passed back.');
+      } else {
+          $msg = &mt('Nothing submitted - no attempts.');
+      }
       $prevattempts=
 	  &start_data_table().&start_data_table_row().
-	  '<td>'.&mt('Nothing submitted - no attempts.').'</td>'.
+	  '<td>'.$msg.'</td>'.
 	  &end_data_table_row().&end_data_table();
     }
   } else {
@@ -4686,6 +4761,9 @@ sub get_student_view {
   }
   if (defined($target)) { $form{'grade_target'} = $target; }
   $feedurl=&Apache::lonnet::clutter($feedurl);
+  if (($feedurl =~ /ext\.tool$/) && ($target eq 'tex')) {
+      $feedurl =~ s{^/adm/wrapper}{};
+  }
   my ($userview,$response)=&Apache::lonnet::ssi_body($feedurl,%form);
   $userview=~s/\<body[^\>]*\>//gi;
   $userview=~s/\<\/body\>//gi;
@@ -5768,13 +5846,18 @@ sub CSTR_pageheader {
         $title = &mt('Authoring Space');
     }
 
+    my ($target,$crumbtarget) = (' target="_top"','_top'); #FIXME lonpubdir: target="_parent"
+    if (($env{'request.lti.login'}) && ($env{'request.lti.target'} eq 'iframe')) {
+        $target = '';
+        $crumbtarget = '';
+    }
+
     my $output =
          '<div>'
         .&Apache::loncommon::help_open_menu('','',3,'Authoring') #FIXME: Broken? Where is it?
         .'<b>'.$title.'</b> '
-        .'<form name="dirs" method="post" action="'.$formaction
-        .'" target="_top">' #FIXME lonpubdir: target="_parent"
-        .&Apache::lonhtmlcommon::crumbs($uname.'/'.$parentpath,'_top','/priv/'.$udom,undef,undef);
+        .'<form name="dirs" method="post" action="'.$formaction.'"'.$target.'>'
+        .&Apache::lonhtmlcommon::crumbs($uname.'/'.$parentpath,$crumbtarget,'/priv/'.$udom,undef,undef);
 
     if ($lastitem) {
         $output .=
@@ -5788,7 +5871,7 @@ sub CSTR_pageheader {
     } else {
         $output .=
              '<br />'
-            #FIXME lonpubdir: &Apache::lonhtmlcommon::crumbs($uname.$thisdisfn.'/','_top','/priv','','+1',1)."</b></tt><br />"
+            #FIXME lonpubdir: &Apache::lonhtmlcommon::crumbs($uname.$thisdisfn.'/',$crumbtarget,'/priv','','+1',1)."</b></tt><br />"
             .&Apache::lonhtmlcommon::select_recent('construct','recent','this.form.action=this.form.recent.value;this.form.submit()')
             .'</form>'
             .&Apache::lonmenu::constspaceform();
@@ -8653,9 +8736,16 @@ sub start_page {
                 if (@advtools > 0) {
                     &Apache::lonmenu::advtools_crumbs(@advtools);
                 }
+                my $ltiscope;
+                if (($env{'request.course.id'}) && ($env{'request.lti.login'})) {
+                    my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'};
+                    my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'};
+                    ($ltiscope) = &LONCAPA::ltiutils::lti_provider_scope($env{'request.lti.uri'},$cdom,$cnum);
+                }
                 my $menulink;
                 # if arg: bread_crumbs_nomenu is true pass 0 as $menulink item.
                 if ((exists($args->{'bread_crumbs_nomenu'})) ||
+                     ($ltiscope eq 'map') || ($ltiscope eq 'resource') ||
                      ((($args->{'crstype'} eq 'Placement') || (($env{'request.course.id'}) &&
                      ($env{'course.'.$env{'request.course.id'}.'.type'} eq 'Placement'))) &&
                      (!$env{'request.role.adv'}))) {
@@ -16176,13 +16266,14 @@ sub group_term {
 }
 
 sub course_types {
-    my @types = ('official','unofficial','community','textbook','placement');
+    my @types = ('official','unofficial','community','textbook','placement','lti');
     my %typename = (
                          official   => 'Official course',
                          unofficial => 'Unofficial course',
                          community  => 'Community',
                          textbook   => 'Textbook course',
                          placement  => 'Placement test',
+                         lti        => 'LTI provider',
                    );
     return (\@types,\%typename);
 }
@@ -16418,7 +16509,7 @@ sub init_user_environment {
                                                       undef,\%userenv,\%domdef,\%is_adv);
             }
 
-            foreach my $crstype ('official','unofficial','community','textbook','placement') {
+            foreach my $crstype ('official','unofficial','community','textbook','placement','lti') {
                 $userenv{'canrequest.'.$crstype} =
                     &Apache::lonnet::usertools_access($username,$domain,$crstype,
                                                       'reload','requestcourses',
@@ -17311,19 +17402,31 @@ sub update_content_constraints {
     my ($cdom,$cnum,$chome,$cid) = @_;
     my %curr_reqd_hash = &Apache::lonnet::userenvironment($cdom,$cnum,'internal.releaserequired');
     my ($reqdmajor,$reqdminor) = split(/\./,$curr_reqd_hash{'internal.releaserequired'});
-    my %checkresponsetypes;
+    my (%checkresponsetypes,%checkcrsrestypes);
     foreach my $key (keys(%Apache::lonnet::needsrelease)) {
         my ($item,$name,$value) = split(/:/,$key);
         if ($item eq 'resourcetag') {
             if ($name eq 'responsetype') {
                 $checkresponsetypes{$value} = $Apache::lonnet::needsrelease{$key}
             }
+        } elsif ($item eq 'course') {
+            if ($name eq 'courserestype') {
+                $checkcrsrestypes{$value} = $Apache::lonnet::needsrelease{$key};
+            }
         }
     }
     my $navmap = Apache::lonnavmaps::navmap->new();
     if (defined($navmap)) {
-        my %allresponses;
-        foreach my $res ($navmap->retrieveResources(undef,sub { $_[0]->is_problem() },1,0)) {
+        my (%allresponses,%allcrsrestypes);
+        foreach my $res ($navmap->retrieveResources(undef,sub { $_[0]->is_problem() || $_[0]->is_tool() },1,0)) {
+            if ($res->is_tool()) {
+                if ($allcrsrestypes{'exttool'}) {
+                    $allcrsrestypes{'exttool'} ++;
+                } else {
+                    $allcrsrestypes{'exttool'} = 1;
+                }
+                next;
+            }
             my %responses = $res->responseTypes();
             foreach my $key (keys(%responses)) {
                 next unless(exists($checkresponsetypes{$key}));
@@ -17336,8 +17439,24 @@ sub update_content_constraints {
                 ($reqdmajor,$reqdminor) = ($major,$minor);
             }
         }
+        foreach my $key (keys(%allcrsrestypes)) {
+            my ($major,$minor) = split(/\./,$checkcrsrestypes{$key});
+            if (($major > $reqdmajor) || ($major == $reqdmajor && $minor > $reqdminor)) {
+                ($reqdmajor,$reqdminor) = ($major,$minor);
+            }
+        }
         undef($navmap);
     }
+    my $suppmap = 'supplemental.sequence';
+    my ($suppcount,$supptools,$errors) = (0,0,0);
+    ($suppcount,$supptools,$errors) = &recurse_supplemental($cnum,$cdom,$suppmap,
+                                                            $suppcount,$supptools,$errors);
+    if ($supptools) {
+        my ($major,$minor) = split(/\./,$checkcrsrestypes{'exttool'});
+        if (($major > $reqdmajor) || ($major == $reqdmajor && $minor > $reqdminor)) {
+            ($reqdmajor,$reqdminor) = ($major,$minor);
+        }
+    }
     unless (($reqdmajor eq '') && ($reqdminor eq '')) {
         &Apache::lonnet::update_released_required($reqdmajor.'.'.$reqdminor,$cdom,$cnum,$chome,$cid);
     }
@@ -17394,7 +17513,7 @@ sub parse_supplemental_title {
 }
 
 sub recurse_supplemental {
-    my ($cnum,$cdom,$suppmap,$numfiles,$errors) = @_;
+    my ($cnum,$cdom,$suppmap,$numfiles,$numexttools,$errors) = @_;
     if ($suppmap) {
         my ($errtext,$fatal) = &LONCAPA::map::mapread('/uploaded/'.$cdom.'/'.$cnum.'/'.$suppmap);
         if ($fatal) {
@@ -17405,8 +17524,12 @@ sub recurse_supplemental {
                     my ($title,$src,$ext,$type,$status)=split(/\:/,$res);
                     if (($src ne '') && ($status eq 'res')) {
                         if ($src =~ m{^\Q/uploaded/$cdom/$cnum/\E(supplemental_\d+\.sequence)$}) {
-                            ($numfiles,$errors) = &recurse_supplemental($cnum,$cdom,$1,$numfiles,$errors);
+                            ($numfiles,$numexttools,$errors) = &recurse_supplemental($cnum,$cdom,$1,
+                                                                   $numfiles,$numexttools,$errors);
                         } else {
+                            if ($src =~ m{^/adm/$cdom/$cnum/\d+/ext\.tool$}) {
+                                $numexttools ++;
+                            }
                             $numfiles ++;
                         }
                     }
@@ -17414,7 +17537,7 @@ sub recurse_supplemental {
             }
         }
     }
-    return ($numfiles,$errors);
+    return ($numfiles,$numexttools,$errors);
 }
 
 sub symb_to_docspath {
@@ -17809,6 +17932,142 @@ sub des_decrypt {
     return $plaintext;
 }
 
+sub make_short_symbs {
+    my ($cdom,$cnum,$navmap) = @_;
+    return unless (ref($navmap));
+    my ($numnew,@errors);
+    my @toshorten = &Apache::loncommon::get_env_multiple('form.addtiny');
+    if (@toshorten) {
+        my (%maps,%resources,%titles);
+        &Apache::loncourserespicker::enumerate_course_contents($navmap,\%maps,\%resources,\%titles,
+                                                               'shorturls',$cdom,$cnum);
+        my %tocreate;
+        if (keys(%resources)) {
+            foreach my $item (sort {$a <=> $b} (@toshorten)) {
+                my $symb = $resources{$item};
+                if ($symb) {
+                    $tocreate{$cnum.'&'.$symb} = 1;
+                }
+            }
+        }
+        if (keys(%tocreate)) {
+            my %coursetiny = &Apache::lonnet::dump('tiny',$cdom,$cnum);
+            my $su = Short::URL->new(no_vowels => 1);
+            my $init = '';
+            my (%newunique,%addcourse,%courseonly,%failed);
+            # get lock on tiny db
+            my $now = time;
+            my $lockhash = {
+                                "lock\0$now" => $env{'user.name'}.
+                                                ':'.$env{'user.domain'},
+                            };
+            my $tries = 0;
+            my $gotlock = &Apache::lonnet::newput_dom('tiny',$lockhash,$cdom);
+            my ($code,$error);
+            while (($gotlock ne 'ok') && ($tries<3)) {
+                $tries ++;
+                sleep 1;
+                $gotlock = &Apache::lonnet::newput_dom('uniquecodes',$lockhash,$cdom);
+            }
+            if ($gotlock eq 'ok') {
+                $init = &shorten_symbs($cdom,$init,$su,\%coursetiny,\%tocreate,\%newunique,
+                                       \%addcourse,\%courseonly,\%failed);
+                if (keys(%failed)) {
+                    my $numfailed = scalar(keys(%failed));
+                    push(@errors,&mt('error: could not obtain unique six character URL for [quant,_1,resource]',$numfailed));
+                }
+                if (keys(%newunique)) {
+                    my $putres = &Apache::lonnet::newput_dom('tiny',\%newunique,$cdom);
+                    if ($putres eq 'ok') {
+                        $numnew = scalar(keys(%newunique));
+                        my $newputres = &Apache::lonnet::newput('tiny',\%addcourse,$cdom,$cnum);
+                        unless ($newputres eq 'ok') {
+                            push(@errors,&mt('error: could not store course look-up of short URLs'));
+                        }
+                    } else {
+                        push(@errors,&mt('error: could not store unique six character URLs'));
+                    }
+                }
+                my $dellockres = &Apache::lonnet::del_dom('tiny',["lock\0$now"],$cdom);
+                unless ($dellockres eq 'ok') {
+                    push(@errors,&mt('error: could not release lockfile'));
+                }
+            } else {
+                push(@errors,&mt('error: could not obtain lockfile'));
+            }
+            if (keys(%courseonly)) {
+                my $result = &Apache::lonnet::newput('tiny',\%courseonly,$cdom,$cnum);
+                if ($result ne 'ok') {
+                    push(@errors,&mt('error: could not update course look-up of short URLs'));
+                }
+            }
+        }
+    }
+    return ($numnew,\@errors);
+}
+
+sub shorten_symbs {
+    my ($cdom,$init,$su,$coursetiny,$tocreate,$newunique,$addcourse,$courseonly,$failed) = @_;
+    return unless ((ref($su)) && (ref($coursetiny) eq 'HASH') && (ref($tocreate) eq 'HASH') &&
+                   (ref($newunique) eq 'HASH') && (ref($addcourse) eq 'HASH') &&
+                   (ref($courseonly) eq 'HASH') && (ref($failed) eq 'HASH'));
+    my (%possibles,%collisions);
+    foreach my $key (keys(%{$tocreate})) {
+        my $num = String::CRC32::crc32($key);
+        my $tiny = $su->encode($num,$init);
+        if ($tiny) {
+            $possibles{$tiny} = $key;
+        }
+    }
+    if (!$init) {
+        $init = 1;
+    } else {
+        $init ++;
+    }
+    if (keys(%possibles)) {
+        my @posstiny = keys(%possibles);
+        my $configuname = &Apache::lonnet::get_domainconfiguser($cdom);
+        my %currtiny = &Apache::lonnet::get('tiny',\@posstiny,$cdom,$configuname);
+        if (keys(%currtiny)) {
+            foreach my $key (keys(%currtiny)) {
+                next if ($currtiny{$key} eq '');
+                if ($currtiny{$key} eq $possibles{$key}) {
+                    my ($tcnum,$tsymb) = split(/\&/,$currtiny{$key});
+                    unless (($coursetiny->{$tsymb} eq $key) || ($addcourse->{$tsymb} eq $key) || ($courseonly->{$tsymb} eq $key)) {
+                        $courseonly->{$tsymb} = $key;
+                    }
+                } else {
+                    $collisions{$possibles{$key}} = 1;
+                }
+                delete($possibles{$key});
+            }
+        }
+        foreach my $key (keys(%possibles)) {
+            $newunique->{$key} = $possibles{$key};
+            my ($tcnum,$tsymb) = split(/\&/,$possibles{$key});
+            unless (($coursetiny->{$tsymb} eq $key) || ($addcourse->{$tsymb} eq $key) || ($courseonly->{$tsymb} eq $key)) {
+                $addcourse->{$tsymb} = $key;
+            }
+        }
+    }
+    if (keys(%collisions)) {
+        if ($init <5) {
+            if (!$init) {
+                $init = 1;
+            } else {
+                $init ++;
+            }
+            $init = &shorten_symbs($cdom,$init,$su,$coursetiny,\%collisions,
+                                   $newunique,$addcourse,$courseonly,$failed);
+        } else {
+            foreach my $key (keys(%collisions)) {
+                $failed->{$key} = 1;
+            }
+        }
+    }
+    return $init;
+}
+
 1;
 __END__;