--- loncom/interface/loncommon.pm	2021/06/15 20:52:26	1.1361
+++ loncom/interface/loncommon.pm	2023/05/22 21:10:55	1.1405
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # a pile of common routines
 #
-# $Id: loncommon.pm,v 1.1361 2021/06/15 20:52:26 raeburn Exp $
+# $Id: loncommon.pm,v 1.1405 2023/05/22 21:10:55 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -61,7 +61,7 @@ use POSIX qw(strftime mktime);
 use Apache::lonmenu();
 use Apache::lonenc();
 use Apache::lonlocal;
-use Apache::lonnet();
+use Apache::lonnavmaps();
 use HTML::Entities;
 use Apache::lonhtmlcommon();
 use Apache::loncoursedata();
@@ -72,6 +72,7 @@ use Apache::lonuserstate();
 use Apache::courseclassifier();
 use LONCAPA qw(:DEFAULT :match);
 use LONCAPA::LWPReq;
+use LONCAPA::map();
 use HTTP::Request;
 use DateTime::TimeZone;
 use DateTime::Locale;
@@ -957,8 +958,8 @@ ENDSCRT
 }
 
 sub select_timezone {
-   my ($name,$selected,$onchange,$includeempty,$disabled)=@_;
-   my $output='<select name="'.$name.'" '.$onchange.$disabled.'>'."\n";
+   my ($name,$selected,$onchange,$includeempty,$id,$disabled)=@_;
+   my $output='<select name="'.$name.'" '.$id.$onchange.$disabled.'>'."\n";
    if ($includeempty) {
        $output .= '<option value=""';
        if (($selected eq '') || ($selected eq 'local')) {
@@ -1232,7 +1233,12 @@ END
         $result.=">".&mt($hashref->{$value}->{'text'})."</option>\n";
     }
     $result .= "</select>\n";
-    my %select2 = %{$hashref->{$firstdefault}->{'select2'}};
+    my %select2;
+    if (ref($hashref->{$firstdefault}) eq 'HASH') {
+        if (ref($hashref->{$firstdefault}->{'select2'}) eq 'HASH') {
+            %select2 = %{$hashref->{$firstdefault}->{'select2'}};
+        }
+    }
     $result .= $middletext;
     $result .= "<select size=\"1\" name=\"$secondselectname\"";
     if ($onchangesecond) {
@@ -1257,7 +1263,7 @@ END
 
 =pod
 
-=item * &help_open_topic($topic,$text,$stayOnPage,$width,$height,$imgid)
+=item * &help_open_topic($topic,$text,$stayOnPage,$width,$height,$imgid,$links_target)
 
 Returns a string corresponding to an HTML link to the given help
 $topic, where $topic corresponds to the name of a .tex file in
@@ -1281,10 +1287,12 @@ $imgid is the id of the img tag used for
 used in a javascript call to switch the image src.  See 
 lonhtmlcommon::htmlareaselectactive() for an example.
 
+$links_target will optionally be set to a target (_top, _parent or _self).
+
 =cut
 
 sub help_open_topic {
-    my ($topic, $text, $stayOnPage, $width, $height, $imgid) = @_;
+    my ($topic, $text, $stayOnPage, $width, $height, $imgid, $links_target) = @_;
     $text = "" if (not defined $text);
     $stayOnPage = 0 if (not defined $stayOnPage);
     $width = 500 if (not defined $width);
@@ -1307,10 +1315,13 @@ sub help_open_topic {
 
     # Add the text
     my $target = ' target="_top"';
-    if (($env{'request.lti.login'}) && ($env{'request.lti.target'} eq 'iframe')) {
+    if ($links_target) {
+        $target = ' target="'.$links_target.'"';
+    } elsif ((($env{'request.lti.login'}) && ($env{'request.lti.target'} eq 'iframe')) ||
+             (($env{'request.deeplink.login'}) && ($env{'request.deeplink.target'} eq '_self'))) {
         $target = '';
     }
-    if ($text ne "") {	
+    if ($text ne "") {
 	$template.='<span class="LC_help_open_topic">'
                   .'<a'.$target.' href="'.$link.'">'
                   .$text.'</a>';
@@ -1395,20 +1406,20 @@ ENDOUTPUT
 
 # now just updates the help link and generates a blue icon
 sub help_open_menu {
-    my ($topic,$component_help,$faq,$bug,$stayOnPage,$width,$height,$text) 
+    my ($topic,$component_help,$faq,$bug,$stayOnPage,$width,$height,$text,$links_target) 
 	= @_;    
     $stayOnPage = 1;
     my $output;
     if ($component_help) {
 	if (!$text) {
 	    $output=&help_open_topic($component_help,undef,$stayOnPage,
-				       $width,$height);
+				       $width,$height,'',$links_target);
 	} else {
 	    my $help_text;
 	    $help_text=&unescape($topic);
 	    $output='<table><tr><td>'.
 		&help_open_topic($component_help,$help_text,$stayOnPage,
-				 $width,$height).'</td></tr></table>';
+				 $width,$height,'',$links_target).'</td></tr></table>';
 	}
     }
     my $banner_link = &update_help_link($topic,$component_help,$faq,$bug,$stayOnPage);
@@ -1416,7 +1427,7 @@ sub help_open_menu {
 }
 
 sub top_nav_help {
-    my ($text) = @_;
+    my ($text,$linkattr) = @_;
     $text = &mt($text);
     my $stay_on_page = 1;
 
@@ -1430,7 +1441,7 @@ sub top_nav_help {
     if ($link) {
         return <<"END";
 $banner_link
-<a href="$link" title="$title">$text</a>
+<a href="$link" title="$title" $linkattr>$text</a>
 END
     } else {
         return '&nbsp;'.$text.'&nbsp;';
@@ -1516,23 +1527,25 @@ sub help_open_bug {
 	$link = $url;
     }
 
-    my $target = ' target="_top"';
-    if (($env{'request.lti.login'}) && ($env{'request.lti.target'} eq 'iframe')) {
-        $target = '';
+    my $target = '_top';
+    if ((($env{'request.lti.login'}) && ($env{'request.lti.target'} eq 'iframe')) ||
+        (($env{'request.deeplink.login'}) && ($env{'request.deeplink.target'} eq '_self'))) {
+        $target = '_blank';
     }
+
     # Add the text
     if ($text ne "")
     {
 	$template .= 
   "<table bgcolor='#AA3333' cellspacing='1' cellpadding='1' border='0'><tr>".
-  "<td bgcolor='#FF5555'><a".$target." href=\"$link\"><span style=\"color:#FFFFFF;font-size:10pt;\">$text</span></a>";
+  "<td bgcolor='#FF5555'><a target=\"$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 href="$link" title="$title"><img src="$bugicon" border="0" alt="(Bug: $topic)" /></a>
+ <a target="$target" href="$link" title="$title"><img src="$bugicon" border="0" alt="(Bug: $topic)" /></a>
 ENDTEMPLATE
     if ($text ne '') { $template.='</td></tr></table>' };
     return $template;
@@ -1808,8 +1821,11 @@ sub colorfuleditor_js {
             save => 'Save page to make this permanent',
         );
         &js_escape(\%js_lt);
+        my $showfile_js = &show_crsfiles_js();
         $browse_or_search = <<"END";
 
+    $showfile_js
+
     function toggleChooser(form,element,titleid,only,search) {
         var disp = 'none';
         if (document.getElementById('chooser_'+element)) {
@@ -1824,22 +1840,64 @@ sub colorfuleditor_js {
                 toggleResImport(form,element);
             }
             document.getElementById('chooser_'+element).style.display = disp;
+            var dirsel = '';
+            var filesel = '';
+            if (document.getElementById('chooser_'+element+'_crsres')) {
+                var currcrsres = document.getElementById('chooser_'+element+'_crsres').style.display;
+                if (currcrsres == 'none') {
+                    dirsel = 'coursepath_'+element;
+                    var filesel = 'coursefile_'+element;
+                    var include;
+                    if (document.getElementById('crsres_include_'+element)) {
+                        include = document.getElementById('crsres_include_'+element).value;
+                    }
+                    populateCrsSelects(form,dirsel,filesel,1,include,1,0,1,1,0);
+                }
+            }
+            if (document.getElementById('chooser_'+element+'_upload')) {
+                var currcrsupload = document.getElementById('chooser_'+element+'_upload').style.display;
+                if (currcrsupload == 'none') {
+                    dirsel = 'crsauthorpath_'+element;
+                    filesel = '';
+                    populateCrsSelects(form,dirsel,filesel,0,'',1,0,1,0,1);
+                }
+            }
         }
     }
 
-    function toggleCrsFile(form,element,numdirs) {
+    function toggleCrsFile(form,element) {
         if (document.getElementById('chooser_'+element+'_crsres')) {
             var curr = document.getElementById('chooser_'+element+'_crsres').style.display;
             if (curr == 'none') {
-                if (numdirs) {
+                if (document.getElementById('coursepath_'+element)) {
+                    var numdirs;
+                    if (document.getElementById('coursepath_'+element).length) {
+                        numdirs = document.getElementById('coursepath_'+element).length;
+                    }
+                    if ((document.getElementById('hascrsres_'+element)) &&
+                        (document.getElementById('nocrsres_'+element))) {
+                        if (numdirs) {
+                            document.getElementById('hascrsres_'+element).style.display='inline-block';
+                            document.getElementById('nocrsres_'+element).style.display='none';
+                        } else {
+                            document.getElementById('hascrsres_'+element).style.display='none';
+                            document.getElementById('nocrsres_'+element).style.display='inline-block';
+                        }
+                    }
                     form.elements['coursepath_'+element].selectedIndex = 0;
                     if (numdirs > 1) {
-                        window['select1'+element+'_changed']();
+                        var selelem = form.elements['coursefile_'+element];
+                        var i, len = selelem.options.length -1;
+                        if (len >=0) {
+                            for (i = len; i >= 0; i--) {
+                                selelem.remove(i);
+                            }
+                            selelem.options[0] = new Option('','');
+                        }
                     }
                 }
-            } 
+            }
             document.getElementById('chooser_'+element+'_crsres').style.display = 'block';
-            
         }
         if (document.getElementById('chooser_'+element+'_upload')) {
             document.getElementById('chooser_'+element+'_upload').style.display = 'none';
@@ -1850,20 +1908,20 @@ sub colorfuleditor_js {
         return;
     }
 
-    function toggleCrsUpload(form,element,numcrsdirs) {
+    function toggleCrsUpload(form,element) {
         if (document.getElementById('chooser_'+element+'_crsres')) {
             document.getElementById('chooser_'+element+'_crsres').style.display = 'none';
         }
         if (document.getElementById('chooser_'+element+'_upload')) {
             var curr = document.getElementById('chooser_'+element+'_upload').style.display;
             if (curr == 'none') {
-                if (numcrsdirs) {
-                   form.elements['crsauthorpath_'+element].selectedIndex = 0;
-                   form.elements['newsubdir_'+element][0].checked = true;
-                   toggleNewsubdir(form,element);
+                form.elements['newsubdir_'+element][0].checked = true;
+                toggleNewsubdir(form,element);
+                document.getElementById('chooser_'+element+'_upload').style.display = 'block';
+                if (document.getElementById('uploadcrsres_'+element)) {
+                    document.getElementById('uploadcrsres_'+element).value = '';
                 }
             }
-            document.getElementById('chooser_'+element+'_upload').style.display = 'block';
         }
         return;
     }
@@ -1907,19 +1965,21 @@ sub colorfuleditor_js {
         var filename = form.elements['coursefile_'+element];
         var path = directory.options[directory.selectedIndex].value;
         var file = filename.options[filename.selectedIndex].value;
-        form.elements[element].value = '$respath';
-        if (path == '/') {
-            form.elements[element].value += file;
-        } else {
-            form.elements[element].value += path+'/'+file;
-        }
-        unClean();
-        if (document.getElementById('previewimg_'+element)) {
-            document.getElementById('previewimg_'+element).src = form.elements[element].value;
-            var newsrc = document.getElementById('previewimg_'+element).src; 
-        }
-        if (document.getElementById('showimg_'+element)) {
-            document.getElementById('showimg_'+element).innerHTML = '($js_lt{save})';
+        if (file != '') {
+            form.elements[element].value = '$respath';
+            if (path == '/') {
+                form.elements[element].value += file;
+            } else {
+                form.elements[element].value += path+'/'+file;
+            }
+            unClean();
+            if (document.getElementById('previewimg_'+element)) {
+                document.getElementById('previewimg_'+element).src = form.elements[element].value;
+                var newsrc = document.getElementById('previewimg_'+element).src; 
+            }
+            if (document.getElementById('showimg_'+element)) {
+                document.getElementById('showimg_'+element).innerHTML = '($js_lt{save})';
+            }
         }
         toggleChooser(form,element);
         return;
@@ -2208,111 +2268,168 @@ sub crsauthor_url {
 }
 
 sub import_crsauthor_form {
-    my ($form,$firstselectname,$secondselectname,$onchangefirst,$only,$suffix,$disabled) = @_;
+    my ($firstselectname,$secondselectname,$onchangefirst,$only,$suffix,$disabled) = @_;
     return (0) unless ($env{'request.course.id'});
     my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'};
     my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'};
     my $crshome = $env{'course.'.$env{'request.course.id'}.'.home'};
     return (0) unless (($cnum ne '') && ($cdom ne ''));
-    my $londocroot = $Apache::lonnet::perlvar{'lonDocRoot'};
     my @ids=&Apache::lonnet::current_machine_ids();
-    my ($output,$is_home,$relpath,%subdirs,%files,%selimport_menus);
-    
+    my ($output,$is_home,$toppath,%subdirs,%files,%selimport_menus,$include,$exclude);
+
     if (grep(/^\Q$crshome\E$/,@ids)) {
         $is_home = 1;
     }
-    $relpath = "/priv/$cdom/$cnum";
-    &Apache::lonnet::recursedirs($is_home,'priv',$londocroot,$relpath,'',\%subdirs,\%files);
+    $toppath = "/priv/$cdom/$cnum";
+    my $nonemptydir = 1;
+    my $js_only;
+    if ($only) {
+        map { $include->{$_} = 1; } split(/\s*,\s*/,$only);
+        $js_only = join(',',map { &js_escape($_); } sort(keys(%{$include})));
+    }
+    $exclude = &Apache::lonnet::priv_exclude();
+    &Apache::lonnet::recursedirs($is_home,1,$include,$exclude,1,0,$toppath,'',\%subdirs,\%files);
+    my $numdirs = scalar(keys(%files));
     my %lt = &Apache::lonlocal::texthash (
         fnam => 'Filename',
         dire => 'Directory',
+        se   => 'Select',
     );
-    my $numdirs = scalar(keys(%files));
-    my (%possexts,$singledir,@singledirfiles);
-    if ($only) {
-        map { $possexts{$_} = 1; } split(/\s*,\s*/,$only);
-    }
-    my (%nonemptydirs,$possdirs);
-    if ($numdirs > 1) {
-        my @order;
-        foreach my $key (sort { lc($a) cmp lc($b) } (keys(%files))) {
-            if (ref($files{$key}) eq 'HASH') {
-                my $shown = $key;
-                if ($key eq '') {
-                    $shown = '/';
-                }
-                my @ordered = ();
-                foreach my $file (sort { lc($a) cmp lc($b) } (keys(%{$files{$key}}))) {
-                    next if ($file =~ /\.rights$/);
-                    if ($only) {
-                        my ($ext) = ($file =~ /\.([^.]+)$/);
-                        unless ($possexts{lc($ext)}) {
-                            next;
-                        }
+    $output = $lt{'dire'}.':&nbsp;'.
+              '<select id="'.$firstselectname.'" name="'.$firstselectname.'" '.
+              'onchange="populateCrsSelects(this.form,'."'$firstselectname','$secondselectname',1,'$js_only',0,1,0,0,0".');">'.
+              '<option value="" selected="selected">'.$lt{'se'}.'</option>';
+    if ($files{'/'}) {
+        $output .= '<option value="/">/</option>'."\n";
+    }
+    foreach my $key (sort { lc($a) cmp lc($b) } (keys(%files))) {
+        next if ($key eq '/');
+        $output .= '<option value="'.$key.'">'.$key.'</option>'."\n";
+    }
+    $output .= '</select><br />'."\n".
+               $lt{'fnam'}.':&nbsp;<select id="'.$secondselectname.'" name="'.$secondselectname.'">'."\n".
+               '<option value="" selected="selected"></option>'."\n".
+               '</select>'."\n".
+               '<input type="hidden" id="crsres_include_'.$suffix.'" value="'.$only.'" />';
+    return ($numdirs,$output);
+}
+
+sub show_crsfiles_js {
+    my $excluderef = &Apache::lonnet::priv_exclude();
+    my $se = &js_escape(&mt('Select'));
+    my $exclude;
+    if (ref($excluderef) eq 'HASH') {
+        $exclude = join(',', map { &js_escape($_); } sort(keys(%{$excluderef})));
+    }
+    my $js = <<"END";
+
+
+    function populateCrsSelects (form,dirsel,filesel,exc,include,setdir,setfile,recurse,nonemptydir,addtopdir) {
+        var relpath = '';
+        if ((setfile) && (dirsel != null) && (dirsel != 'undefined') && (dirsel != '')) {
+            var currdir = form.elements[dirsel].options[form.elements[dirsel].selectedIndex].value;
+            if (currdir == '') {
+                if ((filesel != null) && (filesel != 'undefined') && (filesel != '')) {
+                    selelem = form.elements[filesel];
+                    var j, numfiles = selelem.options.length -1;
+                    if (numfiles >=0) {
+                        for (j = numfiles; j >= 0; j--) {
+                            selelem.remove(j);
+                        }
+                    }
+                    if (selelem.options.length == 0) {
+                        selelem.options[selelem.options.length] = new Option('','');
+                        selelem.selectedIndex = 0;
                     }
-                    $selimport_menus{$key}->{'select2'}->{$file} = $file;
-                    push(@ordered,$file);
-                }
-                if (@ordered) {
-                    push(@order,$key);
-                    $nonemptydirs{$key} = 1;
-                    $selimport_menus{$key}->{'text'} = $shown;
-                    $selimport_menus{$key}->{'default'} = '';
-                    $selimport_menus{$key}->{'select2'}->{''} = '';
-                    $selimport_menus{$key}->{'order'} = \@ordered;
                 }
+                return;
+            } else {
+                relpath = encodeURIComponent(form.elements[dirsel].options[form.elements[dirsel].selectedIndex].value);
             }
         }
-        $possdirs = scalar(keys(%nonemptydirs));
-        if ($possdirs > 1) {
-            my @order = sort { lc($a) cmp lc($b) } (keys(%nonemptydirs));
-            $output = $lt{'dire'}.
-                      &linked_select_forms($form,'<br />'.
-                                           $lt{'fnam'},'',
-                                           $firstselectname,$secondselectname,
-                                           \%selimport_menus,\@order,
-                                           $onchangefirst,'',$suffix).'<br />';
-        } elsif ($possdirs == 1) {
-            $singledir = (keys(%nonemptydirs))[0];
-            if (ref($selimport_menus{$singledir}->{'order'}) eq 'ARRAY') {
-                @singledirfiles = @{$selimport_menus{$singledir}->{'order'}};
-            }
-            delete($selimport_menus{$singledir});
-        }
-    } elsif ($numdirs == 1) {
-        $singledir = (keys(%files))[0];
-        foreach my $file (sort { lc($a) cmp lc($b) } (keys(%{$files{$singledir}}))) {
-            if ($only) {
-                my ($ext) = ($file =~ /\.([^.]+)$/);
-                unless ($possexts{lc($ext)}) {
-                    next;
+        var http = new XMLHttpRequest();
+        var url = "/adm/courseauthor";
+        var crsrole = "$env{'request.role'}";
+        var exclude = '';
+        if (exc) {
+            exclude = '$exclude';
+        }
+        var params = "role=course&files=1&rec="+recurse+"&nonempty="+nonemptydir+"&exc="+exclude+"&inc="+include+"&addtop="+addtopdir+"&path="+relpath;
+        http.open("POST", url, true);
+        http.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
+        http.onreadystatechange = function() {
+            if (http.readyState == 4 && http.status == 200) {
+                var data = JSON.parse(http.responseText);
+                var selelem;
+                if ((setdir) && (dirsel != null) && (dirsel != 'undefined') && (dirsel != '')) {
+                    if (Array.isArray(data.dirs)) {
+                        selelem = form.elements[dirsel];
+                        var i, numdirs = selelem.options.length -1;
+                        if (numdirs >=0) {
+                            for (i = numdirs; i >= 0; i--) {
+                                selelem.remove(i);
+                            }
+                        }
+                        var len = data.dirs.length;
+                        if (len) {
+                            selelem.options[selelem.options.length] = new Option('$se','');
+                            var j;
+                            for (j = 0; j < len; j++) {
+                                selelem.options[selelem.options.length] = new Option(data.dirs[j],data.dirs[j]);
+                            }
+                            selelem.selectedIndex = 0;
+                        }
+                        if (!setfile) {
+                            if ((filesel != null) && (filesel != 'undefined') && (filesel != '')) {
+                                selelem = form.elements[filesel];
+                                var j, numfiles = selelem.options.length -1;
+                                if (numfiles >=0) {
+                                    for (j = numfiles; j >= 0; j--) {
+                                        selelem.remove(j);
+                                    }
+                                }
+                                if (selelem.options.length == 0) {
+                                    selelem.options[selelem.options.length] = new Option('','');
+                                    selelem.selectedIndex = 0;
+                                }
+                            }
+                        }
+                    }
+                }
+                if ((setfile) && (filesel != null) && (filesel != 'undefined') && (filesel != '')) {
+                    selelem = form.elements[filesel];
+                    var i, numfiles = selelem.options.length -1;
+                    if (numfiles >=0) {
+                        for (i = numfiles; i >= 0; i--) {
+                            selelem.remove(i);
+                        }
+                    }
+                    var x;
+                    for (x in data.files) {
+                        if (Array.isArray(data.files[x])) {
+                            if (data.files[x].length > 1) {
+                                selelem.options[selelem.options.length] = new Option('$se','');
+                            }
+                            var len = data.files[x].length;
+                            if (len) {
+                                var k;
+                                for (k = 0; k < len; k++) {
+                                    selelem.options[selelem.options.length] = new Option(data.files[x][k],data.files[x][k]);
+                                }
+                                selelem.selectedIndex = 0;
+                            }
+                        }
+                    }
+                    if (selelem.options.length == 0) {
+                        selelem.options[selelem.options.length] = new Option('','');
+                        selelem.selectedIndex = 0;
+                    }
                 }
-            } else {
-                next if ($file =~ /\.rights$/);
             }
-            push(@singledirfiles,$file);
-        }
-        if (@singledirfiles) {
-            $possdirs = 1;
-        }
-    }
-    if (($possdirs == 1) && (@singledirfiles)) {
-        my $showdir = $singledir;
-        if ($singledir eq '') {
-            $showdir = '/';
-        }
-        $output = $lt{'dire'}.
-                  '<select name="'.$firstselectname.'">'.
-                  '<option value="'.$singledir.'">'.$showdir.'</option>'."\n".
-                  '</select><br />'.
-                  $lt{'fnam'}.'<select name="'.$secondselectname.'">'."\n".
-                  '<option value="" selected="selected">'.$lt{'se'}.'</option>'."\n";
-        foreach my $file (@singledirfiles) {
-            $output .= '<option value="'.$file.'">'.$file.'</option>'."\n";
         }
-        $output .= '</select><br />'."\n";
+        http.send(params);
     }
-    return ($possdirs,$output);
+END
 }
 
 =pod
@@ -2664,7 +2781,7 @@ sub display_filter {
     my $onchange = "javascript:toggleHistoryOptions(this,'containingphrase','$context',
                                                     '$secondid','$thirdid')";
     return '<span class="LC_nobreak"><label>'.&mt('Records: [_1]',
-			       &Apache::lonmeta::selectbox('show',$env{'form.show'},undef,
+			       &Apache::lonmeta::selectbox('show',$env{'form.show'},'',undef,
 							   (&mt('all'),10,20,50,100,1000,10000))).
 	   '</label></span> <span class="LC_nobreak">'.
            &mt('Filter: [_1]',
@@ -3648,6 +3765,155 @@ sub check_passwd_rules {
     return $warning;
 }
 
+sub passwd_validation_js {
+    my ($currpasswdval,$domain,$context,$id) = @_;
+    my (%passwdconf,$alertmsg);
+    if ($context eq 'linkprot') {
+        my %domconfig = &Apache::lonnet::get_dom('configuration',['ltisec'],$domain);
+        if (ref($domconfig{'ltisec'}) eq 'HASH') {
+            if (ref($domconfig{'ltisec'}{'rules'}) eq 'HASH') {
+                %passwdconf = %{$domconfig{'ltisec'}{'rules'}};
+            }
+        }
+        if ($id eq 'add') {
+            $alertmsg = &mt('Secret for added launcher did not satisfy requirement(s):').'\n\n';
+        } elsif ($id =~ /^\d+$/) {
+            my $pos = $id+1;
+            $alertmsg = &mt('Secret for launcher [_1] did not satisfy requirement(s):','#'.$pos).'\n\n';
+        } else {
+            $alertmsg = &mt('A secret did not satisfy requirement(s):').'\n\n';
+        }
+    } else {
+        %passwdconf = &Apache::lonnet::get_passwdconf($domain);
+        $alertmsg = &mt('Initial password did not satisfy requirement(s):').'\n\n';
+    }
+    my ($min,$max,@chars,$numrules,$intargjs,%alert);
+    $numrules = 0;
+    $min = $Apache::lonnet::passwdmin;
+    if (ref($passwdconf{'chars'}) eq 'ARRAY') {
+        if ($passwdconf{'min'} =~ /^\d+$/) {
+            if ($passwdconf{'min'} > $min) {
+                $min = $passwdconf{'min'};
+            }
+        }
+        if ($passwdconf{'max'} =~ /^\d+$/) {
+            $max = $passwdconf{'max'};
+            $numrules ++;
+        }
+        @chars = @{$passwdconf{'chars'}};
+        if (@chars) {
+            $numrules ++;
+        }
+    }
+    if ($min > 0) {
+        $numrules ++;
+    }
+    if (($min > 0) || ($max ne '') || (@chars > 0)) {
+        if ($min) {
+            $alert{'min'} = &mt('minimum [quant,_1,character]',$min).'\n';
+        }
+        if ($max) {
+            $alert{'max'} = &mt('maximum [quant,_1,character]',$max).'\n';
+        }
+        my (@charalerts,@charrules);
+        if (@chars) {
+            if (grep(/^uc$/,@chars)) {
+                push(@charalerts,&mt('contain at least one upper case letter'));
+                push(@charrules,'uc');
+            }
+            if (grep(/^lc$/,@chars)) {
+                push(@charalerts,&mt('contain at least one lower case letter'));
+                push(@charrules,'lc');
+            }
+            if (grep(/^num$/,@chars)) {
+                push(@charalerts,&mt('contain at least one number'));
+                push(@charrules,'num');
+            }
+            if (grep(/^spec$/,@chars)) {
+                push(@charalerts,&mt('contain at least one non-alphanumeric'));
+                push(@charrules,'spec');
+            }
+        }
+        $intargjs = qq|            var rulesmsg = '';\n|.
+                    qq|            var currpwval = $currpasswdval;\n|;
+            if ($min) {
+                $intargjs .= qq|
+            if (currpwval.length < $min) {
+                rulesmsg += ' - $alert{min}';
+            }
+|;
+            }
+            if ($max) {
+                $intargjs .= qq|
+            if (currpwval.length > $max) {
+                rulesmsg += ' - $alert{max}';
+            }
+|;
+            }
+            if (@chars > 0) {
+                my $charrulestr = '"'.join('","',@charrules).'"';
+                my $charalertstr = '"'.join('","',@charalerts).'"';
+                $intargjs .= qq|            var brokerules = new Array();\n|.
+                             qq|            var charrules = new Array($charrulestr);\n|.
+                             qq|            var charalerts = new Array($charalertstr);\n|;
+                my %rules;
+                map { $rules{$_} = 1; } @chars;
+                if ($rules{'uc'}) {
+                    $intargjs .= qq|
+            var ucRegExp = /[A-Z]/;
+            if (!ucRegExp.test(currpwval)) {
+                brokerules.push('uc');
+            }
+|;
+                }
+                if ($rules{'lc'}) {
+                    $intargjs .= qq|
+            var lcRegExp = /[a-z]/;
+            if (!lcRegExp.test(currpwval)) {
+                brokerules.push('lc');
+            }
+|;
+                }
+                if ($rules{'num'}) {
+                     $intargjs .= qq|
+            var numRegExp = /[0-9]/;
+            if (!numRegExp.test(currpwval)) {
+                brokerules.push('num');
+            }
+|;
+                }
+                if ($rules{'spec'}) {
+                     $intargjs .= q|
+            var specRegExp = /[!"#$%&'()*+,\-.\/:;<=>?@[\\^\]_`{\|}~]/;
+            if (!specRegExp.test(currpwval)) {
+                brokerules.push('spec');
+            }
+|;
+                }
+                $intargjs .= qq|
+            if (brokerules.length > 0) {
+                for (var i=0; i<brokerules.length; i++) {
+                    for (var j=0; j<charrules.length; j++) {
+                        if (brokerules[i] == charrules[j]) {
+                            rulesmsg += ' - '+charalerts[j]+'\\n';
+                            break;
+                        }
+                    }
+                }
+            }
+|;
+            }
+            $intargjs .= qq|
+            if (rulesmsg != '') {
+                rulesmsg = '$alertmsg'+rulesmsg;
+                alert(rulesmsg);
+                return false;
+            }
+|;
+    }
+    return ($numrules,$intargjs);
+}
+
 ###############################################################
 ##    Get Kerberos Defaults for Domain                 ##
 ###############################################################
@@ -4128,6 +4394,30 @@ sub syllabuswrapper {
 
 # -----------------------------------------------------------------------------
 
+sub aboutme_on {
+    my ($uname,$udom)=@_;
+    unless ($uname) { $uname=$env{'user.name'}; }
+    unless ($udom)  { $udom=$env{'user.domain'}; }
+    return if ($udom eq 'public' && $uname eq 'public');
+    my $hashkey=$uname.':'.$udom;
+    my ($aboutme,$cached)=&Apache::lonnet::is_cached_new('aboutme',$hashkey);
+    if ($cached) {
+        return $aboutme;
+    }
+    $aboutme = &Apache::lonnet::usertools_access($uname,$udom,'aboutme');
+    &Apache::lonnet::do_cache_new('aboutme',$hashkey,$aboutme,3600);
+    return $aboutme;
+}
+
+sub devalidate_aboutme_cache {
+    my ($uname,$udom)=@_;
+    if (!$udom)  { $udom =$env{'user.domain'}; }
+    if (!$uname) { $uname=$env{'user.name'};   }
+    return if ($udom eq 'public' && $uname eq 'public');
+    my $id=$uname.':'.$udom;
+    &Apache::lonnet::devalidate_cache_new('aboutme',$id);
+}
+
 sub track_student_link {
     my ($linktext,$sname,$sdom,$target,$start,$only_body) = @_;
     my $link ="/adm/trackstudent?";
@@ -5201,8 +5491,90 @@ sub findallcourses {
 ###############################################
 
 sub blockcheck {
-    my ($setters,$activity,$uname,$udom,$url,$is_course,$symb,$caller) = @_;
-
+    my ($setters,$activity,$clientip,$uname,$udom,$url,$is_course,$symb,$caller) = @_;
+    unless (($activity eq 'docs') || ($activity eq 'reinit') || ($activity eq 'alert')) {
+        my ($has_evb,$check_ipaccess);
+        my $dom = $env{'user.domain'};
+        if ($env{'request.course.id'}) {
+            my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'};
+            my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'};
+            my $checkrole = "cm./$cdom/$cnum";
+            my $sec = $env{'request.course.sec'};
+            if ($sec ne '') {
+                $checkrole .= "/$sec";
+            }
+            if ((&Apache::lonnet::allowed('evb',undef,undef,$checkrole)) &&
+                ($env{'request.role'} !~ /^st/)) {
+                $has_evb = 1;
+            }
+            unless ($has_evb) {
+                if (($activity eq 'printout') || ($activity eq 'grades') || ($activity eq 'search') ||
+                    ($activity eq 'boards') || ($activity eq 'groups') || ($activity eq 'chat')) {
+                    if ($udom eq $cdom) {
+                        $check_ipaccess = 1;
+                    }
+                }
+            }
+        } elsif (($activity eq 'com') || ($activity eq 'port') || ($activity eq 'blogs') ||
+                ($activity eq 'about') || ($activity eq 'wishlist') || ($activity eq 'passwd')) {
+            my $checkrole;
+            if ($env{'request.role.domain'} eq '') {
+                $checkrole = "cm./$env{'user.domain'}/";
+            } else {
+                $checkrole = "cm./$env{'request.role.domain'}/";
+            }
+            if (($checkrole) && (&Apache::lonnet::allowed('evb',undef,undef,$checkrole))) {
+                $has_evb = 1;
+            }
+        }
+        unless ($has_evb || $check_ipaccess) {
+            my @machinedoms = &Apache::lonnet::current_machine_domains();
+            if (($dom eq 'public') && ($activity eq 'port')) {
+                $dom = $udom;
+            }
+            if (($dom ne '') && (grep(/^\Q$dom\E$/,@machinedoms))) {
+                $check_ipaccess = 1;
+            } else {
+                my $lonhost = $Apache::lonnet::perlvar{'lonHostID'};
+                my $internet_names = &Apache::lonnet::get_internet_names($lonhost);
+                my $prim = &Apache::lonnet::domain($dom,'primary');
+                my $intdom = &Apache::lonnet::internet_dom($prim);
+                if (($intdom ne '') && (ref($internet_names) eq 'ARRAY')) {
+                    if (grep(/^\Q$intdom\E$/,@{$internet_names})) {
+                        $check_ipaccess = 1;
+                    }
+                }
+            }
+        }
+        if ($check_ipaccess) {
+            my ($ipaccessref,$cached)=&Apache::lonnet::is_cached_new('ipaccess',$dom);
+            unless (defined($cached)) {
+                my %domconfig =
+                    &Apache::lonnet::get_dom('configuration',['ipaccess'],$dom);
+                $ipaccessref = &Apache::lonnet::do_cache_new('ipaccess',$dom,$domconfig{'ipaccess'},1800);
+            }
+            if ((ref($ipaccessref) eq 'HASH') && ($clientip)) {
+                foreach my $id (keys(%{$ipaccessref})) {
+                    if (ref($ipaccessref->{$id}) eq 'HASH') {
+                        my $range = $ipaccessref->{$id}->{'ip'};
+                        if ($range) {
+                            if (&Apache::lonnet::ip_match($clientip,$range)) {
+                                if (ref($ipaccessref->{$id}->{'commblocks'}) eq 'HASH') {
+                                    if ($ipaccessref->{$id}->{'commblocks'}->{$activity} eq 'on') {
+                                        return ('','','',$id,$dom);
+                                        last;
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        if (($activity eq 'wishlist') || ($activity eq 'annotate')) {
+            return ();
+        }
+    }
     if (defined($udom) && defined($uname)) {
         # If uname and udom are for a course, check for blocks in the course.
         if (($is_course) || (&Apache::lonnet::is_course($udom,$uname))) {
@@ -5218,7 +5590,10 @@ sub blockcheck {
     my $startblock = 0;
     my $endblock = 0;
     my $triggerblock = '';
-    my %live_courses = &findallcourses(undef,$uname,$udom);
+    my %live_courses;
+    unless (($activity eq 'wishlist') || ($activity eq 'annotate')) {
+        %live_courses = &findallcourses(undef,$uname,$udom);
+    }
 
     # If uname is for a user, and activity is course-specific, i.e.,
     # boards, chat or groups, check for blocking in current course only.
@@ -5503,14 +5878,17 @@ sub parse_block_record {
 }
 
 sub blocking_status {
-    my ($activity,$uname,$udom,$url,$is_course,$symb,$caller) = @_;
+    my ($activity,$clientip,$uname,$udom,$url,$is_course,$symb,$caller) = @_;
     my %setters;
 
 # check for active blocking
-    my ($startblock,$endblock,$triggerblock) = 
-        &blockcheck(\%setters,$activity,$uname,$udom,$url,$is_course,$symb,$caller);
+    if ($clientip eq '') {
+        $clientip = &Apache::lonnet::get_requestor_ip();
+    }
+    my ($startblock,$endblock,$triggerblock,$by_ip,$blockdom) = 
+        &blockcheck(\%setters,$activity,$clientip,$uname,$udom,$url,$is_course,$symb,$caller);
     my $blocked = 0;
-    if ($startblock && $endblock) {
+    if (($startblock && $endblock) || ($by_ip)) {
         $blocked = 1;
     }
 
@@ -5564,6 +5942,10 @@ END_MYBLOCK
         $text = &mt('Checking Course Update Blocked');
     } elsif ($activity eq 'about') {
         $text = &mt('Access to User Information Pages Blocked');
+    } elsif ($activity eq 'wishlist') {
+        $text = &mt('Access to Stored Links Blocked');
+    } elsif ($activity eq 'annotate') {
+        $text = &mt('Access to Annotations Blocked');
     }
     $output .= <<"END_BLOCK";
 <div class='$class'>
@@ -5745,6 +6127,17 @@ sub get_domainconf {
                                     }
                                 }
                             }
+                        } elsif ($key eq 'saml') {
+                            if (ref($domconfig{'login'}{$key}) eq 'HASH') {
+                                foreach my $host (keys(%{$domconfig{'login'}{$key}})) {
+                                    if (ref($domconfig{'login'}{$key}{$host}) eq 'HASH') {
+                                        $designhash{$udom.'.login.'.$key.'_'.$host} = 1;
+                                        foreach my $item ('text','img','alt','url','title','window','notsso') {
+                                            $designhash{$udom.'.login.'.$key.'_'.$item.'_'.$host} = $domconfig{'login'}{$key}{$host}{$item};
+                                        }
+                                    }
+                                }
+                            }
                         } else {
                             foreach my $img (keys(%{$domconfig{'login'}{$key}})) {
                                 $designhash{$udom.'.login.'.$key.'_'.$img} = 
@@ -5849,8 +6242,12 @@ sub domainlogo {
 		&Apache::lonnet::repcopy($local_name);
 	    }
 	   $imgsrc = &lonhttpdurl($imgsrc);
-        } 
-        return '<img src="'.$imgsrc.'" alt="'.$domain.'" />';
+        }
+        my $alttext = $domain;
+        if ($designhash{$domain.'.login.alttext_domlogo'} ne '') {
+            $alttext = $designhash{$domain.'.login.alttext_domlogo'};
+        }
+        return '<img src="'.$imgsrc.'" alt="'.$alttext.'" id="lclogindomlogo" />';
     } elsif (defined(&Apache::lonnet::domain($domain,'description'))) {
         return &Apache::lonnet::domain($domain,'description');
     } else {
@@ -5968,6 +6365,10 @@ sub head_subbox {
 Input: (optional) filename from which breadcrumb trail is built.
        In most cases no input as needed, as $env{'request.filename'}
        is appropriate for use in building the breadcrumb trail.
+       frameset flag
+       If page header is being requested for use in a frameset, then
+       the second (option) argument -- frameset will be true, and
+       the target attribute set for links should be target="_parent".
 
 Returns: HTML div with CSTR path and recent box
          To be included on Authoring Space pages
@@ -5975,7 +6376,7 @@ Returns: HTML div with CSTR path and rec
 =cut
 
 sub CSTR_pageheader {
-    my ($trailfile) = @_;
+    my ($trailfile,$frameset) = @_;
     if ($trailfile eq '') {
         $trailfile = $env{'request.filename'};
     }
@@ -6008,10 +6409,16 @@ 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')) {
+    my ($target,$crumbtarget) = (' target="_top"','_top');
+    if ($frameset) {
+        $target = ' target="_parent"';
+        $crumbtarget = '_parent';
+    } elsif (($env{'request.lti.login'}) && ($env{'request.lti.target'} eq 'iframe')) {
         $target = '';
         $crumbtarget = '';
+    } elsif (($env{'request.deeplink.login'}) && ($env{'request.deeplink.target'})) {
+        $target = ' target="'.$env{'request.deeplink.target'}.'"';
+        $crumbtarget = $env{'request.deeplink.target'};
     }
 
     my $output =
@@ -6029,14 +6436,14 @@ sub CSTR_pageheader {
     }
 
     if ($crsauthor) {
-        $output .= '</form>'.&Apache::lonmenu::constspaceform();
+        $output .= '</form>'.&Apache::lonmenu::constspaceform($frameset);
     } else {
         $output .=
              '<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();
+            .&Apache::lonmenu::constspaceform($frameset);
     }
     $output .= '</div>';
 
@@ -6104,6 +6511,21 @@ Inputs:
             context, this will contain a reference to hash of items
             to be included in the page header and/or inline menu.
 
+=item * $menucoll, optional argument, if specific menu collection is in
+            effect, either set as the default for the course, or set for
+            the deeplink paramater for $env{'request.deeplink.login'}
+            then $menucoll will be the number of that collection. 
+
+=item * $menuref, optional argument, reference to a hash, containing the
+            menu options included for the menu in effect, based on the
+            configuration for the numbered menu collection in use.  
+
+=item * $showncrumbsref, reference to a scalar. Calls to lonmenu::innerregister
+            within &bodytag() can result in calls to lonhtmlcommon::breadcrumbs(),
+            if so, $showncrumbsref is set there to 1, and will propagate back
+            via &bodytag() to &start_page(), to prevent lonhtmlcommon::breadcrumbs()
+            being called a second time.
+
 =back
 
 Returns: A uniform header for LON-CAPA web pages.  
@@ -6116,7 +6538,7 @@ other decorations will be returned.
 sub bodytag {
     my ($title,$function,$addentries,$bodyonly,$domain,$forcereg,
         $no_nav_bar,$bgcolor,$args,$advtoolsref,$ltiscope,$ltiuri,
-        $ltimenu,$menucoll,$menuref)=@_;
+        $ltimenu,$menucoll,$menuref,$showncrumbsref)=@_;
 
     my $public;
     if ((($env{'user.name'} eq 'public') && ($env{'user.domain'} eq 'public'))
@@ -6255,7 +6677,9 @@ sub bodytag {
             Apache::lonmenu::utilityfunctions($httphost), 'start');
 
         unless ($args->{'no_primary_menu'}) {
-            my ($left,$right) = Apache::lonmenu::primary_menu($crstype,$ltimenu,$menucoll,$menuref);
+            my ($left,$right) = Apache::lonmenu::primary_menu($crstype,$ltimenu,$menucoll,$menuref,
+                                                              $args->{'links_disabled'},
+                                                              $args->{'links_target'});
 
             if ($env{'request.noversionuri'} =~ m{^/res/adm/pages/}) {
                 if ($dc_info) {
@@ -6287,18 +6711,20 @@ sub bodytag {
             unless ($args->{'no_inline_menu'}) {
                 $bodytag .= Apache::lonmenu::secondary_menu($httphost,$ltiscope,$ltimenu,
                                                             $args->{'no_primary_menu'},
-                                                            $menucoll,$menuref);
+                                                            $menucoll,$menuref,
+                                                            $args->{'links_disabled'},
+                                                            $args->{'links_target'});
             }
             $bodytag .= Apache::lonmenu::serverform();
             $bodytag .= Apache::lonhtmlcommon::scripttag('', 'end');
             if ($env{'request.state'} eq 'construct') {
                 $bodytag .= &Apache::lonmenu::innerregister($forcereg,
-                                $args->{'bread_crumbs'},'','',$hostname,$ltiscope,$ltiuri);
+                                $args->{'bread_crumbs'},'','',$hostname,
+                                $ltiscope,$ltiuri,$showncrumbsref);
             } elsif ($forcereg) {
                 $bodytag .= &Apache::lonmenu::innerregister($forcereg,undef,
-                                                            $args->{'group'},
-                                                            $args->{'hide_buttons'},
-                                                            $hostname,$ltiscope,$ltiuri);
+                                $args->{'group'},$args->{'hide_buttons'},
+                                $hostname,$ltiscope,$ltiuri,$showncrumbsref);
             } else {
                 $bodytag .= 
                     &Apache::lonmenu::prepare_functions($env{'request.noversionuri'},
@@ -6382,8 +6808,38 @@ sub endbodytag {
     }
     if ( exists( $env{'internal.head.redirect'} ) ) {
         if (!(ref($args) eq 'HASH' && $args->{'noredirectlink'})) {
+            my ($endbodyjs,$idattr);
+            if ($env{'internal.head.to_opener'}) {
+                my $linkid = 'LC_continue_link';
+                $idattr = ' id="'.$linkid.'"';
+                my $redirect_for_js = &js_escape($env{'internal.head.redirect'});
+                $endbodyjs=<<ENDJS;
+<script type="text/javascript">
+// <![CDATA[
+function ebFunction(evt) {
+    evt.preventDefault();
+    var dest = '$redirect_for_js';
+    if (window.opener != null && !window.opener.closed) {
+        window.opener.location.href=dest;
+        window.close();
+    } else {
+        window.location.href=dest;
+    }
+    return false;
+}
+
+\$(document).ready(function () {
+  if (document.getElementById('$linkid')) {
+    var clickelem = document.getElementById('$linkid');
+    clickelem.addEventListener('click',ebFunction,false);
+  }
+});
+// ]]>
+</script>
+ENDJS
+            }
 	    $endbodytag=
-	        "<br /><a href=\"$env{'internal.head.redirect'}\">".
+	        "$endbodyjs<br /><a href=\"$env{'internal.head.redirect'}\"$idattr>".
 	        &mt('Continue').'</a>'.
 	        $endbodytag;
         }
@@ -7704,6 +8160,11 @@ fieldset {
   /* overflow: hidden; */
 }
 
+fieldset#LC_selectuser {
+    margin: 0;
+    padding: 0;
+}
+
 article.geogebraweb div {
     margin: 0;
 }
@@ -8247,6 +8708,10 @@ a#LC_content_toolbar_edittoplevel {
   background-image:url(/res/adm/pages/edittoplevel.gif);
 }
 
+a#LC_content_toolbar_printout {
+  background-image:url(/res/adm/pages/printout.gif);
+}
+
 ul#LC_toolbar li a:hover {
   background-position: bottom center;
 }
@@ -8364,6 +8829,18 @@ ul.LC_funclist li {
 		cursor:pointer;
 }
 
+.LCisDisabled {
+  cursor: not-allowed;
+  opacity: 0.5;
+}
+
+a[aria-disabled="true"] {
+  color: currentColor;
+  display: inline-block;  /* For IE11/ MS Edge bug */
+  pointer-events: none;
+  text-decoration: none;
+}
+
 pre.LC_wordwrap {
   white-space: pre-wrap;
   white-space: -moz-pre-wrap;
@@ -8537,7 +9014,13 @@ Inputs: $title - optional title for the
                                    3- whether the side effect should occur
                            (side effect of setting 
                                $env{'internal.head.redirect'} to the url 
-                               redirected too)
+                               redirected to)
+                                   4- whether the redirect target should be
+                                      the opener of the current (pop-up)
+                                      window (side effect of setting
+                                      $env{'internal.head.to_opener'} to
+                                      1, if true.
+                                   5- whether encrypt check should be skipped
             domain         -> force to color decorate a page for a specific
                                domain
             function       -> force usage of a specific rolish color scheme
@@ -8600,15 +9083,45 @@ sub headtag {
         }
     }
     if (ref($args->{'redirect'})) {
-	my ($time,$url,$inhibit_continue) = @{$args->{'redirect'}};
-	$url = &Apache::lonenc::check_encrypt($url);
+	my ($time,$url,$inhibit_continue,$to_opener,$skip_enc_check) = @{$args->{'redirect'}};
+        if (!$skip_enc_check) {
+            $url = &Apache::lonenc::check_encrypt($url);
+        }
 	if (!$inhibit_continue) {
 	    $env{'internal.head.redirect'} = $url;
 	}
-	$result.=<<ADDMETA
+	$result.=<<"ADDMETA";
 <meta http-equiv="pragma" content="no-cache" />
+ADDMETA
+        if ($to_opener) {
+            $env{'internal.head.to_opener'} = 1;
+            my $dest = &js_escape($url);
+            my $timeout = int($time * 1000);
+            $result .=<<"ENDJS";
+<script type="text/javascript">
+// <![CDATA[
+function LC_To_Opener() {
+    var dest = '$dest';
+    if (dest != '') {
+        if (window.opener != null && !window.opener.closed) {
+            window.opener.location.href=dest;
+            window.close();
+        } else {
+            window.location.href=dest;
+        }
+    }
+}
+\$(document).ready(function () {
+    setTimeout('LC_To_Opener()',$timeout);
+});
+// ]]>
+</script>
+ENDJS
+        } else {
+            $result.=<<"ADDMETA";
 <meta http-equiv="Refresh" content="$time; url=$url" />
 ADDMETA
+        }
     } else {
         unless (($args->{'frameset'}) || ($args->{'js_ready'}) || ($args->{'only_body'}) || ($args->{'no_nav_bar'})) {
             my $requrl = $env{'request.uri'};
@@ -8833,7 +9346,8 @@ sub print_suppression {
         }
         my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'};
         my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'};
-        my $blocked = &blocking_status('printout',$cnum,$cdom,undef,1);
+        my $clientip = &Apache::lonnet::get_requestor_ip();
+        my $blocked = &blocking_status('printout',$clientip,$cnum,$cdom,undef,1);
         if ($blocked) {
             my $checkrole = "cm./$cdom/$cnum";
             if ($env{'request.course.sec'} ne '') {
@@ -8952,6 +9466,10 @@ $args - additional optional args support
                                will contain https://<hostname> if server uses
                                https (as per hosts.tab), but request is for http
              hostname       -> hostname, originally from $r->hostname(), (optional).
+             links_disabled -> Links in primary and secondary menus are disabled
+                               (Can enable them once page has loaded - see lonroles.pm
+                               for an example).
+             links_target   -> Target for links, e.g., _parent (optional).
 
 =back
 
@@ -9040,6 +9558,7 @@ sub start_page {
         }
     }
 
+    my $showncrumbs;
     if (! exists($args->{'skip_phases'}{'body'}) ) {
 	if ($args->{'frameset'}) {
 	    my $attr_string = &make_attr_string($args->{'force_register'},
@@ -9052,7 +9571,8 @@ sub start_page {
                          $args->{'only_body'},      $args->{'domain'},
                          $args->{'force_register'}, $args->{'no_nav_bar'},
                          $args->{'bgcolor'},        $args,
-                         \@advtools,$ltiscope,$ltiuri,\%ltimenu,$menucoll,\%menu);
+                         \@advtools,$ltiscope,$ltiuri,\%ltimenu,$menucoll,
+                         \%menu,\$showncrumbs);
         }
     }
 
@@ -9074,6 +9594,7 @@ sub start_page {
 
     #Breadcrumbs
     if (exists($args->{'bread_crumbs'}) or exists($args->{'bread_crumbs_component'})) {
+        unless ($showncrumbs) {
 		&Apache::lonhtmlcommon::clear_breadcrumbs();
 		#if any br links exists, add them to the breadcrumbs
 		if (exists($args->{'bread_crumbs'}) and ref($args->{'bread_crumbs'}) eq 'ARRAY') {         
@@ -9096,12 +9617,20 @@ sub start_page {
                 } else {
                     undef($menulink);
                 }
+                my $linkprotout;
+                if ($env{'request.deeplink.login'}) {
+                    my $linkprotout = &Apache::lonmenu::linkprot_exit();
+                    if ($linkprotout) {
+                        &Apache::lonhtmlcommon::add_breadcrumb_tool('tools',$linkprotout);
+                    }
+                }
 		#if bread_crumbs_component exists show it as headline else show only the breadcrumbs
 		if(exists($args->{'bread_crumbs_component'})){
 			$result .= &Apache::lonhtmlcommon::breadcrumbs($args->{'bread_crumbs_component'},'',$menulink);
                 } else {
 			$result .= &Apache::lonhtmlcommon::breadcrumbs('','',$menulink);
 		}
+        }
     }
     return $result;
 }
@@ -9142,11 +9671,45 @@ sub menucoll_in_effect {
     my ($menucoll,$deeplinkmenu,%menu);
     if ($env{'request.course.id'}) {
         $menucoll = $env{'course.'.$env{'request.course.id'}.'.menudefault'};
-        if (($env{'request.deeplink.login'}) &&
-            ($env{'request.noversionuri'} =~ m{^/(res|uploaded)/})) {
-            my $deeplink = &Apache::lonnet::EXT('resource.0.deeplink');
+        if ($env{'request.deeplink.login'}) {
+            my ($deeplink_symb,$deeplink,$check_login_symb);
+            my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'};
+            my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'};
+            if ($env{'request.noversionuri'} =~ m{^/(res|uploaded)/}) {
+                if ($env{'request.noversionuri'} =~ /\.(page|sequence)$/) {
+                    my $navmap = Apache::lonnavmaps::navmap->new();
+                    if (ref($navmap)) {
+                        $deeplink = $navmap->get_mapparam(undef,
+                                                          &Apache::lonnet::declutter($env{'request.noversionuri'}),
+                                                          '0.deeplink');
+                    } else {
+                        $check_login_symb = 1;
+                    }
+                } else {
+                    my $symb = &Apache::lonnet::symbread();
+                    if ($symb) {
+                        $deeplink = &Apache::lonnet::EXT('resource.0.deeplink',$symb);
+                    } else {
+                        $check_login_symb = 1;
+                    }
+                }
+            } else {
+                $check_login_symb = 1;
+            }
+            if ($check_login_symb) {
+                $deeplink_symb = &deeplink_login_symb($cnum,$cdom);
+                if ($deeplink_symb =~ /\.(page|sequence)$/) {
+                    my $mapname = &Apache::lonnet::deversion((&Apache::lonnet::decode_symb($deeplink_symb))[2]);
+                    my $navmap = Apache::lonnavmaps::navmap->new();
+                    if (ref($navmap)) {
+                        $deeplink = $navmap->get_mapparam(undef,$mapname,'0.deeplink');
+                    }
+                } else {
+                    $deeplink = &Apache::lonnet::EXT('resource.0.deeplink',$deeplink_symb);
+                }
+            }
             if ($deeplink ne '') {
-                my ($listed,$scope,$access,$display) = split(/,/,$deeplink);
+                my ($state,$others,$listed,$scope,$protect,$display,$target) = split(/,/,$deeplink);
                 if ($display =~ /^\d+$/) {
                     $deeplinkmenu = 1;
                     $menucoll = $display;
@@ -9160,6 +9723,91 @@ sub menucoll_in_effect {
     return ($menucoll,$deeplinkmenu,\%menu);
 }
 
+sub deeplink_login_symb {
+    my ($cnum,$cdom) = @_;
+    my $login_symb;
+    if ($env{'request.deeplink.login'}) {
+        $login_symb = &symb_from_tinyurl($env{'request.deeplink.login'},$cnum,$cdom);
+    }
+    return $login_symb;
+}
+
+sub symb_from_tinyurl {
+    my ($url,$cnum,$cdom) = @_;
+    if ($url =~ m{^\Q/tiny/$cdom/\E(\w+)$}) {
+        my $key = $1;
+        my ($tinyurl,$login);
+        my ($result,$cached)=&Apache::lonnet::is_cached_new('tiny',$cdom."\0".$key);
+        if (defined($cached)) {
+            $tinyurl = $result;
+        } else {
+            my $configuname = &Apache::lonnet::get_domainconfiguser($cdom);
+            my %currtiny = &Apache::lonnet::get('tiny',[$key],$cdom,$configuname);
+            if ($currtiny{$key} ne '') {
+                $tinyurl = $currtiny{$key};
+                &Apache::lonnet::do_cache_new('tiny',$cdom."\0".$key,$currtiny{$key},600);
+            }
+        }
+        if ($tinyurl ne '') {
+            my ($cnumreq,$symb) = split(/\&/,$tinyurl);
+            if (wantarray) {
+                return ($cnumreq,$symb);
+            } elsif ($cnumreq eq $cnum) {
+                return $symb;
+            }
+        }
+    }
+    if (wantarray) {
+        return ();
+    } else {
+        return;
+    }
+}
+
+sub usable_exttools {
+    my %tooltypes;
+    if ($env{'request.course.id'}) {
+        if ($env{'course.'.$env{'request.course.id'}.'.internal.exttool'}) {
+           if ($env{'course.'.$env{'request.course.id'}.'.internal.exttool'} eq 'both') {
+               %tooltypes = (
+                             crs => 1,
+                             dom => 1,
+                            );
+           } elsif ($env{'course.'.$env{'request.course.id'}.'.internal.exttool'} eq 'crs') {
+               $tooltypes{'crs'} = 1;
+           } elsif ($env{'course.'.$env{'request.course.id'}.'.internal.exttool'} eq 'dom') {
+               $tooltypes{'dom'} = 1;
+           }
+        } else {
+            my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'};
+            my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'};
+            my $crstype = lc($env{'course.'.$env{'request.course.id'}.'.type'});
+            if ($crstype eq '') {
+                $crstype = 'course';
+            }
+            if ($crstype eq 'course') {
+                if ($env{'course.'.$env{'request.course.id'}.'internal.coursecode'}) {
+                    $crstype = 'official';
+                } elsif ($env{'course.'.$env{'request.course.id'}.'.internal.textbook'}) {
+                    $crstype = 'textbook';
+                } elsif ($env{'course.'.$env{'request.course.id'}.'.internal.lti'}) {
+                    $crstype = 'lti';
+                } else {
+                    $crstype = 'unofficial';
+                }
+            }
+            my %domdefaults = &Apache::lonnet::get_domain_defaults($cdom);
+            if ($domdefaults{$crstype.'domexttool'}) {
+                $tooltypes{'dom'} = 1;
+            }
+            if ($domdefaults{$crstype.'exttool'}) {
+                $tooltypes{'crs'} = 1;
+            }
+        }
+    }
+    return %tooltypes;
+}
+
 sub wishlist_window {
     return(<<'ENDWISHLIST');
 <script type="text/javascript">
@@ -9249,7 +9897,15 @@ ENDLINK
 }
 
 sub modal_adhoc_script {
-    my ($funcname,$width,$height,$content)=@_;
+    my ($funcname,$width,$height,$content,$possmathjax)=@_;
+    my $mathjax;
+    if ($possmathjax) {
+        $mathjax = <<'ENDJAX';
+               if (typeof MathJax == 'object') {
+                   MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
+               }
+ENDJAX
+    }
     return (<<ENDADHOC);
 <script type="text/javascript">
 // <![CDATA[
@@ -9260,6 +9916,7 @@ sub modal_adhoc_script {
                 modalWindow.height = $height;
                 modalWindow.content = '$content';
                 modalWindow.open();
+                $mathjax
         };  
 // ]]>
 </script>
@@ -9267,7 +9924,7 @@ ENDADHOC
 }
 
 sub modal_adhoc_inner {
-    my ($funcname,$width,$height,$content)=@_;
+    my ($funcname,$width,$height,$content,$possmathjax)=@_;
     my $innerwidth=$width-20;
     $content=&js_ready(
                  &start_page('Dialog',undef,{'only_body'=>1,'bgcolor'=>'#FFFFFF'}).
@@ -9276,12 +9933,12 @@ sub modal_adhoc_inner {
                  &end_scrollbox().
                  &end_page()
              );
-    return &modal_adhoc_script($funcname,$width,$height,$content);
+    return &modal_adhoc_script($funcname,$width,$height,$content,$possmathjax);
 }
 
 sub modal_adhoc_window {
-    my ($funcname,$width,$height,$content,$linktext)=@_;
-    return &modal_adhoc_inner($funcname,$width,$height,$content).
+    my ($funcname,$width,$height,$content,$linktext,$possmathjax)=@_;
+    return &modal_adhoc_inner($funcname,$width,$height,$content,$possmathjax).
            "<a href=\"javascript:$funcname();void(0);\">".$linktext."</a>";
 }
 
@@ -13813,7 +14470,9 @@ sub process_extracted_files {
                             my $url = '/uploaded/'.$docudom.'/'.$docuname.'/'.
                                       $docstype.'/'.$mapinner{$outer}.'/'.$newidx.'/'.
                                       $title;
-                            if (($outer !~ /\D/) && ($mapinner{$outer} !~ /\D/) && ($newidx !~ /\D/)) {
+                            if (($outer !~ /\D/) &&
+                                (($mapinner{$outer} eq 'default') || ($mapinner{$outer} !~ /\D/)) &&
+                                ($newidx !~ /\D/)) {
                                 if (!-e "$prefix$dir/$docstype/$mapinner{$outer}") {
                                     mkdir("$prefix$dir/$docstype/$mapinner{$outer}",0755);
                                 }
@@ -15869,26 +16528,29 @@ sub assign_category_rows {
 
 sub commit_customrole {
     my ($udom,$uname,$url,$three,$four,$five,$start,$end,$context) = @_;
+    my $result = &Apache::lonnet::assigncustomrole(
+                     $udom,$uname,$url,$three,$four,$five,$end,$start,undef,undef,$context);
     my $output = &mt('Assigning custom role').' "'.$five.'" by '.$four.':'.$three.' in '.$url.
                          ($start?', '.&mt('starting').' '.localtime($start):'').
-                         ($end?', ending '.localtime($end):'').': <b>'.
-              &Apache::lonnet::assigncustomrole(
-                 $udom,$uname,$url,$three,$four,$five,$end,$start,undef,undef,$context).
-                 '</b><br />';
-    return $output;
+                         ($end?', ending '.localtime($end):'').': <b>'.$result.'</b><br />';
+    if (wantarray) {
+        return ($output,$result);
+    } else {
+        return $output;
+    }
 }
 
 sub commit_standardrole {
     my ($udom,$uname,$url,$three,$start,$end,$one,$two,$sec,$context,$credits) = @_;
-    my ($output,$logmsg,$linefeed);
+    my ($output,$logmsg,$linefeed,$result);
     if ($context eq 'auto') {
         $linefeed = "\n";
     } else {
         $linefeed = "<br />\n";
     }  
     if ($three eq 'st') {
-        my $result = &commit_studentrole(\$logmsg,$udom,$uname,$url,$three,$start,$end,
-                                         $one,$two,$sec,$context,$credits);
+        $result = &commit_studentrole(\$logmsg,$udom,$uname,$url,$three,$start,$end,
+                                      $one,$two,$sec,$context,$credits);
         if (($result =~ /^error/) || ($result eq 'not_in_class') || 
             ($result eq 'unknown_course') || ($result eq 'refused')) {
             $output = $logmsg.' '.&mt('Error: ').$result."\n"; 
@@ -15908,14 +16570,18 @@ sub commit_standardrole {
         $output = &mt('Assigning').' '.$three.' in '.$url.
                ($start?', '.&mt('starting').' '.localtime($start):'').
                ($end?', '.&mt('ending').' '.localtime($end):'').': ';
-        my $result = &Apache::lonnet::assignrole($udom,$uname,$url,$three,$end,$start,'','',$context);
+        $result = &Apache::lonnet::assignrole($udom,$uname,$url,$three,$end,$start,'','',$context);
         if ($context eq 'auto') {
             $output .= $result.$linefeed;
         } else {
             $output .= '<b>'.$result.'</b>'.$linefeed;
         }
     }
-    return $output;
+    if (wantarray) {
+        return ($output,$result);
+    } else {
+        return $output;
+    }
 }
 
 sub commit_studentrole {
@@ -15944,7 +16610,7 @@ sub commit_studentrole {
                 }
                 $oldsecurl = $uurl;
                 $expire_role_result = 
-                    &Apache::lonnet::assignrole($udom,$uname,$uurl,'st',$now,'','',$context);
+                    &Apache::lonnet::assignrole($udom,$uname,$uurl,'st',$now,'','','',$context);
                 if ($env{'request.course.sec'} ne '') { 
                     if ($expire_role_result eq 'refused') {
                         my @roles = ('st');
@@ -16340,6 +17006,9 @@ sub construct_course {
     if ($args->{'crstype'}) {
         $cenv{'type'}=$args->{'crstype'};
     }
+    if ($args->{'lti'}) {
+        $cenv{'internal.lti'}=$args->{'lti'};
+    }
     if ($args->{'crsid'}) {
         $cenv{'courseid'}=$args->{'crsid'};
     }
@@ -16583,7 +17252,6 @@ sub construct_course {
 #
     unless (($args->{'nonstandard'}) || ($args->{'firstres'} eq 'blank')
 	    || ($cloneid)) {
-	use LONCAPA::map;
 	$outcome .= &mt('Setting first resource').': ';
 
 	my $map = '/uploaded/'.$$crsudom.'/'.$$crsunum.'/default.sequence';
@@ -16970,7 +17638,7 @@ sub init_user_environment {
             my %is_adv = ( is_adv => $env{'user.adv'} );
             my %domdef = &Apache::lonnet::get_domain_defaults($domain);
 
-            foreach my $tool ('aboutme','blog','webdav','portfolio') {
+            foreach my $tool ('aboutme','blog','webdav','portfolio','timezone') {
                 $userenv{'availabletools.'.$tool} = 
                     &Apache::lonnet::usertools_access($username,$domain,$tool,'reload',
                                                       undef,\%userenv,\%domdef,\%is_adv);
@@ -17839,34 +18507,58 @@ sub needs_coursereinit {
     }
     if (($now-$env{'request.course.timechecked'})>$interval) {
         &Apache::lonnet::appenv({'request.course.timechecked'=>$now});
-        my $blocked = &blocking_status('reinit',$cnum,$cdom,undef,1);
+        my $blocked = &blocking_status('reinit',undef,$cnum,$cdom,undef,1);
         if ($blocked) {
             return ();
         }
-        my $lastchange = &Apache::lonnet::get_coursechange($cdom,$cnum);
-        if ($lastchange > $env{'request.course.tied'}) {
-            my %curr_reqd_hash = &Apache::lonnet::userenvironment($cdom,$cnum,'internal.releaserequired');
-            if ($curr_reqd_hash{'internal.releaserequired'} ne '') {
-                my $required = $env{'course.'.$cdom.'_'.$cnum.'.internal.releaserequired'};
-                if ($curr_reqd_hash{'internal.releaserequired'} ne $required) {
-                    &Apache::lonnet::appenv({'course.'.$cdom.'_'.$cnum.'.internal.releaserequired' =>
-                                             $curr_reqd_hash{'internal.releaserequired'}});
-                    my ($switchserver,$switchwarning) =
-                        &check_release_required($loncaparev,$cdom.'_'.$cnum,$env{'request.role'},
-                                                $curr_reqd_hash{'internal.releaserequired'});
-                    if ($switchwarning ne '' || $switchserver ne '') {
-                        return ('switch',$switchwarning,$switchserver);
-                    }
+        my $update;
+        my $lastmainchange = &Apache::lonnet::get_coursechange($cdom,$cnum);
+        my $lastsuppchange = &Apache::lonnet::get_suppchange($cdom,$cnum);
+        if ($lastmainchange > $env{'request.course.tied'}) {
+            my ($needswitch,$switchwarning,$switchserver) = &switch_for_update($loncaparev,$cdom,$cnum);
+            if ($needswitch) {
+                return ('switch',$switchwarning,$switchserver);
+            }
+            $update = 'main';
+        }
+        if ($lastsuppchange > $env{'request.course.suppupdated'}) {
+            if ($update) {
+                $update = 'both';
+            } else {
+                my ($needswitch,$switchwarning,$switchserver) = &switch_for_update($loncaparev,$cdom,$cnum);
+                if ($needswitch) {
+                    return ('switch',$switchwarning,$switchserver);
+                } else {
+                    $update = 'supp';
                 }
             }
-            return ('update');
+            return ($update);
+        }
+    }
+    return ();
+}
+
+sub switch_for_update {
+    my ($loncaparev,$cdom,$cnum) = @_;
+    my %curr_reqd_hash = &Apache::lonnet::userenvironment($cdom,$cnum,'internal.releaserequired');
+    if ($curr_reqd_hash{'internal.releaserequired'} ne '') {
+        my $required = $env{'course.'.$cdom.'_'.$cnum.'.internal.releaserequired'};
+        if ($curr_reqd_hash{'internal.releaserequired'} ne $required) {
+            &Apache::lonnet::appenv({'course.'.$cdom.'_'.$cnum.'.internal.releaserequired' =>
+                                    $curr_reqd_hash{'internal.releaserequired'}});
+            my ($switchserver,$switchwarning) =
+                &check_release_required($loncaparev,$cdom.'_'.$cnum,$env{'request.role'},
+                                        $curr_reqd_hash{'internal.releaserequired'});
+            if ($switchwarning ne '' || $switchserver ne '') {
+                return ('switch',$switchwarning,$switchserver);
+            }
         }
     }
     return ();
 }
 
 sub update_content_constraints {
-    my ($cdom,$cnum,$chome,$cid,$keeporder) = @_;
+    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,%checkcrsrestypes);
@@ -17914,25 +18606,7 @@ sub update_content_constraints {
         }
         undef($navmap);
     }
-    my (@resources,@order,@resparms,@zombies);
-    if ($keeporder) {
-        use LONCAPA::map;
-        @resources = @LONCAPA::map::resources;
-        @order = @LONCAPA::map::order;
-        @resparms = @LONCAPA::map::resparms;
-        @zombies = @LONCAPA::map::zombies;
-    }
-    my $suppmap = 'supplemental.sequence';
-    my ($suppcount,$supptools,$errors) = (0,0,0);
-    ($suppcount,$supptools,$errors) = &recurse_supplemental($cnum,$cdom,$suppmap,
-                                                            $suppcount,$supptools,$errors);
-    if ($keeporder) {
-        @LONCAPA::map::resources = @resources;
-        @LONCAPA::map::order = @order;
-        @LONCAPA::map::resparms = @resparms;
-        @LONCAPA::map::zombies = @zombies;
-    }
-    if ($supptools) {
+    if (&Apache::lonnet::count_supptools($cnum,$cdom,1)) {
         my ($major,$minor) = split(/\./,$checkcrsrestypes{'exttool'});
         if (($major > $reqdmajor) || ($major == $reqdmajor && $minor > $reqdminor)) {
             ($reqdmajor,$reqdminor) = ($major,$minor);
@@ -17958,7 +18632,7 @@ sub allmaps_incourse {
     if ($lastchange > $env{'request.course.tied'}) {
         my ($furl,$ferr) = &Apache::lonuserstate::readmap("$cdom/$cnum");
         unless ($ferr) {
-            &update_content_constraints($cdom,$cnum,$chome,$cid,1);
+            &update_content_constraints($cdom,$cnum,$chome,$cid);
         }
     }
     my $navmap = Apache::lonnavmaps::navmap->new();
@@ -17984,8 +18658,10 @@ sub parse_supplemental_title {
         my $name =  &plainname($uname,$udom);
         $name = &HTML::Entities::encode($name,'"<>&\'');
         $renametitle = &HTML::Entities::encode($renametitle,'"<>&\'');
-        $title='<i>'.&Apache::lonlocal::locallocaltime($time).'</i> '.
-            $name.': <br />'.$foldertitle;
+        $title='<i>'.&Apache::lonlocal::locallocaltime($time).'</i> '.$name;
+        if ($foldertitle ne '') {
+            $title .= ': <br />'.$foldertitle;
+        }
     }
     if (wantarray) {
         return ($title,$foldertitle,$renametitle);
@@ -17993,32 +18669,147 @@ sub parse_supplemental_title {
     return $title;
 }
 
+sub get_supplemental {
+    my ($cnum,$cdom,$ignorecache,$possdel)=@_;
+    my $hashid=$cnum.':'.$cdom;
+    my ($supplemental,$cached,$set_httprefs);
+    unless ($ignorecache) {
+        ($supplemental,$cached) = &Apache::lonnet::is_cached_new('supplemental',$hashid);
+    }
+    unless (defined($cached)) {
+        my $chome=&Apache::lonnet::homeserver($cnum,$cdom);
+        unless ($chome eq 'no_host') {
+            my @order = @LONCAPA::map::order;
+            my @resources = @LONCAPA::map::resources;
+            my @resparms = @LONCAPA::map::resparms;
+            my @zombies = @LONCAPA::map::zombies;
+            my ($errors,%ids,%hidden);
+            $errors =
+                &recurse_supplemental($cnum,$cdom,'supplemental.sequence',
+                                      $errors,$possdel,\%ids,\%hidden);
+            @LONCAPA::map::order = @order;
+            @LONCAPA::map::resources = @resources;
+            @LONCAPA::map::resparms = @resparms;
+            @LONCAPA::map::zombies = @zombies;
+            $set_httprefs = 1;
+            if ($env{'request.course.id'} eq $cdom.'_'.$cnum) {
+                &Apache::lonnet::appenv({'request.course.suppupdated' => time});
+            }
+            $supplemental = {
+                               ids => \%ids,
+                               hidden => \%hidden,
+                            };
+            &Apache::lonnet::do_cache_new('supplemental',$hashid,$supplemental,600);
+        }
+    }
+    return ($supplemental,$set_httprefs);
+}
+
 sub recurse_supplemental {
-    my ($cnum,$cdom,$suppmap,$numfiles,$numexttools,$errors) = @_;
-    if ($suppmap) {
+    my ($cnum,$cdom,$suppmap,$errors,$possdel,$suppids,$hiddensupp,$hidden) = @_;
+    if (($suppmap) && (ref($suppids) eq 'HASH') && (ref($hiddensupp) eq 'HASH')) {
+        my $mapnum;
+        if ($suppmap eq 'supplemental.sequence') {
+            $mapnum = 0;
+        } else {
+            ($mapnum) = ($suppmap =~ /^supplemental_(\d+)\.sequence$/);
+        }
         my ($errtext,$fatal) = &LONCAPA::map::mapread('/uploaded/'.$cdom.'/'.$cnum.'/'.$suppmap);
         if ($fatal) {
             $errors ++;
         } else {
-            if ($#LONCAPA::map::resources > 0) {
-                foreach my $res (@LONCAPA::map::resources) {
-                    my ($title,$src,$ext,$type,$status)=split(/\:/,$res);
+            my @order = @LONCAPA::map::order;
+            if (@order > 0) {
+                my @resources = @LONCAPA::map::resources;
+                my @resparms = @LONCAPA::map::resparms;
+                foreach my $idx (@order) {
+                    my ($title,$src,$ext,$type,$status)=split(/\:/,$resources[$idx]);
                     if (($src ne '') && ($status eq 'res')) {
+                        my $id = $mapnum.':'.$idx;
+                        push(@{$suppids->{$src}},$id);
+                        if (($hidden) || (&get_supp_parameter($resparms[$idx],'parameter_hiddenresource') =~ /^yes/i)) {
+                            $hiddensupp->{$id} = 1;
+                        }
                         if ($src =~ m{^\Q/uploaded/$cdom/$cnum/\E(supplemental_\d+\.sequence)$}) {
-                            ($numfiles,$numexttools,$errors) = &recurse_supplemental($cnum,$cdom,$1,
-                                                                   $numfiles,$numexttools,$errors);
+                            $errors = &recurse_supplemental($cnum,$cdom,$1,$errors,$possdel,$suppids,
+                                                            $hiddensupp,$hiddensupp->{$id});
                         } else {
-                            if ($src =~ m{^/adm/$cdom/$cnum/\d+/ext\.tool$}) {
-                                $numexttools ++;
+                            my $allowed;
+                            if (($env{'request.role.adv'}) || (!$hiddensupp->{$id})) {
+                                $allowed = 1;
+                            } elsif ($possdel) {
+                                foreach my $item (@{$suppids->{$src}}) {
+                                    next if ($item eq $id);
+                                    unless ($hiddensupp->{$item}) {
+                                       $allowed = 1;
+                                       last;
+                                    }
+                                }
+                                if ((!$allowed) && (exists($env{'httpref.'.$src}))) {
+                                    &Apache::lonnet::delenv('httpref.'.$src);
+                                }
+                            }
+                            if ($allowed && (!exists($env{'httpref.'.$src}))) {
+                                &Apache::lonnet::allowuploaded('/adm/coursedoc',$src);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+    return $errors;
+}
+
+sub set_supp_httprefs {
+    my ($cnum,$cdom,$supplemental,$possdel) = @_;
+    if (ref($supplemental) eq 'HASH') {
+        if ((ref($supplemental->{'ids'}) eq 'HASH') && (ref($supplemental->{'hidden'}) eq 'HASH')) {
+            foreach my $src (keys(%{$supplemental->{'ids'}})) {
+                next if ($src =~ /\.sequence$/);
+                if (ref($supplemental->{'ids'}->{$src}) eq 'ARRAY') {
+                    my $allowed;
+                    if ($env{'request.role.adv'}) {
+                        $allowed = 1;
+                    } else {
+                        foreach my $id (@{$supplemental->{'ids'}->{$src}}) {
+                            unless ($supplemental->{'hidden'}->{$id}) {
+                                $allowed = 1;
+                                last;
+                            }
+                        }
+                    }
+                    if (exists($env{'httpref.'.$src})) {
+                        if ($possdel) {
+                            unless ($allowed) {
+                                &Apache::lonnet::delenv('httpref.'.$src);
                             }
-                            $numfiles ++;
                         }
+                    } elsif ($allowed) {
+                        &Apache::lonnet::allowuploaded('/adm/coursedoc',$src);
                     }
                 }
             }
+            if ($env{'request.course.id'} eq $cdom.'_'.$cnum) {
+                &Apache::lonnet::appenv({'request.course.suppupdated' => time});
+            }
+        }
+    }
+}
+
+sub get_supp_parameter {
+    my ($resparm,$name)=@_;
+    return if ($resparm eq '');
+    my $value=undef;
+    my $ptype=undef;
+    foreach (split('&&&',$resparm)) {
+        my ($thistype,$thisname,$thisvalue)=split('___',$_);
+        if ($thisname eq $name) {
+            $value=$thisvalue;
+            $ptype=$thistype;
         }
     }
-    return ($numfiles,$numexttools,$errors);
+    return $value;
 }
 
 sub symb_to_docspath {
@@ -18091,6 +18882,67 @@ sub symb_to_docspath {
     return $path;
 }
 
+sub validate_folderpath {
+    my ($supplementalflag,$allowed,$coursenum,$coursedom) = @_;
+    if ($env{'form.folderpath'} ne '') {
+        my @items = split(/\&/,$env{'form.folderpath'});
+        my ($badpath,$changed,$got_supp,$supppath,%supphidden,%suppids);
+        for (my $i=0; $i<@items; $i++) {
+            my $odd = $i%2;
+            if (($odd) && (!$supplementalflag) && ($items[$i] !~ /^[^:]*:(|\d+):(|1):(|1):(|1):(|1)$/)) {
+                $badpath = 1;
+            } elsif ($odd && $supplementalflag) {
+                my $idx = $i-1;
+                if ($items[$i] =~ /^([^:]*)::(|1):::$/) {
+                    my $esc_name = $1;
+                    if ((!$allowed) || ($items[$idx] eq 'supplemental')) {
+                        $supppath .= '&'.$esc_name;
+                        $changed = 1;
+                    } else {
+                        $supppath .= '&'.$items[$i];
+                    }
+                } elsif (($allowed) && ($items[$idx] ne 'supplemental')) {
+                    $changed = 1;
+                    my $is_hidden;
+                    unless ($got_supp) {
+                        my ($supplemental) = &get_supplemental($coursenum,$coursedom);
+                        if (ref($supplemental) eq 'HASH') {
+                            if (ref($supplemental->{'hidden'}) eq 'HASH') {
+                                %supphidden = %{$supplemental->{'hidden'}};
+                            }
+                            if (ref($supplemental->{'ids'}) eq 'HASH') {
+                                %suppids = %{$supplemental->{'ids'}};
+                            }
+                        }
+                        $got_supp = 1;
+                    }
+                    if (ref($suppids{"/uploaded/$coursedom/$coursenum/$items[$idx].sequence"}) eq 'ARRAY') {
+                        my $mapid = $suppids{"/uploaded/$coursedom/$coursenum/$items[$idx].sequence"}->[0];
+                        if ($supphidden{$mapid}) {
+                            $is_hidden = 1;
+                        }
+                    }
+                    $supppath .= '&'.$items[$i].'::'.$is_hidden.':::';
+                } else {
+                    $supppath .= '&'.$items[$i];
+                }
+            } elsif ((!$odd) && ($items[$i] !~ /^(default|supplemental)(|_\d+)$/)) {
+                $badpath = 1;
+            } elsif ($supplementalflag) {
+                $supppath .= '&'.$items[$i];
+            }
+            last if ($badpath);
+        }
+        if ($badpath) {
+            delete($env{'form.folderpath'});
+        } elsif ($changed && $supplementalflag) {
+            $supppath =~ s/^\&//;
+            $env{'form.folderpath'} = $supppath;
+        }
+    }
+    return;
+}
+
 sub captcha_display {
     my ($context,$lonhost,$defdom) = @_;
     my ($output,$error);
@@ -18211,9 +19063,10 @@ sub create_captcha {
 
         if (-e $Apache::lonnet::perlvar{'lonCaptchaDir'}.'/'.$md5sum.'.png') {
             $output = '<input type="hidden" name="crypt" value="'.$md5sum.'" />'."\n".
+                      '<span class="LC_nobreak">'.
                       &mt('Type in the letters/numbers shown below').'&nbsp;'.
-                      '<input type="text" size="5" name="code" value="" autocomplete="off" />'.
-                      '<br />'.
+                      '<input type="text" size="5" name="code" value="" autocomplete="new-password" />'.
+                      '</span><br />'.
                       '<img src="'.$captcha_params{'www_output_dir'}.'/'.$md5sum.'.png" alt="captcha" />';
             last;
         }
@@ -18259,7 +19112,8 @@ sub check_captcha {
 sub create_recaptcha {
     my ($pubkey,$version) = @_;
     if ($version >= 2) {
-        return '<div class="g-recaptcha" data-sitekey="'.$pubkey.'"></div>';
+        return '<div class="g-recaptcha" data-sitekey="'.$pubkey.'"></div>'.
+               '<div style="padding:0;clear:both;margin:0;border:0"></div>';
     } else {
         my $use_ssl;
         if ($ENV{'SERVER_PORT'} == 443) {
@@ -18364,7 +19218,7 @@ sub critical_redirect {
         if (($env{'request.course.id'}) && (($context eq 'flip') || ($context eq 'contents'))) {
             my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'};
             my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'};
-            my $blocked = &blocking_status('alert',$cnum,$cdom,undef,1);
+            my $blocked = &blocking_status('alert',undef,$cnum,$cdom,undef,1);
             if ($blocked) {
                 my $checkrole = "cm./$cdom/$cnum";
                 if ($env{'request.course.sec'} ne '') {
@@ -18752,7 +19606,7 @@ sub page_menu {
             my @entries = split(/\&/,$value);
             foreach my $entry (@entries) {
                 my ($name,$fields) = split(/=/,$entry);
-                if (($name eq 'top') || ($name eq 'inline') || ($name eq 'main')) {
+                if (($name eq 'top') || ($name eq 'inline') || ($name eq 'foot') || ($name eq 'main')) {
                     $menu{$name} = $fields;
                 } else {
                     my @shown;