--- loncom/interface/loncommon.pm	2024/08/22 17:05:49	1.1075.2.161.2.26
+++ loncom/interface/loncommon.pm	2025/01/25 17:51:52	1.1446
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # a pile of common routines
 #
-# $Id: loncommon.pm,v 1.1075.2.161.2.26 2024/08/22 17:05:49 raeburn Exp $
+# $Id: loncommon.pm,v 1.1446 2025/01/25 17:51:52 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -71,17 +71,21 @@ use Apache::lonuserutils();
 use Apache::lonuserstate();
 use Apache::courseclassifier();
 use LONCAPA qw(:DEFAULT :match);
+use LONCAPA::ltiutils;
+use LONCAPA::LWPReq;
 use LONCAPA::map();
 use HTTP::Request;
 use DateTime::TimeZone;
 use DateTime::Locale;
 use Encode();
+use Text::Aspell;
 use Authen::Captcha;
 use Captcha::reCAPTCHA;
 use JSON::DWIW;
-use LWP::UserAgent;
 use Crypt::DES;
 use DynaLoader; # for Crypt::DES version
+use MIME::Lite;
+use MIME::Types;
 use File::Copy();
 use File::Path();
 use String::CRC32();
@@ -170,6 +174,7 @@ sub ssi_with_retries {
 # ----------------------------------------------- Filetypes/Languages/Copyright
 my %language;
 my %supported_language;
+my %supported_codes;
 my %latex_language;		# For choosing hyphenation in <transl..>
 my %latex_language_bykey;	# for choosing hyphenation from metadata
 my %cprtag;
@@ -204,14 +209,15 @@ BEGIN {
             while (my $line = <$fh>) {
                 next if ($line=~/^\#/);
                 chomp($line);
-                my ($key,$two,$country,$three,$enc,$val,$sup,$latex)=(split(/\t/,$line));
+                my ($key,$code,$country,$three,$enc,$val,$sup,$latex)=(split(/\t/,$line));
                 $language{$key}=$val.' - '.$enc;
                 if ($sup) {
                     $supported_language{$key}=$sup;
+		    $supported_codes{$key}   = $code;
                 }
 		if ($latex) {
 		    $latex_language_bykey{$key} = $latex;
-		    $latex_language{$two} = $latex;
+		    $latex_language{$code} = $latex;
 		}
             }
             close($fh);
@@ -452,7 +458,7 @@ sub studentbrowser_javascript {
             }
         }
         if ((courseadv == 'only') || (courseadv == 'none')) { url+="&courseadv="+courseadv; }
-        if (uident !== '') { url+="&identelement="+uident; }
+        if (uident !== '') { url+="&identelement="+uident; } 
         var title = 'Student_Browser';
         var options = 'scrollbars=1,resizable=1,menubar=0';
         options += ',width=700,height=600';
@@ -696,7 +702,7 @@ if (!Array.prototype.indexOf) {
         var n = 0;
         if (arguments.length > 0) {
             n = Number(arguments[1]);
-            if (n !== n) { // shortcut for verifying if it's NaN
+            if (n !== n) { // shortcut for verifying if it is NaN
                 n = 0;
             } else if (n !== 0 && n !== (1 / 0) && n !== -(1 / 0)) {
                 n = (n > 0 || -1) * Math.floor(Math.abs(n));
@@ -897,6 +903,8 @@ sub selectcourse_link {
    my $linktext = &mt('Select Course');
    if ($selecttype eq 'Community') {
        $linktext = &mt('Select Community');
+   } elsif ($selecttype eq 'Placement') {
+       $linktext = &mt('Select Placement Test'); 
    } elsif ($selecttype eq 'Course/Community') {
        $linktext = &mt('Select Course/Community');
        $type = '';
@@ -932,12 +940,12 @@ sub check_uncheck_jscript {
 function checkAll(field) {
     if (field.length > 0) {
         for (i = 0; i < field.length; i++) {
-            if (!field[i].disabled) {
+            if (!field[i].disabled) { 
                 field[i].checked = true;
             }
         }
     } else {
-        if (!field.disabled) {
+        if (!field.disabled) { 
             field.checked = true;
         }
     }
@@ -1013,7 +1021,7 @@ sub select_datelocale {
                 }
                 $locale_names{$id} = Encode::encode('UTF-8',$locale_names{$id});
                 push(@possibles,$id);
-            }
+            } 
         }
     }
     foreach my $item (sort(@possibles)) {
@@ -1049,6 +1057,33 @@ sub select_language {
 
 =pod
 
+
+=item * &list_languages()
+
+Returns an array reference that is suitable for use in language prompters.
+Each array element is itself a two element array.  The first element
+is the language code.  The second element a descsriptiuon of the 
+language itself.  This is suitable for use in e.g.
+&Apache::edit::select_arg (once dereferenced that is).
+
+=cut 
+
+sub list_languages {
+    my @lang_choices;
+
+    foreach my $id (&languageids()) {
+	my $code = &supportedlanguagecode($id);
+	if ($code) {
+	    my $selector    = $supported_codes{$id};
+	    my $description = &plainlanguagedescription($id);
+	    push(@lang_choices, [$selector, $description]);
+	}
+    }
+    return \@lang_choices;
+}
+
+=pod
+
 =item * &linked_select_forms(...)
 
 linked_select_forms returns a string containing a <script></script> block
@@ -1081,6 +1116,9 @@ linked_select_forms takes the following
 =item * $onchangesecond, additional javascript call to execute for an onchange
         event for the second <select> tag
 
+=item * $suffix, to differentiate separate uses of select2data javascript
+        objects in a page.
+
 =back 
 
 Below is an example of such a hash.  Only the 'text', 'default', and 
@@ -1135,7 +1173,8 @@ sub linked_select_forms {
         $hashref,
         $menuorder,
         $onchangefirst,
-        $onchangesecond
+        $onchangesecond,
+        $suffix
         ) = @_;
     my $second = "document.$formname.$secondselectname";
     my $first = "document.$formname.$firstselectname";
@@ -1143,20 +1182,20 @@ sub linked_select_forms {
     my $result = '';
     $result.='<script type="text/javascript" language="JavaScript">'."\n";
     $result.="// <![CDATA[\n";
-    $result.="var select2data = new Object();\n";
+    $result.="var select2data${suffix} = new Object();\n";
     $" = '","';
     my $debug = '';
     foreach my $s1 (sort(keys(%$hashref))) {
-        $result.="select2data.d_$s1 = new Object();\n";        
-        $result.="select2data.d_$s1.def = new String('".
+        $result.="select2data${suffix}['d_$s1'] = new Object();\n";        
+        $result.="select2data${suffix}['d_$s1'].def = new String('".
             $hashref->{$s1}->{'default'}."');\n";
-        $result.="select2data.d_$s1.values = new Array(";
+        $result.="select2data${suffix}['d_$s1'].values = new Array(";
         my @s2values = sort(keys( %{ $hashref->{$s1}->{'select2'} } ));
         if (ref($hashref->{$s1}->{'order'}) eq 'ARRAY') {
             @s2values = @{$hashref->{$s1}->{'order'}};
         }
         $result.="\"@s2values\");\n";
-        $result.="select2data.d_$s1.texts = new Array(";        
+        $result.="select2data${suffix}['d_$s1'].texts = new Array(";        
         my @s2texts;
         foreach my $value (@s2values) {
             push(@s2texts, $hashref->{$s1}->{'select2'}->{$value});
@@ -1166,19 +1205,17 @@ sub linked_select_forms {
     $"=' ';
     $result.= <<"END";
 
-function select1_changed() {
+function select1${suffix}_changed() {
     // Determine new choice
-    var newvalue = "d_" + $first.value;
+    var newvalue = "d_" + $first.options[$first.selectedIndex].value;
     // update select2
-    var values     = select2data[newvalue].values;
-    var texts      = select2data[newvalue].texts;
-    var select2def = select2data[newvalue].def;
+    var values     = select2data${suffix}[newvalue].values;
+    var texts      = select2data${suffix}[newvalue].texts;
+    var select2def = select2data${suffix}[newvalue].def;
     var i;
     // out with the old
-    for (i = 0; i < $second.options.length; i++) {
-        $second.options[i] = null;
-    }
-    // in with the nuclear
+    $second.options.length = 0;
+    // in with the new
     for (i=0;i<values.length; i++) {
         $second.options[i] = new Option(values[i]);
         $second.options[i].value = values[i];
@@ -1192,7 +1229,7 @@ function select1_changed() {
 </script>
 END
     # output the initial values for the selection lists
-    $result .= "<select size=\"1\" name=\"$firstselectname\" onchange=\"select1_changed();$onchangefirst\">\n";
+    $result .= "<select size=\"1\" name=\"$firstselectname\" onchange=\"select1${suffix}_changed();$onchangefirst\">\n";
     my @order = sort(keys(%{$hashref}));
     if (ref($menuorder) eq 'ARRAY') {
         @order = @{$menuorder};
@@ -1203,7 +1240,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) {
@@ -1271,11 +1313,7 @@ sub help_open_topic {
     $topic=~s/\W/\_/g;
 
     if (!$stayOnPage) {
-        if ($env{'browser.mobile'}) {
-	    $link = "javascript:openMyModal('/adm/help/${filename}.hlp',$width,$height,'yes');";
-        } else {
-            $link = "javascript:void(open('/adm/help/${filename}.hlp', 'Help_for_$topic', 'menubar=0,toolbar=1,scrollbars=1,width=$width,height=$height,resizable=yes'))";
-        }
+	$link = "javascript:openMyModal('/adm/help/${filename}.hlp',$width,$height,'yes');";
     } elsif ($stayOnPage eq 'popup') {
         $link = "javascript:void(open('/adm/help/${filename}.hlp', 'Help_for_$topic', 'menubar=0,toolbar=1,scrollbars=1,width=$width,height=$height,resizable=yes'))";
     } else {
@@ -1290,7 +1328,7 @@ sub help_open_topic {
              (($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>';
@@ -1331,11 +1369,11 @@ sub helpLatexCheatsheet {
 	  .&help_open_topic('Other_Symbols',&mt('Other Symbols'),$stayOnPage,undef,600)
 	  .'</span>';
     unless ($not_author) {
-        $out .= ' <span>'
-	       .&help_open_topic('Authoring_Output_Tags',&mt('Output Tags'),$stayOnPage,undef,600)
-	       .'</span> <span>'
+        $out .= '<span>'
+               .&help_open_topic('Authoring_Output_Tags',&mt('Output Tags'),$stayOnPage,undef,600)
+               .'</span> <span>'
                .&help_open_topic('Authoring_Multilingual_Problems',&mt('Languages'),$stayOnPage,undef,600)
-               .'</span>';
+	       .'</span>';
     }
     $out .= '</span>'; # End cheatsheet
     return $out;
@@ -1398,10 +1436,8 @@ sub help_open_menu {
 sub top_nav_help {
     my ($text,$linkattr) = @_;
     $text = &mt($text);
-    my $stay_on_page;
-    unless ($env{'environment.remote'} eq 'on') {
-        $stay_on_page = 1;
-    }
+    my $stay_on_page = 1;
+
     my ($link,$banner_link);
     unless ($env{'request.noversionuri'} =~ m{^/adm/helpmenu}) {
         $link = ($stay_on_page) ? "javascript:helpMenu('display')"
@@ -1433,7 +1469,7 @@ sub help_menu_js {
 					'js_ready'    => 1,
                                         'use_absolute' => $httphost,
 					'add_entries' => {
-					    'border' => '0',
+					    'border' => '0', 
 					    'rows'   => "110,*",},});
     my $end_page =
         &Apache::loncommon::end_page({'frameset' => 1,
@@ -1780,6 +1816,209 @@ RESIZE
 }
 
 sub colorfuleditor_js {
+    my $browse_or_search;
+    my $respath;
+    my ($cnum,$cdom) = &crsauthor_url();
+    if ($cnum) {
+        $respath = "/res/$cdom/$cnum/";
+        my %js_lt = &Apache::lonlocal::texthash(
+            sunm => 'Sub-directory name',
+            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)) {
+            var curr = document.getElementById('chooser_'+element).style.display;
+            if (curr == 'none') {
+                disp='inline';
+                if (form.elements['chooser_'+element].length) {
+                    for (var i=0; i<form.elements['chooser_'+element].length; i++) {
+                        form.elements['chooser_'+element][i].checked = false;
+                    }
+                }
+                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) {
+        if (document.getElementById('chooser_'+element+'_crsres')) {
+            var curr = document.getElementById('chooser_'+element+'_crsres').style.display;
+            if (curr == 'none') {
+                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) {
+                        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';
+            if (document.getElementById('uploadcrsres_'+element)) {
+                document.getElementById('uploadcrsres_'+element).value = '';
+            }
+        }
+        return;
+    }
+
+    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') {
+                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 = '';
+                }
+            }
+        }
+        return;
+    }
+
+    function toggleResImport(form,element) {
+        var choices = new Array('crsres','upload');
+        for (var i=0; i<choices.length; i++) {
+            if (document.getElementById('chooser_'+element+'_'+choices[i])) {
+                document.getElementById('chooser_'+element+'_'+choices[i]).style.display = 'none';
+            }
+        }
+    }
+
+    function toggleNewsubdir(form,element) {
+        var newsub = form.elements['newsubdir_'+element];
+        if (newsub) {
+            if (newsub.length) {
+                for (var j=0; j<newsub.length; j++) {
+                    if (newsub[j].checked) {
+                        if (document.getElementById('newsubdirname_'+element)) {
+                            if (newsub[j].value == '1') {
+                                document.getElementById('newsubdirname_'+element).type = "text";
+                                if (document.getElementById('newsubdir_'+element)) {
+                                    document.getElementById('newsubdir_'+element).innerHTML = '<br />$js_lt{sunm}';
+                                }
+                            } else {
+                                document.getElementById('newsubdirname_'+element).type = "hidden";
+                                document.getElementById('newsubdirname_'+element).value = "";
+                                document.getElementById('newsubdir_'+element).innerHTML = "";
+                            }
+                        }
+                        break; 
+                    }
+                }
+            }
+        }
+    }
+
+    function updateCrsFile(form,element) {
+        var directory = form.elements['coursepath_'+element];
+        var filename = form.elements['coursefile_'+element];
+        var path = directory.options[directory.selectedIndex].value;
+        var file = filename.options[filename.selectedIndex].value;
+        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;
+    }
+
+    function uploadDone(suffix,name) {
+        if (name) {
+	    document.forms["lonhomework"].elements[suffix].value = name;
+            unClean();
+            toggleChooser(document.forms["lonhomework"],suffix);
+        }
+    }
+
+\$(document).ready(function(){
+
+    \$(document).delegate('form :submit', 'click', function( event ) {
+        if ( \$( this ).hasClass( "LC_uploadcrsres" ) ) {
+            var buttonId = this.id;
+            var suffix = buttonId.toString();
+            suffix = suffix.replace(/^crsupload_/,'');
+            event.preventDefault();
+            document.lonhomework.target = 'crsupload_target_'+suffix;
+            document.lonhomework.action = '/adm/coursepub?LC_uploadcrsres='+suffix;
+            \$(this.form).submit();
+            document.lonhomework.target = '';
+            if (document.getElementById('crsuploadto_'+suffix)) {
+                document.lonhomework.action = document.getElementById('crsuploadto_'+suffix).value;
+            }
+            return false;
+        }
+    });
+});
+END
+    }
     return <<"COLORFULEDIT"
 <script type="text/javascript">
 // <![CDATA[>
@@ -1825,14 +2064,14 @@ sub colorfuleditor_js {
             }
 
             // only iterate whole storage if nothing to override
-            if(localStorage.getItem(key) == null){
+            if(localStorage.getItem(key) == null){        
 
                 // prevent storage from growing large
                 if(localStorage.length > 50){
                     var regex_getTimestamp = /^(?:\d)+;/;
                     var oldest_timestamp = regex_getTimestamp.exec(localStorage.key(0));
                     var oldest_key;
-
+                    
                     for(var i = 1; i < localStorage.length; i++){
                         if (regex_getTimestamp.exec(localStorage.key(i)) < oldest_timestamp) {
                             oldest_key = localStorage.key(i);
@@ -1862,7 +2101,7 @@ sub colorfuleditor_js {
                 pairs = valueArr[i].split(',');
                 elements = document.getElementsByName(pairs[0]);
 
-                for (var j = 0; j < elements.length; j++){
+                for (var j = 0; j < elements.length; j++){  
                     elements[j].style.display = pairs[1];
                     if (pairs[1] == "none"){
                         var regex_id = /([_\\d]+)\$/;
@@ -1875,7 +2114,7 @@ sub colorfuleditor_js {
     }
 
     function getTagList () {
-
+        
         var stringToSearch = document.lonhomework.innerHTML;
 
         var ret = new Array();
@@ -1883,7 +2122,7 @@ sub colorfuleditor_js {
         var tag_list = stringToSearch.match(regex_findBlock);
 
         if(tag_list != null){
-            for(var i = 0; i < tag_list.length; i++){
+            for(var i = 0; i < tag_list.length; i++){            
                 ret.push(tag_list[i].replace(/"/, ''));
             }
         }
@@ -1920,7 +2159,7 @@ sub colorfuleditor_js {
 
             for(var i = 0; i < tag_list.length; i++){
                 elem_list = document.getElementsByName(tag_list[i]);
-
+                
                 if(elem_list.length > 0){
                     elem = elem_list[0];
                     break;
@@ -1943,7 +2182,7 @@ sub colorfuleditor_js {
             rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */
         );
     }
-
+    
     function autosize(depth){
         var cmInst = window['cm'+depth];
         var fitsizeButton = document.getElementById('fitsize'+depth);
@@ -1962,7 +2201,7 @@ sub colorfuleditor_js {
         }
     }
 
-
+$browse_or_search
 
 // ]]>
 </script>
@@ -2010,10 +2249,254 @@ sub insert_folding_button {
     my $curDepth = $Apache::lonxml::curdepth;
     my $lastresource = $env{'request.ambiguous'};
 
-    return "<input type=\"button\" id=\"folding_btn_$curDepth\"
+    return "<input type=\"button\" id=\"folding_btn_$curDepth\" 
             value=\"".&mt('Hide')."\" onclick=\"fold_box('$curDepth','$lastresource')\">";
 }
 
+sub crsauthor_url {
+    my ($url) = @_;
+    if ($url eq '') {
+        $url = $ENV{'REQUEST_URI'};
+    }
+    my ($cnum,$cdom);
+    if ($env{'request.course.id'}) {
+        my ($audom,$auname) = ($url =~ m{^/priv/($match_domain)/($match_name)/});
+        if ($audom ne '' && $auname ne '') {
+            if (($env{'course.'.$env{'request.course.id'}.'.num'} eq $auname) &&
+                ($env{'course.'.$env{'request.course.id'}.'.domain'} eq $audom)) {
+                $cnum = $auname;
+                $cdom = $audom;
+            }
+        }
+    }
+    return ($cnum,$cdom);
+}
+
+sub import_crsauthor_form {
+    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 @ids=&Apache::lonnet::current_machine_ids();
+    my ($output,$is_home,$toppath,%subdirs,%files,%selimport_menus,$include,$exclude);
+
+    if (grep(/^\Q$crshome\E$/,@ids)) {
+        $is_home = 1;
+    }
+    $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',
+    );
+    $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;
+                    }
+                }
+                return;
+            } else {
+                relpath = encodeURIComponent(form.elements[dirsel].options[form.elements[dirsel].selectedIndex].value);
+            }
+        }
+        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;
+                    }
+                }
+            }
+        }
+        http.send(params);
+    }
+END
+}
+
+sub crsauthor_rights {
+    my ($rightsfile,$path,$docroot,$cnum,$cdom) = @_;
+    my $sourcerights = "$path/$rightsfile";
+    my $now = time;
+    if (!-e $sourcerights) {
+        my $cid = $cdom.'_'.$cnum;
+        if (!-e "$docroot/priv/$cdom") {
+            mkdir("$docroot/priv/$cdom",0755);
+        }
+        if (!-e "$docroot/priv/$cdom/$cnum") {
+            mkdir("$docroot/priv/$cdom/$cnum",0755);
+        }
+        if (open(my $fh,">$sourcerights")) {
+            print $fh <<END;
+<accessrule effect="deny" realm="" type="course" role="" />
+<accessrule effect="allow" realm="$cid" type="course" role="" />
+END
+            close($fh);
+        }
+    }
+    if (!-e "$sourcerights.meta") {
+        if (open(my $fh,">$sourcerights.meta")) {
+            my $author=$env{'environment.firstname'}.' '.
+                       $env{'environment.middlename'}.' '.
+                       $env{'environment.lastname'}.' '.
+                       $env{'environment.generation'};
+            $author =~ s/\s+$//;
+            print $fh <<"END";
+
+<abstract></abstract>
+<author>$author</author>
+<authorspace>$cnum:$cdom</authorspace>
+<copyright>private</copyright>
+<creationdate>$now</creationdate>
+<customdistributionfile></customdistributionfile>
+<dependencies></dependencies>
+<domain>$cdom</domain>
+<highestgradelevel>0</highestgradelevel>
+<keywords></keywords>
+<language>notset</language>
+<lastrevisiondate>$now</lastrevisiondate>
+<lowestgradelevel>0</lowestgradelevel>
+<mime>rights</mime>
+<modifyinguser>$env{'user.name'}:$env{'user.domain'}</modifyinguser>
+<notes></notes>
+<obsolete></obsolete>
+<obsoletereplacement></obsoletereplacement>
+<owner>$cnum:$cdom</owner>
+<rule>deny:::course,allow:$cid::course</rule>
+<sourceavail></sourceavail>
+<standards></standards>
+<subject></subject>
+<title>Course Authoring Rights</title>
+END
+            close($fh);
+        }
+    }
+    return;
+}
+
 =pod
 
 =item * &iframe_wrapper_headjs()
@@ -2303,10 +2786,24 @@ sub create_text_file {
 # ------------------------------------------
 
 sub domain_select {
-    my ($name,$value,$multiple)=@_;
+    my ($name,$value,$multiple,$incdoms,$excdoms)=@_;
+    my @possdoms;
+    if (ref($incdoms) eq 'ARRAY') {
+        @possdoms = @{$incdoms};
+    } else {
+        @possdoms = &Apache::lonnet::all_domains();
+    }
+
     my %domains=map { 
 	$_ => $_.' '. &Apache::lonnet::domain($_,'description') 
-    } &Apache::lonnet::all_domains();
+    } @possdoms;
+
+    if ((ref($excdoms) eq 'ARRAY') && (@{$excdoms} > 0)) {
+        foreach my $dom (@{$excdoms}) {
+            delete($domains{$dom});
+        }
+    }
+
     if ($multiple) {
 	$domains{''}=&mt('Any domain');
 	$domains{'select_form_order'} = [sort {lc($a) cmp lc($b) } (keys(%domains))];
@@ -2383,7 +2880,7 @@ option_name => displayed text. An option
 a javascript onchange item, e.g., onchange="this.form.submit();".
 An optional arg -- $readonly -- if true will cause the select form
 to be disabled, e.g., for the case where an instructor has a section-
-specific role, and is viewing/modifying parameters.  
+specific role, and is viewing/modifying parameters. 
 
 See lonrights.pm for an example invocation and use.
 
@@ -2451,7 +2948,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]',
@@ -2586,7 +3083,7 @@ The optional $incdoms is a reference to
 
 The optional $excdoms is a reference to an array of domains which will be excluded from the available options.
 
-The optional $disabled argument, if true, adds the disabled attribute to the select tag. 
+The optional $disabled argument, if true, adds the disabled attribute to the select tag.
 
 =cut
 
@@ -2607,7 +3104,7 @@ sub select_dom_form {
     }
     if ($includeempty) { @domains=('',@domains); }
     if (ref($excdoms) eq 'ARRAY') {
-        map { $exclude{$_} = 1; } @{$excdoms};
+        map { $exclude{$_} = 1; } @{$excdoms}; 
     }
     my $selectdomain = "<select name=\"$name\" size=\"1\"$onchange$disabled>\n";
     foreach my $dom (@domains) {
@@ -2965,7 +3462,7 @@ sub authform_nochange {
               kerb_def_dom => 'MSU.EDU',
               @_,
           );
-    my ($authnum,%can_assign) = &get_assignable_auth($in{'domain'}); 
+    my ($authnum,%can_assign) = &get_assignable_auth($in{'domain'});
     my $result;
     if (!$authnum) {
         $result = &mt('Under your current role you are not permitted to change login settings for this user');
@@ -3159,7 +3656,7 @@ sub authform_local {
     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 'loc') {
             if ($can_assign{'loc'}) {
@@ -3229,7 +3726,7 @@ sub authform_filesystem {
             } else {
                 $result = &mt('Currently Filesystem Authenticated.');
                 return $result;
-            }           
+            }
         }
     } else {
         if ($authnum == 1) {
@@ -3329,6 +3826,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') {
@@ -3452,6 +3950,21 @@ sub passwd_validation_js {
         } else {
             $alertmsg = &mt('A secret did not satisfy requirement(s):').'\n\n';
         }
+    } elsif ($context eq 'ltitools') {
+        my %domconfig = &Apache::lonnet::get_dom('configuration',['toolsec'],$domain);
+        if (ref($domconfig{'toolsec'}) eq 'HASH') {
+            if (ref($domconfig{'toolsec'}{'rules'}) eq 'HASH') {
+                %passwdconf = %{$domconfig{'toolsec'}{'rules'}};
+            }
+        }
+        if ($id eq 'add') {
+            $alertmsg = &mt('Secret for added external tool did not satisfy requirement(s):').'\n\n';
+        } elsif ($id =~ /^\d+$/) {
+            my $pos = $id+1;
+            $alertmsg = &mt('Secret for external tool [_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';
@@ -3750,13 +4263,73 @@ sub get_related_words {
     untie %thesaurus_db;
     return @Words;
 }
+###############################################################
+#
+#  Spell checking
+#
 
 =pod
 
 =back
 
+=head1 Spell checking
+
+=over 4
+
+=item * &check_spelling($wordlist $language)
+
+Takes a string containing words and feeds it to an external
+spellcheck program via a pipeline. Returns a string containing
+them mis-spelled words.
+
+Parameters:
+
+=over 4
+
+=item - $wordlist
+
+String that will be fed into the spellcheck program.
+
+=item - $language
+
+Language string that specifies the language for which the spell
+check will be performed.
+
+=back
+
+=back
+
+Note: This sub assumes that aspell is installed.
+
+
 =cut
 
+
+sub check_spelling {
+    my ($wordlist, $language) = @_;
+    my @misspellings;
+    
+    # Generate the speller and set the langauge.
+    # if explicitly selected:
+
+    my $speller = Text::Aspell->new;
+    if ($language) {
+	$speller->set_option('lang', $language);
+    }
+
+    # Turn the word list into an array of words by splittingon whitespace
+
+    my @words = split(/\s+/, $wordlist);
+
+    foreach my $word (@words) {
+	if(! $speller->check($word)) {
+	    push(@misspellings, $word);
+	}
+    }
+    return join(' ', @misspellings);
+    
+}
+
 # -------------------------------------------------------------- Plaintext name
 =pod
 
@@ -4001,6 +4574,8 @@ sub syllabuswrapper {
     return qq{<a href="/public/$domain/$coursedir/syllabus">$linktext</a>};
 }
 
+# -----------------------------------------------------------------------------
+
 sub aboutme_on {
     my ($uname,$udom)=@_;
     unless ($uname) { $uname=$env{'user.name'}; }
@@ -4025,8 +4600,6 @@ sub devalidate_aboutme_cache {
     &Apache::lonnet::devalidate_cache_new('aboutme',$id);
 }
 
-# -----------------------------------------------------------------------------
-
 sub track_student_link {
     my ($linktext,$sname,$sdom,$target,$start,$only_body) = @_;
     my $link ="/adm/trackstudent?";
@@ -4244,7 +4817,11 @@ category
 
 sub filecategorytypes {
     my ($cat) = @_;
-    return @{$category_extensions{lc($cat)}};
+    if (ref($category_extensions{lc($cat)}) eq 'ARRAY') { 
+        return @{$category_extensions{lc($cat)}};
+    } else {
+        return ();
+    }
 }
 
 =pod
@@ -4411,7 +4988,7 @@ Return string with previous attempt on p
 
 =item * $usec: section of the desired student
 
-=item * $identifier: counter for student (multiple students one problem) or
+=item * $identifier: counter for student (multiple students one problem) or 
     problem (one student; whole sequence).
 
 =back
@@ -4498,7 +5075,7 @@ sub get_previous_attempt {
             my (@hidden,@unsolved);
             if (%typeparts) {
                 foreach my $id (keys(%typeparts)) {
-                    if (($returnhash{$version.':'.$id.'.type'} eq 'anonsurvey') ||
+                    if (($returnhash{$version.':'.$id.'.type'} eq 'anonsurvey') || 
                         ($returnhash{$version.':'.$id.'.type'} eq 'anonsurveycred')) {
                         push(@hidden,$id);
                     } elsif ($identifier ne '') {
@@ -4559,7 +5136,7 @@ sub get_previous_attempt {
                         if ($key =~ /\./) {
                             my $value = $returnhash{$version.':'.$key};
                             if ($key =~ /\.rndseed$/) {
-                                my ($id) = ($key =~ /^(.+)\.rndseed$/);
+                                my ($id) = ($key =~ /^(.+)\.[^.]+$/);
                                 if (exists($returnhash{$version.':'.$id.'.rawrndseed'})) {
                                     $value = $returnhash{$version.':'.$id.'.rawrndseed'};
                                 }
@@ -4576,7 +5153,7 @@ sub get_previous_attempt {
                     next if ($key =~ /\.foilorder$/);
                     my $value = $returnhash{$version.':'.$key};
                     if ($key =~ /\.rndseed$/) {
-                        my ($id) = ($key =~ /^(.+)\.rndseed$/);
+                        my ($id) = ($key =~ /^(.+)\.[^.]+$/);
                         if (exists($returnhash{$version.':'.$id.'.rawrndseed'})) {
                             $value = $returnhash{$version.':'.$id.'.rawrndseed'};
                         }
@@ -4607,7 +5184,7 @@ sub get_previous_attempt {
                       if ($key =~/$regexp$/ && (defined &$gradesub)) {
                           $value = &$gradesub($value);
                       }
-                      $prevattempts.='<td>'.$value.'&nbsp;</td>';
+                      $prevattempts.='<td>'. $value.'&nbsp;</td>';
                   } else {
                       $prevattempts.='<td>&nbsp;</td>';
                   }
@@ -4623,7 +5200,7 @@ sub get_previous_attempt {
 	      if ($key =~/$regexp$/ && (defined &$gradesub)) {
                   $value = &$gradesub($value);
               }
-	      $prevattempts.='<td>'.$value.'&nbsp;</td>';
+	     $prevattempts.='<td>'.$value.'&nbsp;</td>';
           }
       }
       $prevattempts.= &end_data_table_row().&end_data_table();
@@ -4650,11 +5227,13 @@ sub get_previous_attempt {
 sub format_previous_attempt_value {
     my ($key,$value) = @_;
     if (($key =~ /timestamp/) || ($key=~/duedate/)) {
-	$value = &Apache::lonlocal::locallocaltime($value);
+        $value = &Apache::lonlocal::locallocaltime($value);
     } elsif (ref($value) eq 'ARRAY') {
-	$value = '('.join(', ', @{ $value }).')';
+        $value = &HTML::Entities::encode('('.join(', ', @{ $value }).')','"<>&');
     } elsif ($key =~ /answerstring$/) {
         my %answers = &Apache::lonnet::str2hash($value);
+        my @answer = %answers;
+        %answers = map {&HTML::Entities::encode($_, '"<>&')} @answer;
         my @anskeys = sort(keys(%answers));
         if (@anskeys == 1) {
             my $answer = $answers{$anskeys[0]};
@@ -4677,7 +5256,7 @@ sub format_previous_attempt_value {
             } 
         }
     } else {
-	$value = &unescape($value);
+        $value = &HTML::Entities::encode(&unescape($value), '"<>&');
     }
     return $value;
 }
@@ -4739,6 +5318,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;
@@ -5092,7 +5674,6 @@ sub findallcourses {
 
 sub blockcheck {
     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'};
@@ -5110,7 +5691,8 @@ sub blockcheck {
             }
             unless ($has_evb) {
                 if (($activity eq 'printout') || ($activity eq 'grades') || ($activity eq 'search') ||
-                    ($activity eq 'boards') || ($activity eq 'groups') || ($activity eq 'chat')) {
+                    ($activity eq 'index') || ($activity eq 'boards') || ($activity eq 'groups') || 
+                    ($activity eq 'chat')) {
                     if ($udom eq $cdom) {
                         $check_ipaccess = 1;
                     }
@@ -5201,8 +5783,9 @@ sub blockcheck {
 
     if (($activity eq 'boards' || $activity eq 'chat' ||
          $activity eq 'groups' || $activity eq 'printout' ||
-         $activity eq 'search' || $activity eq 'reinit' ||
-         $activity eq 'alert') && ($env{'request.course.id'})) {
+         $activity eq 'search' || $activity eq 'index' ||
+         $activity eq 'reinit' || $activity eq 'alert') &&
+        ($env{'request.course.id'})) {
         foreach my $key (keys(%live_courses)) {
             if ($key ne $env{'request.course.id'}) {
                 delete($live_courses{$key});
@@ -5309,7 +5892,7 @@ sub blockcheck {
 
         # Retrieve blocking times and identity of blocker for course
         # of specified user, unless user has 'evb' privilege.
-        
+
         my ($start,$end,$trigger) = 
             &get_blocks($setters,$activity,$cdom,$cnum,$url,$symb,$caller);
         if (($start != 0) && 
@@ -5499,7 +6082,7 @@ sub blocking_status {
     my $querystring  = "?activity=$activity";
 # $uname and $udom decide whose portfolio (or information page) the user is trying to look at
     if (($activity eq 'port') || ($activity eq 'about') || ($activity eq 'passwd')) {
-        $querystring .= "&amp;udom=$udom"      if ($udom =~ /^$match_domain$/);
+        $querystring .= "&amp;udom=$udom"      if ($udom =~ /^$match_domain$/); 
         $querystring .= "&amp;uname=$uname"    if ($uname =~ /^$match_username$/);
     } elsif ($activity eq 'docs') {
         my $showurl = &Apache::lonenc::check_encrypt($url);
@@ -5536,6 +6119,8 @@ END_MYBLOCK
         $text = &mt('Gradebook Blocked');
     } elsif ($activity eq 'search') {
         $text = &mt('Search Blocked');
+    } elsif ($activity eq 'index') {
+        $text = &mt('Content Index Blocked');
     } elsif ($activity eq 'alert') {
         $text = &mt('Checking Critical Messages Blocked');
     } elsif ($activity eq 'reinit') {
@@ -5569,8 +6154,7 @@ sub check_ip_acc {
     if (!defined($acc) || $acc =~ /^\s*$/ || $acc =~/^\s*no\s*$/i) {
         return 1;
     }
-    my $allowed=0;
-    my $ip;
+    my ($ip,$allowed);
     if (($ENV{'REMOTE_ADDR'} eq '127.0.0.1') ||
         ($ENV{'REMOTE_ADDR'} eq &Apache::lonnet::get_host_ip($Apache::lonnet::perlvar{'lonHostID'}))) {
         $ip = $env{'request.host'} || $ENV{'REMOTE_ADDR'} || $clientip;
@@ -5589,17 +6173,18 @@ sub check_ip_acc {
     foreach my $item (split(',',$acc)) {
         $item =~ s/^\s*//;
         $item =~ s/\s*$//;
+        my $pattern;
         if ($item =~ /^\!(.+)$/) {
             push(@denies,$1);
         } else {
             push(@allows,$item);
         }
-    }
-    my $numdenies = scalar(@denies);
-    my $numallows = scalar(@allows);
-    my $count = 0;
-    foreach my $pattern (@denies,@allows) {
-        $count ++;
+   }
+   my $numdenies = scalar(@denies);
+   my $numallows = scalar(@allows);
+   my $count = 0;
+   foreach my $pattern (@denies,@allows) {
+        $count ++; 
         my $acctype = 'allowfrom';
         if ($count <= $numdenies) {
             $acctype = 'denyfrom';
@@ -5710,6 +6295,7 @@ sub get_domainconf {
                                                 my $server = $domconfig{'login'}{'loginvia'}{$hostname}{'server'};
                                                 $designhash{$udom.'.login.loginvia'} = $server;
                                                 if ($domconfig{'login'}{'loginvia'}{$hostname}{'serverpath'} eq 'custom') {
+
                                                     $designhash{$udom.'.login.loginvia_'.$hostname} = $server.':'.$domconfig{'login'}{'loginvia'}{$hostname}{'custompath'};
                                                 } else {
                                                     $designhash{$udom.'.login.loginvia_'.$hostname} = $server.':'.$domconfig{'login'}{'loginvia'}{$hostname}{'serverpath'};
@@ -5968,7 +6554,7 @@ Input: (optional) filename from which br
        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".
-       If $title is supplied as the third arg, that will be used to
+       If $title is supplied as the third arg, that will be used to 
        the left of the breadcrumbs tail for the current path.
 
 Returns: HTML div with CSTR path and recent box
@@ -6000,7 +6586,15 @@ sub CSTR_pageheader {
         $lastitem = $thisdisfn;
     }
 
-    if ($title eq '') {
+    my $crsauthor;
+    if (($env{'request.course.id'}) &&
+        ($env{'course.'.$env{'request.course.id'}.'.num'} eq $uname) &&
+        ($env{'course.'.$env{'request.course.id'}.'.domain'} eq $udom)) {
+        $crsauthor = 1;
+        if ($title eq '') {
+            $title = &mt('Course Authoring Space');
+        }
+    } elsif ($title eq '') {
         $title = &mt('Authoring Space');
     }
 
@@ -6029,13 +6623,18 @@ sub CSTR_pageheader {
             .$lastitem
             .'</span>';
     }
-    $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($frameset)
-        .'</div>';
+
+    if ($crsauthor) {
+        $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($frameset);
+    }
+    $output .= '</div>';
 
     return $output;
 }
@@ -6080,7 +6679,7 @@ Returns: %editors hash in which keys are
          created in a course.
 
          Value for each key is 1. Possible keys
-         are: edit, xml, and daxe. 
+         are: edit, xml, and daxe.
 
          For a regular Authoring Space, if no specific
          set of editors has been set for the Author
@@ -6218,9 +6817,6 @@ Inputs:
 
 =item * $bgcolor, used to override the bgcolor on a webpage to a specific value
 
-=item * $no_inline_link, if true and in remote mode, don't show the
-         'Switch To Inline Menu' link
-
 =item * $args, optional argument valid values are
             no_auto_mt_title -> prevents &mt()ing the title arg
             use_absolute     -> for external resource or syllabus, this will
@@ -6248,11 +6844,11 @@ Inputs:
 =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.
+            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.
+            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(),
@@ -6271,8 +6867,8 @@ other decorations will be returned.
 
 sub bodytag {
     my ($title,$function,$addentries,$bodyonly,$domain,$forcereg,
-        $no_nav_bar,$bgcolor,$no_inline_link,$args,$advtoolsref,
-        $ltiscope,$ltiuri,$ltimenu,$menucoll,$menuref,$showncrumbsref)=@_;
+        $no_nav_bar,$bgcolor,$args,$advtoolsref,$ltiscope,$ltiuri,
+        $ltimenu,$menucoll,$menuref,$showncrumbsref)=@_;
 
     my $public;
     if ((($env{'user.name'} eq 'public') && ($env{'user.domain'} eq 'public'))
@@ -6284,7 +6880,6 @@ sub bodytag {
     my $hostname = $args->{'hostname'};
 
     $function = &get_users_function() if (!$function);
-    my $img =    &designparm($function.'.img',$domain);
     my $font =   &designparm($function.'.font',$domain);
     my $pgbg   = $bgcolor || &designparm($function.'.pgbg',$domain);
 
@@ -6328,7 +6923,7 @@ sub bodytag {
                 $role = &mt('Helpdesk[_1]','&nbsp;'.$2);
             }
         } else {
-            $role = (split(/\//,$role,4))[-1];
+            $role = (split(/\//,$role,4))[-1]; 
         }
         if ($sec) {
             $role .= ('&nbsp;'x2).'-&nbsp;'.&mt('section:').'&nbsp;'.$sec;
@@ -6395,33 +6990,24 @@ sub bodytag {
     } elsif ($args->{'crstype'}) {
         $crstype = $args->{'crstype'};
     }
-
-    $role = '<span class="LC_nobreak">('.$role.')</span>' if ($role && !$env{'browser.mobile'});
-
-    if ($env{'request.state'} eq 'construct') { $forcereg=1; }
-
-
-
-    my $funclist;
-    if (($env{'environment.remote'} eq 'on') && ($env{'request.state'} ne 'construct')) {
-        $bodytag .= Apache::lonhtmlcommon::scripttag(Apache::lonmenu::utilityfunctions($httphost), 'start')."\n".
-                    Apache::lonmenu::serverform();
-        my $forbodytag;
-        &Apache::lonmenu::prepare_functions($env{'request.noversionuri'},
-                                            $forcereg,$args->{'group'},
-                                            $args->{'bread_crumbs'},
-                                            $advtoolsref,'','',\$forbodytag);
-        unless (ref($args->{'bread_crumbs'}) eq 'ARRAY') {
-            $funclist = $forbodytag;
-        }
+    if (($crstype eq 'Placement') && (!$env{'request.role.adv'})) {
+        undef($role);
     } else {
+        $role = '<span class="LC_nobreak">('.$role.')</span>' if ($role && !$env{'browser.mobile'});
+    }
+
+        if ($env{'request.state'} eq 'construct') { $forcereg=1; }
 
         #    if ($env{'request.state'} eq 'construct') {
         #        $titleinfo = &CSTR_pageheader(); #FIXME: Will be removed once all scripts have their own calls
         #    }
 
-        $bodytag .= Apache::lonhtmlcommon::scripttag(
-            Apache::lonmenu::utilityfunctions($httphost), 'start');
+        my $need_endlcint;
+        unless ($args->{'switchserver'}) {
+            $bodytag .= Apache::lonhtmlcommon::scripttag(
+                Apache::lonmenu::utilityfunctions($httphost), 'start');
+            $need_endlcint = 1;
+        }
 
         my $collapsible;
         if ($args->{'collapsible_header'} ne '') {
@@ -6450,12 +7036,16 @@ END
                                                               $args->{'links_disabled'},
                                                               $args->{'links_target'},
                                                               $collapsible);
+
             if ($env{'request.noversionuri'} =~ m{^/res/adm/pages/}) {
                 if ($dc_info) {
                     $dc_info = qq|<span class="LC_cusr_subheading">$dc_info</span>|;
                 }
                 $bodytag .= qq|<div id="LC_nav_bar">$left $role<br />
                                <em>$realm</em> $dc_info</div>|;
+                if ($need_endlcint) {
+                    $bodytag .= Apache::lonhtmlcommon::scripttag('', 'end');
+                }
                 return $bodytag;
             }
 
@@ -6471,8 +7061,11 @@ END
             $bodytag .= qq|<div id="LC_realm">$realm $dc_info</div>|;
         }
 
-        #if directed to not display the secondary menu, don't.
+        #if directed to not display the secondary menu, don't.  
         if ($args->{'no_secondary_menu'}) {
+            if ($need_endlcint) {
+                $bodytag .= Apache::lonhtmlcommon::scripttag('', 'end');
+            }
             return $bodytag;
         }
         #don't show menus for public users
@@ -6485,7 +7078,9 @@ END
                                                             $args->{'links_target'});
             }
             $bodytag .= Apache::lonmenu::serverform();
-            $bodytag .= Apache::lonhtmlcommon::scripttag('', 'end');
+            if ($need_endlcint) {
+                $bodytag .= Apache::lonhtmlcommon::scripttag('', 'end');
+            }
             if ($env{'request.state'} eq 'construct') {
                 $bodytag .= &Apache::lonmenu::innerregister($forcereg,
                                 $args->{'bread_crumbs'},'','',$hostname,
@@ -6495,21 +7090,19 @@ END
                                 $args->{'group'},$args->{'hide_buttons'},
                                 $hostname,$ltiscope,$ltiuri,$showncrumbsref);
             } else {
-                my $forbodytag;
-                &Apache::lonmenu::prepare_functions($env{'request.noversionuri'},
-                                                    $forcereg,$args->{'group'},
-                                                    $args->{'bread_crumbs'},
-                                                    $advtoolsref,'',$hostname,
-                                                    \$forbodytag);
-                unless (ref($args->{'bread_crumbs'}) eq 'ARRAY') {
-                    $bodytag .= $forbodytag;
-                }
+                $bodytag .= 
+                    &Apache::lonmenu::prepare_functions($env{'request.noversionuri'},
+                                                        $forcereg,$args->{'group'},
+                                                        $args->{'bread_crumbs'},
+                                                        $advtoolsref,'',$hostname);
             }
-        }else{
-            # this is to seperate menu from content when there's no secondary
-            # menu. Especially needed for public accessible ressources.
+        } else {
+            # this is to separate menu from content when there's no secondary
+            # menu. Especially needed for publicly accessible resources.
             $bodytag .= '<hr style="clear:both" />';
-            $bodytag .= Apache::lonhtmlcommon::scripttag('', 'end'); 
+            if ($need_endlcint) {
+                $bodytag .= Apache::lonhtmlcommon::scripttag('', 'end');
+            }
         }
         if ($args->{'collapsible_header'} ne '') {
             $bodytag .= $args->{'collapsible_header'}.
@@ -6517,54 +7110,6 @@ END
                         '</div></div>';
         }
         return $bodytag;
-    }
-
-#
-# Top frame rendering, Remote is up
-#
-
-    my $imgsrc = $img;
-    if ($img =~ /^\/adm/) {
-        $imgsrc = &lonhttpdurl($img);
-    }
-    my $upperleft='<img src="'.$imgsrc.'" alt="'.$function.'" />';
-
-    my $help=($no_inline_link?''
-              :&Apache::loncommon::top_nav_help('Help'));
-
-    # Explicit link to get inline menu
-    my $menu= ($no_inline_link?''
-               :'<a href="/adm/remote?action=collapse" target="_top">'.&mt('Switch to Inline Menu Mode').'</a>');
-
-    if ($dc_info) {
-        $dc_info = qq|<span class="LC_cusr_subheading">($dc_info)</span>|;
-    }
-
-    my $name = &plainname($env{'user.name'},$env{'user.domain'});
-    unless ($public) {
-        $name = &aboutmewrapper($name,$env{'user.name'},$env{'user.domain'},
-                                undef,'LC_menubuttons_link');
-    }
-
-    unless ($env{'form.inhibitmenu'}) {
-        $bodytag .= qq|<div id="LC_nav_bar">$name $role</div>
-                       <ol class="LC_primary_menu LC_floatright LC_right">
-                       <li>$help</li>
-                       <li>$menu</li>
-                       </ol><div id="LC_realm"> $realm $dc_info</div>|;
-    }
-    if ($env{'request.state'} eq 'construct') {
-        if (!$public){
-            if ($env{'request.state'} eq 'construct') {
-                $funclist = &Apache::lonhtmlcommon::scripttag(
-                                &Apache::lonmenu::utilityfunctions($httphost), 'start').
-                            &Apache::lonhtmlcommon::scripttag('','end').
-                            &Apache::lonmenu::innerregister($forcereg,
-                                                            $args->{'bread_crumbs'});
-            }
-        }
-    }
-    return $bodytag."\n".$funclist;
 }
 
 sub dc_courseid_toggle {
@@ -6596,15 +7141,8 @@ sub make_attr_string {
 		delete($attr_ref->{$key});
 	    }
 	}
-        if ($env{'environment.remote'} eq 'on') {
-            $attr_ref->{'onload'}  =
-                &Apache::lonmenu::loadevents().  $on_load;
-            $attr_ref->{'onunload'}=
-                &Apache::lonmenu::unloadevents().$on_unload;
-        } else {  
-	    $attr_ref->{'onload'}  = $on_load;
-	    $attr_ref->{'onunload'}= $on_unload;
-        }
+	$attr_ref->{'onload'}  = $on_load;
+	$attr_ref->{'onunload'}= $on_unload;
     }
 
     my $attr_string;
@@ -6699,7 +7237,6 @@ Inputs: (all optional)
 sub standard_css {
     my ($function,$domain,$bgcolor) = @_;
     $function  = &get_users_function() if (!$function);
-    my $img    = &designparm($function.'.img',   $domain);
     my $tabbg  = &designparm($function.'.tabbg', $domain);
     my $font   = &designparm($function.'.font',  $domain);
     my $fontmenu = &designparm($function.'.fontmenu', $domain);
@@ -6752,6 +7289,7 @@ body {
   line-height:130%;
   font-size:0.83em;
   color:$font;
+  background-color: $pgbg_or_bgcolor;
 }
 
 a:focus,
@@ -6763,6 +7301,16 @@ form, .inline {
   display: inline;
 }
 
+.LC_visually_hidden:not(:focus):not(:active) {
+    clip-path: inset(50%);
+    height: 1px;
+    overflow: hidden;
+    position: absolute;
+    white-space: nowrap;
+    width: 1px;
+    display: inline;
+}
+
 .LC_menus_content.shown{
   display: block;
 }
@@ -6975,6 +7523,12 @@ ul.LC_breadcrumb_tools_outerlist li {
     float: right;
 }
 
+.LC_placement_prog {
+    padding-right: 20px;
+    font-weight: bold;
+    font-size: 90%;
+}
+
 table#LC_title_bar td {
   background: $tabbg;
 }
@@ -7066,7 +7620,7 @@ td.LC_menubuttons_text {
 }
 
 td.LC_zero_height {
-  line-height: 0;
+  line-height: 0; 
   cellpadding: 0;
 }
 
@@ -7391,6 +7945,12 @@ td.LC_parm_overview_restrictions  {
   border-collapse: collapse;
 }
 
+span.LC_parm_recursive,
+td.LC_parm_recursive {
+  font-weight: bold;
+  font-size: smaller;
+}
+
 table.LC_parm_overview_restrictions td {
   border-width: 1px 4px 1px 4px;
   border-style: solid;
@@ -7667,6 +8227,24 @@ table.LC_prior_tries td {
   padding: 6px;
 }
 
+.LC_prob_status {
+  display: table;
+  padding: 0;
+  margin: 0;
+}
+
+.LC_prob_status_row {
+  display: table-row;
+}
+
+.LC_status_cell {
+  display: table-cell;
+  padding-top: 0;
+  padding-left: 0;
+  padding-bottom: 0;
+  padding-right: 5px;
+}
+
 span.LC_prior_numerical,
 span.LC_prior_string,
 span.LC_prior_custom,
@@ -7743,7 +8321,12 @@ table.LC_data_table tr > td.LC_docs_entr
   color: #990000;
 }
 
+.LC_docs_alias {
+  color: #440055;  
+}
+
 .LC_domprefs_email,
+.LC_docs_alias_name,
 .LC_docs_reinit_warn,
 .LC_docs_ext_edit {
   font-size: x-small;
@@ -8045,7 +8628,7 @@ ol.LC_primary_menu li {
   line-height: 1.5em;
 }
 
-ol.LC_primary_menu li a, 
+ol.LC_primary_menu li a,
 ol.LC_primary_menu li p {
   display: block;
   margin: 0;
@@ -8060,7 +8643,7 @@ ol.LC_primary_menu li p span.LC_primary_
 }
 
 ol.LC_primary_menu li p span.LC_primary_menu_innerarrow {
-  display: inline-block;
+  display: inline-block;	
   width: 5%;
   float: right;
   text-align: right;
@@ -8095,9 +8678,9 @@ ol.LC_primary_menu li:hover li, ol.LC_pr
   float: none;
   border-left: 1px solid black;
   border-right: 1px solid black;
-/* A dark bottom border to visualize different menu options;
+/* A dark bottom border to visualize different menu options; 
 overwritten in the create_submenu routine for the last border-bottom of the menu */
-  border-bottom: 1px solid $data_table_dark;
+  border-bottom: 1px solid $data_table_dark; 
 }
 
 ol.LC_primary_menu li li p:hover {
@@ -8886,15 +9469,16 @@ sub headtag {
         $inhibitprint = &print_suppression();
     }
 
-    if (!$args->{'frameset'}) {
+    if (!$args->{'frameset'} && !$args->{'switchserver'}) {
 	$result .= &Apache::lonhtmlcommon::htmlareaheaders();
     }
-    if ($args->{'force_register'}) {
-        $result .= &Apache::lonmenu::registerurl(1);
+    if ($args->{'force_register'} && $env{'request.noversionuri'} !~ m{^/res/adm/pages/}) {
+        $result .= Apache::lonxml::display_title();
     }
     if (!$args->{'no_nav_bar'} 
 	&& !$args->{'only_body'}
-	&& !$args->{'frameset'}) {
+	&& !$args->{'frameset'}
+	&& !$args->{'switchserver'}) {
 	$result .= &help_menu_js($httphost);
         $result.=&modal_window();
         $result.=&togglebox_script();
@@ -8917,12 +9501,12 @@ sub headtag {
     if (ref($args->{'redirect'})) {
 	my ($time,$url,$inhibit_continue,$to_opener,$skip_enc_check) = @{$args->{'redirect'}};
         if (!$skip_enc_check) {
-	    $url = &Apache::lonenc::check_encrypt($url);
+            $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) {
@@ -8999,7 +9583,7 @@ ADDMETA
                         my $newserver = &Apache::lonnet::spareserver(undef,30000,undef,1,$dom_in_use);
                         if (($newserver eq '') && ($offloadoth)) {
                             my @domains = &Apache::lonnet::current_machine_domains();
-                            if (($dom_in_use ne '') && (!grep(/^\Q$dom_in_use\E$/,@domains))) {
+                            if (($dom_in_use ne '') && (!grep(/^\Q$dom_in_use\E$/,@domains))) { 
                                 ($newserver) = &Apache::lonnet::choose_server($dom_in_use);
                             }
                         }
@@ -9093,13 +9677,13 @@ OFFLOAD
     if ($title =~ /^LON-CAPA\s+/) {
         $result .= '<title> '.$title.'</title>';
     } else {
-        $result .= '<title> LON-CAPA '.$title.'</title>';  
+        $result .= '<title> LON-CAPA '.$title.'</title>';
     }
     $result .= "\n".'<link rel="stylesheet" type="text/css" href="'.$url.'"';
     if (!$args->{'frameset'}) {
         $result .= ' /';
     }
-    $result .= '>'
+    $result .= '>' 
         .$inhibitprint
 	.$head_extra;
     my $clientmobile;
@@ -9110,7 +9694,7 @@ OFFLOAD
     }
     if ($clientmobile) {
         $result .= '
-<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0, minimum-scale=1.0, maximum-scale=1.0">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
 <meta name="apple-mobile-web-app-capable" content="yes" />';
     }
     $result .= '<meta name="google" content="notranslate" />'."\n";
@@ -9132,12 +9716,12 @@ sub font_settings {
     my $headerstring='';
     if ((!$env{'browser.mathml'} && $env{'browser.unicode'}) ||
         ((ref($args) eq 'HASH') && ($args->{'browser.unicode'}))) {
-	$headerstring.=
-	    '<meta http-equiv="Content-Type" content="text/html; charset=utf-8"';
+        $headerstring.=
+            '<meta http-equiv="Content-Type" content="text/html; charset=utf-8"';
         if (!$args->{'frameset'}) {
-            $headerstring.= ' /';
+	    $headerstring.= ' /';
         }
-        $headerstring .= '>'."\n";
+	$headerstring .= '>'."\n";
     }
     return $headerstring;
 }
@@ -9291,14 +9875,17 @@ $args - additional optional args support
              skip_phases    -> hash ref of 
                                     head -> skip the <html><head> generation
                                     body -> skip all <body> generation
-             no_inline_link -> if true and in remote mode, don't show the
-                                    'Switch To Inline Menu' link
              no_auto_mt_title -> prevent &mt()ing the title arg
              bread_crumbs ->             Array containing breadcrumbs
              bread_crumbs_component ->  if exists show it as headline else show only the breadcrumbs
+             bread_crumbs_style -> breadcrumbs are contained within <div id="LC_breadcrumbs">,
+                                   and &standard_css() contains CSS for #LC_breadcrumbs, if you want
+                                   to override those values, or add to them, specify the value to
+                                   include in the style attribute to include in the div tag by using
+                                   bread_crumbs_style (e.g., overflow: visible)
              bread_crumbs_nomenu -> if true will pass false as the value of $menulink
                                     to lonhtmlcommon::breadcrumbs
-             group          -> includes the current group, if page is for a
+             group          -> includes the current group, if page is for a 
                                specific group
              use_absolute   -> for request for external resource or syllabus, this
                                will contain https://<hostname> if server uses
@@ -9408,9 +9995,9 @@ sub start_page {
                          $args->{'function'},       $args->{'add_entries'},
                          $args->{'only_body'},      $args->{'domain'},
                          $args->{'force_register'}, $args->{'no_nav_bar'},
-                         $args->{'bgcolor'},        $args->{'no_inline_link'},
-                         $args,                     \@advtools,
-                         $ltiscope,$ltiuri,\%ltimenu,$menucoll,\%menu,\$showncrumbs);
+                         $args->{'bgcolor'},        $args,
+                         \@advtools,$ltiscope,$ltiuri,\%ltimenu,$menucoll,
+                         \%menu,\$showncrumbs);
         }
     }
 
@@ -9447,7 +10034,10 @@ sub start_page {
                 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')) {
+                     ($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'}))) {
                     $menulink = 0;
                 } else {
                     undef($menulink);
@@ -9461,16 +10051,14 @@ sub start_page {
                 }
 		#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);
+			$result .= &Apache::lonhtmlcommon::breadcrumbs($args->{'bread_crumbs_component'},
+                                                                       '',$menulink,'',
+                                                                       $args->{'bread_crumbs_style'});
+                } else {
+			$result .= &Apache::lonhtmlcommon::breadcrumbs('','',$menulink,'',
+                                                                       $args->{'bread_crumbs_style'});
 		}
         }
-    } elsif (($env{'environment.remote'} eq 'on') &&
-             ($env{'form.inhibitmenu'} ne 'yes') &&
-             ($env{'request.noversionuri'} =~ m{^/res/}) &&
-             ($env{'request.noversionuri'} !~ m{^/res/adm/pages/})) {
-        $result .= '<div style="padding:0;margin:0;clear:both"><hr /></div>';
     }
     return $result;
 }
@@ -9526,7 +10114,7 @@ sub menucoll_in_effect {
                         $check_login_symb = 1;
                     }
                 } else {
-                    my $symb=&Apache::lonnet::symbread();
+                    my $symb = &Apache::lonnet::symbread();
                     if ($symb) {
                         $deeplink = &Apache::lonnet::EXT('resource.0.deeplink',$symb);
                     } else {
@@ -9767,7 +10355,7 @@ sub modal_adhoc_inner {
     my ($funcname,$width,$height,$content,$possmathjax)=@_;
     my $innerwidth=$width-20;
     $content=&js_ready(
-               &start_page('Dialog',undef,{'only_body'=>1,'bgcolor'=>'#FFFFFF'}).
+                 &start_page('Dialog',undef,{'only_body'=>1,'bgcolor'=>'#FFFFFF'}).
                  &start_scrollbox($width.'px',$innerwidth.'px',$height.'px','myModal','#FFFFFF',undef,1).
                  $content.
                  &end_scrollbox().
@@ -10336,6 +10924,16 @@ Scalar: 1 if 'Course' to be used, 0 othe
 
 ###############################################
 sub show_course {
+    my ($udom,$uname) = @_;
+    if (($udom ne '') && ($uname ne '')) {
+        if (($udom ne $env{'user.domain'}) || ($uname ne $env{'user.name'})) {
+            if (&Apache::lonnet::is_advanced_user($udom,$uname)) {
+                return 0;
+            } else {
+                return 1;
+            }
+        }
+    }
     my $course = !$env{'user.adv'};
     if (!$env{'user.adv'}) {
         foreach my $env (keys(%env)) {
@@ -10459,7 +11057,7 @@ sub get_sections {
         }
     }
 
-    if ($check_students) {
+    if ($check_students) { 
 	my ($classlist) = &Apache::loncoursedata::get_classlist($cdom,$cnum);
 	my $sec_index = &Apache::loncoursedata::CL_SECTION();
 	my $status_index = &Apache::loncoursedata::CL_STATUS();
@@ -10781,8 +11379,8 @@ Incoming parameters:
 2. user's domain
 3. quota name - portfolio, author, or course
    (if no quota name provided, defaults to portfolio).
-4. crstype - official, unofficial, textbook or community, if quota name is
-   course
+4. crstype - official, unofficial, textbook, placement or community, 
+   if quota name is course
 
 Returns:
 1. Disk quota (in MB) assigned to student.
@@ -10855,8 +11453,9 @@ sub get_user_quota {
         if ($quota eq '' || wantarray) {
             if ($quotaname eq 'course') {
                 my %domdefs = &Apache::lonnet::get_domain_defaults($udom);
-                if (($crstype eq 'official') || ($crstype eq 'unofficial') ||
-                    ($crstype eq 'community') || ($crstype eq 'textbook')) {
+                if (($crstype eq 'official') || ($crstype eq 'unofficial') || 
+                    ($crstype eq 'community') || ($crstype eq 'textbook') ||
+                    ($crstype eq 'placement')) { 
                     $defquota = $domdefs{$crstype.'quota'};
                 }
                 if ($defquota eq '') {
@@ -11004,7 +11603,7 @@ Inputs: 7
 4. filename of file for which action is being requested
 5. filesize (kB) of file
 6. action being taken: copy or upload.
-7. quotatype (in course context -- official, unofficial, community or textbook).
+7. quotatype (in course context -- official, unofficial, textbook, placement or community).
 
 Returns: 1 scalar: HTML to display containing warning if quota would be exceeded,
          otherwise return null.
@@ -11040,6 +11639,8 @@ sub excess_filesize_warning {
 ###############################################
 
 
+
+
 sub get_secgrprole_info {
     my ($cdom,$cnum,$needroles,$type)  = @_;
     my %sections_count = &get_sections($cdom,$cnum);
@@ -11148,7 +11749,16 @@ sub user_picker {
         $allow_blank = 0;
         $domform = &select_dom_form($currdom,'srchdomain',$allow_blank,1,undef,[$currdom]);
     } else {
-        $domform = &select_dom_form($currdom,'srchdomain',$allow_blank,1);
+        my $defdom = $env{'request.role.domain'};
+        my ($trusted,$untrusted);
+        if (($context eq 'requestcrs') || ($context eq 'course')) {
+            ($trusted,$untrusted) = &Apache::lonnet::trusted_domains('enroll',$defdom);
+        } elsif ($context eq 'author') {
+            ($trusted,$untrusted) = &Apache::lonnet::trusted_domains('othcoau',$defdom);
+        } elsif ($context eq 'domain') {
+            ($trusted,$untrusted) = &Apache::lonnet::trusted_domains('domroles',$defdom);
+        }
+        $domform = &select_dom_form($currdom,'srchdomain',$allow_blank,1,undef,$trusted,$untrusted);
     }
     my $srchinsel = ' <select name="srchin">';
 
@@ -11353,7 +11963,7 @@ sub user_rule_check {
     if (ref($usershash) eq 'HASH') {
         if (keys(%{$usershash}) > 1) {
             my (%by_username,%by_id,%userdoms);
-            my $checkid;
+            my $checkid; 
             if (ref($checks) eq 'HASH') {
                 if ((!defined($checks->{'username'})) && (defined($checks->{'id'}))) {
                     $checkid = 1;
@@ -11364,7 +11974,7 @@ sub user_rule_check {
                 if ($checkid) {
                     if (ref($usershash->{$user}) eq 'HASH') {
                         if ($usershash->{$user}->{'id'} ne '') {
-                            $by_id{$udom}{$usershash->{$user}->{'id'}} = $uname;
+                            $by_id{$udom}{$usershash->{$user}->{'id'}} = $uname; 
                             $userdoms{$udom} = 1;
                             if (ref($inst_results) eq 'HASH') {
                                 $inst_results->{$uname.':'.$udom} = {};
@@ -11434,7 +12044,7 @@ sub user_rule_check {
                 if (ref($usershash->{$user}) eq 'HASH') {
                     if (ref($checks) eq 'HASH') {
                         if (defined($checks->{'username'})) {
-                            ($inst_response{$user},%{$inst_results->{$user}}) =
+                            ($inst_response{$user},%{$inst_results->{$user}}) = 
                                 &Apache::lonnet::get_instuser($udom,$uname);
                         } elsif (defined($checks->{'id'})) {
                             if ($usershash->{$user}->{'id'} ne '') {
@@ -11457,7 +12067,7 @@ sub user_rule_check {
                         if (ref($domconfig{'usercreation'}) eq 'HASH') {
                             foreach my $item ('username','id') {
                                 if (ref($domconfig{'usercreation'}{$item.'_rule'}) eq 'ARRAY') {
-                                   $$curr_rules{$udom}{$item} =
+                                   $$curr_rules{$udom}{$item} = 
                                        $domconfig{'usercreation'}{$item.'_rule'};
                                 }
                             }
@@ -11480,7 +12090,7 @@ sub user_rule_check {
                     $id = $inst_results->{$user}->{'id'};
                 }
             }
-            if ($id eq '') {
+            if ($id eq '') { 
                 if (ref($usershash->{$user})) {
                     $id = $usershash->{$user}->{'id'};
                 }
@@ -11799,7 +12409,7 @@ future_reservable - ref to hash of stude
 
     Keys in inner hash are:
     (a) symb: either blank or symb to which slot use is restricted.
-    (b) startreserve:  start date of reservation period.
+    (b) startreserve: start date of reservation period.
     (c) uniqueperiod: start,end dates when slot is to be uniquely
         selected.
 
@@ -11809,13 +12419,35 @@ future_reservable - ref to hash of stude
 
 sub get_future_slots {
     my ($cnum,$cdom,$now,$symb) = @_;
+    my $map;
+    if ($symb) {
+        ($map) = &Apache::lonnet::decode_symb($symb);
+    }
     my (%reservable_now,%future_reservable,@sorted_reservable,@sorted_future);
     my %slots = &Apache::lonnet::get_course_slots($cnum,$cdom);
     foreach my $slot (keys(%slots)) {
         next unless($slots{$slot}->{'type'} eq 'schedulable_student');
         if ($symb) {
-            next if (($slots{$slot}->{'symb'} ne '') && 
-                     ($slots{$slot}->{'symb'} ne $symb));
+            if ($slots{$slot}->{'symb'} ne '') {
+                my $canuse;
+                my %oksymbs;
+                my @slotsymbs = split(/\s*,\s*/,$slots{$slot}->{'symb'});
+                map { $oksymbs{$_} = 1; } @slotsymbs;
+                if ($oksymbs{$symb}) {
+                    $canuse = 1;
+                } else {
+                    foreach my $item (@slotsymbs) {
+                        if ($item =~ /\.(page|sequence)$/) {
+                            (undef,undef,my $sloturl) = &Apache::lonnet::decode_symb($item);
+                            if (($map ne '') && ($map eq $sloturl)) {
+                                $canuse = 1;
+                                last;
+                            }
+                        }
+                    }
+                }
+                next unless ($canuse);
+            }
         }
         if (($slots{$slot}->{'starttime'} > $now) &&
             ($slots{$slot}->{'endtime'} > $now)) {
@@ -11868,7 +12500,7 @@ sub get_future_slots {
                 $reservable_now{$slot} = {
                                            symb       => $symb,
                                            endreserve => $lastres,
-                                           uniqueperiod => $uniqueperiod,   
+                                           uniqueperiod => $uniqueperiod,
                                          };
             } elsif (($startreserve > $now) &&
                      (!$endreserve || $endreserve > $startreserve)) {
@@ -12033,7 +12665,23 @@ sub get_env_multiple {
     return(@values);
 }
 
+# Looks at given dependencies, and returns something depending on the context.
+# For coursedocs paste, returns (undef, $counter, $numpathchg, \%existing).
+# For syllabus rewrites, returns (undef, $counter, $numpathchg, \%existing, \%mapping).
+# For all other contexts, returns ($output, $counter, $numpathchg).
+# $output: string with the HTML output. Can contain missing dependencies with an upload form, existing dependencies, and dependencies no longer in use.
+# $counter: integer with the number of existing dependencies when no HTML output is returned, and the number of missing dependencies when an HTML output is returned.
+# $numpathchg: integer with the number of cleaned up dependency paths.
+# \%existing: hash reference clean path -> 1 only for existing dependencies.
+# \%mapping: hash reference clean path -> original path for all dependencies.
+# @param {string} actionurl - The path to the handler, indicative of the context.
+# @param {string} state - Can contain HTML with hidden inputs that will be added to the output form.
+# @param {hash reference} allfiles - List of file info from lonnet::extract_embedded_items
+# @param {hash reference} codebase - undef, not modified by lonnet::extract_embedded_items ?
+# @param {hash reference} args - More parameters ! Possible keys: error_on_invalid_names (boolean), ignore_remote_references (boolean), current_path (string), docs_url (string), docs_title (string), context (string)
+# @return {Array} - array depending on the context (not a reference)
 sub ask_for_embedded_content {
+    # NOTE: documentation was added afterwards, it could be wrong
     my ($actionurl,$state,$allfiles,$codebase,$args)=@_;
     my (%subdependencies,%dependencies,%mapping,%existing,%newfiles,%pathchanges,
         %currsubfile,%unused,$rem);
@@ -12049,6 +12697,9 @@ sub ask_for_embedded_content {
     my $heading = &mt('Upload embedded files');
     my $buttontext = &mt('Upload');
 
+    # fills these variables based on the context:
+    # $navmap, $cdom, $cnum, $udom, $uname, $url, $toplevel, $getpropath,
+    # $path, $fileloc, $title, $rem, $filename
     if ($env{'request.course.id'}) {
         if ($actionurl eq '/adm/dependencies') {
             $navmap = Apache::lonnavmaps::navmap->new();
@@ -12056,7 +12707,7 @@ sub ask_for_embedded_content {
         $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'};
         $cnum = $env{'course.'.$env{'request.course.id'}.'.num'};
     }
-    if (($actionurl eq '/adm/portfolio') ||
+    if (($actionurl eq '/adm/portfolio') || 
         ($actionurl eq '/adm/coursegrp_portfolio')) {
         my $current_path='/';
         if ($env{'form.currentpath'}) {
@@ -12088,18 +12739,18 @@ sub ask_for_embedded_content {
             $toplevel = $url;
             if ($args->{'context'} eq 'paste') {
                 ($cdom,$cnum) = ($url =~ m{^\Q/uploaded/\E($match_domain)/($match_courseid)/});
-                ($path) =
+                ($path) = 
                     ($toplevel =~ m{^(\Q/uploaded/$cdom/$cnum/\E(?:docs|supplemental)/(?:default|\d+)/\d+)/});
                 $fileloc = &Apache::lonnet::filelocation('',$toplevel);
                 $fileloc =~ s{^/}{};
             }
         }
-    } elsif ($actionurl eq '/adm/dependencies') {
+    } elsif ($actionurl eq '/adm/dependencies')  {
         if ($env{'request.course.id'} ne '') {
             if (ref($args) eq 'HASH') {
                 $url = $args->{'docs_url'};
                 $title = $args->{'docs_title'};
-                $toplevel = $url;
+                $toplevel = $url; 
                 unless ($toplevel =~ m{^/}) {
                     $toplevel = "/$url";
                 }
@@ -12133,6 +12784,16 @@ sub ask_for_embedded_content {
         $fileloc = &Apache::lonnet::filelocation('',$toplevel).'/';
         $fileloc =~ s{^/}{};
     }
+    
+    # parses the dependency paths to get some info
+    # fills $newfiles, $mapping, $subdependencies, $dependencies
+    # $newfiles: hash URL -> 1 for new files or external URLs
+    # (will be completed later)
+    # $mapping:
+    #   for external URLs: external URL -> external URL
+    #   for relative paths: clean path -> original path
+    # $subdependencies: hash clean path -> clean file name -> 1 for relative paths in subdirectories
+    # $dependencies: hash clean or not file name -> 1 for relative paths not in subdirectories
     foreach my $file (keys(%{$allfiles})) {
         my $embed_file;
         if (($path eq "/uploaded/$cdom/$cnum/portfolio/syllabus") && ($file =~ m{^\Q$path/\E(.+)$})) {
@@ -12175,11 +12836,24 @@ sub ask_for_embedded_content {
             }
         }
     }
+    
+    # looks for all existing files in dependency subdirectories (from $subdependencies filled above)
+    # and lists
+    # fills $currsubfile, $pathchanges, $existing, $numexisting, $newfiles, $unused
+    # $currsubfile: hash clean path -> file name -> 1 for all existing files in the path
+    # $pathchanges: hash clean path -> 1 if the file in subdirectory exists and
+    #                                    the path had to be cleaned up
+    # $existing: hash clean path -> 1 if the file exists
+    # $numexisting: number of keys in $existing
+    # $newfiles: updated with clean path -> 1 for files in subdirectories that do not exist
+    # $unused: only for /adm/dependencies, hash clean path -> 1 for existing files in
+    #                                      dependency subdirectories that are
+    #                                      not listed as dependencies, with some exceptions using $rem
     my $dirptr = 16384;
     foreach my $path (keys(%subdependencies)) {
         $currsubfile{$path} = {};
-        if (($actionurl eq '/adm/portfolio') ||
-            ($actionurl eq '/adm/coursegrp_portfolio')) { 
+        if (($actionurl eq '/adm/portfolio') || 
+            ($actionurl eq '/adm/coursegrp_portfolio')) {
             my ($sublistref,$listerror) =
                 &Apache::lonnet::dirlist($url.$path,$udom,$uname,$getpropath);
             if (ref($sublistref) eq 'ARRAY') {
@@ -12250,6 +12924,9 @@ sub ask_for_embedded_content {
             }
         }
     }
+    
+    # fills $currfile, hash file name -> 1 or [$size,$mtime]
+    # for files in $url or $fileloc (target directory) in some contexts
     my %currfile;
     if (($actionurl eq '/adm/portfolio') ||
         ($actionurl eq '/adm/coursegrp_portfolio')) {
@@ -12288,6 +12965,8 @@ sub ask_for_embedded_content {
             }
         }
     }
+    # updates $pathchanges, $existing, $numexisting, $newfiles and $unused for files that
+    # are not in subdirectories, using $currfile
     foreach my $file (keys(%dependencies)) {
         if (exists($currfile{$file})) {
             unless ($mapping{$file} eq $file) {
@@ -12316,17 +12995,25 @@ sub ask_for_embedded_content {
             $unused{$file} = 1;
         }
     }
+    
+    # returns some results for coursedocs paste and syllabus rewrites ($output is undef)
     if (($actionurl eq '/adm/coursedocs') && (ref($args) eq 'HASH') &&
         ($args->{'context'} eq 'paste')) {
         $counter = scalar(keys(%existing));
         $numpathchg = scalar(keys(%pathchanges));
         return ($output,$counter,$numpathchg,\%existing);
-    } elsif (($actionurl eq "/public/$cdom/$cnum/syllabus") &&
+    } elsif (($actionurl eq "/public/$cdom/$cnum/syllabus") && 
              (ref($args) eq 'HASH') && ($args->{'context'} eq 'rewrites')) {
         $counter = scalar(keys(%existing));
         $numpathchg = scalar(keys(%pathchanges));
         return ($output,$counter,$numpathchg,\%existing,\%mapping);
     }
+    
+    # returns HTML otherwise, with dependency results and to ask for more uploads
+    
+    # $upload_output: missing dependencies (with upload form)
+    # $modify_output: uploaded dependencies (in use)
+    # $delete_output: files no longer in use (unused files are not listed for londocs, bug?)
     foreach my $embed_file (sort {lc($a) cmp lc($b)} keys(%newfiles)) {
         if ($actionurl eq '/adm/dependencies') {
             next if ($embed_file =~ m{^\w+://});
@@ -12550,7 +13237,7 @@ sub ask_for_embedded_content {
 
 Performs clean-up of directories, subdirectories and filename in an
 embedded object, referenced in an HTML file which is being uploaded
-to a course or portfolio, where
+to a course or portfolio, where 
 "Upload embedded images/multimedia files if HTML file" checkbox was
 checked.
 
@@ -12569,7 +13256,7 @@ sub clean_path {
         @contents = ($embed_file);
     }
     my $lastidx = scalar(@contents)-1;
-    for (my $i=0; $i<=$lastidx; $i++) {
+    for (my $i=0; $i<=$lastidx; $i++) { 
         $contents[$i]=~s{\\}{/}g;
         $contents[$i]=~s/\s+/\_/g;
         $contents[$i]=~s{[^/\w\.\-]}{}g;
@@ -12908,7 +13595,7 @@ sub modify_html_refs {
     }
     my (%allfiles,%codebase,$output,$content);
     my @changes = &get_env_multiple('form.namechange');
-    unless ((@changes > 0)  || ($context eq 'syllabus')) {
+    unless ((@changes > 0) || ($context eq 'syllabus')) {
         if (wantarray) {
             return ('',0,0); 
         } else {
@@ -13043,7 +13730,7 @@ sub modify_html_refs {
                         }
                     }
                     if ($rewrites) {
-                        my $saveresult;
+                        my $saveresult; 
                         my $url = &Apache::lonnet::store_edited_file($container,$content,$udom,$uname,\$saveresult);
                         if ($url eq $container) {
                             my ($fname) = ($container =~ m{/([^/]+)$});
@@ -13569,7 +14256,7 @@ sub process_decompression {
             }
             my $numskip = scalar(@to_skip);
             my $numoverwrite = scalar(@to_overwrite);
-            if (($numskip) && (!$numoverwrite)) {
+            if (($numskip) && (!$numoverwrite)) { 
                 $warning = &mt('All items in the archive file already exist, and no overwriting of existing files has been requested.');         
             } elsif ($dir eq '') {
                 $error = &mt('Directory containing archive file unavailable.');
@@ -13579,11 +14266,11 @@ sub process_decompression {
                     my $tempdir = time.'_'.$$.int(rand(10000));
                     mkdir("$dir/$tempdir",0755);
                     if (&File::Copy::move("$dir/$file","$dir/$tempdir/$file")) {
-                        ($decompressed,$display) =
+                        ($decompressed,$display) = 
                             &decompress_uploaded_file($file,"$dir/$tempdir");
                         foreach my $item (@to_skip) {
                             if (($item ne '') && ($item !~ /\.\./)) {
-                                if (-f "$dir/$tempdir/$item") {
+                                if (-f "$dir/$tempdir/$item") { 
                                     unlink("$dir/$tempdir/$item");
                                 } elsif (-d "$dir/$tempdir/$item") {
                                     &File::Path::remove_tree("$dir/$tempdir/$item",{ safe => 1 });
@@ -13623,7 +14310,7 @@ sub process_decompression {
                     if (ref($newdirlistref) eq 'ARRAY') {
                         foreach my $dir_line (@{$newdirlistref}) {
                             my ($item,undef,undef,$testdir)=split(/\&/,$dir_line,5);
-                            unless (($item =~ /^\.+$/) || ($item eq $file)) { 
+                            unless (($item =~ /^\.+$/) || ($item eq $file)) {
                                 push(@newitems,$item);
                                 if ($dirptr&$testdir) {
                                     $is_dir{$item} = 1;
@@ -13678,7 +14365,7 @@ sub process_decompression {
                                     $env{'form.archive_title_'.$i} = $env{'form.camtasia_foldername'};
                                     $displayed{'folder'} = $i;
                                 } elsif ((($item eq "$contents[0]/index.html") && ($version == 6)) ||
-                                         (($item eq "$contents[0]/$contents[0]".'.html') && ($version == 8))) {
+                                         (($item eq "$contents[0]/$contents[0]".'.html') && ($version == 8))) { 
                                     $env{'form.archive_'.$i} = 'display';
                                     $env{'form.archive_title_'.$i} = $env{'form.camtasia_moviename'};
                                     $displayed{'web'} = $i;
@@ -14130,7 +14817,7 @@ sub process_extracted_files {
         $folders{'0'} = $items[-2];
         if ($env{'form.folderpath'} =~ /\:1$/) {
             $containers{'0'}='page';
-        } else {
+        } else {  
             $containers{'0'}='sequence';
         }
     }
@@ -14211,7 +14898,7 @@ sub process_extracted_files {
                             $newseqid{$i} = $newidx;
                             unless ($errtext) {
                                 $result .=  '<li>'.&mt('Folder: [_1] added to course',
-                                                       &HTML::Entities::encode($docstitle,'<>&"'))..
+                                                       &HTML::Entities::encode($docstitle,'<>&"')).
                                             '</li>'."\n";
                             }
                         }
@@ -14238,7 +14925,7 @@ sub process_extracted_files {
                                             $fetch =~ s/^\Q$prefix$dir\E//;
                                             $prompttofetch{$fetch} = 1;
                                         }
-                                   }
+                                    }
                                 }
                                 $LONCAPA::map::resources[$newidx]=
                                     $docstitle.':'.$url.':false:normal:res';
@@ -14263,7 +14950,7 @@ sub process_extracted_files {
                 }
             } else {
                 $warning .= &mt('Item extracted from archive: [_1] has unexpected path.',
-                                &HTML::Entities::encode($path,'<>&"')).'<br />';
+                                &HTML::Entities::encode($path,'<>&"')).'<br />'; 
             }
         }
         for (my $i=1; $i<=$numitems; $i++) {
@@ -14285,7 +14972,7 @@ sub process_extracted_files {
                         }
                         if ($itemidx eq '') {
                             $itemidx =  0;
-                        }
+                        } 
                         if (grep(/^\Q$referrer{$i}\E$/,@archdirs)) {
                             if ($mapinner{$referrer{$i}}) {
                                 $fullpath = "$prefix$dir/$docstype/$mapinner{$referrer{$i}}";
@@ -14334,13 +15021,13 @@ sub process_extracted_files {
                                     $showpath = "$relpath/$title";
                                 } else {
                                     $showpath = "/$title";
-                                }
+                                } 
                                 $result .= '<li>'.&mt('[_1] included as a dependency',
                                                       &HTML::Entities::encode($showpath,'<>&"')).
                                            '</li>'."\n";
                                 unless ($ishome) {
                                     my $fetch = "$fullpath/$title";
-                                    $fetch =~ s/^\Q$prefix$dir\E//;
+                                    $fetch =~ s/^\Q$prefix$dir\E//; 
                                     $prompttofetch{$fetch} = 1;
                                 }
                             }
@@ -15556,13 +16243,13 @@ generated by lonerrorhandler.pm, CHECKRP
 lonsupportreq.pm, loncoursequeueadmin.pm, searchcat.pl respectively.
 
 Inputs:
-defmail (scalar - email address of default recipient),
+defmail (scalar - email address of default recipient), 
 mailing type (scalar: errormail, packagesmail, helpdeskmail,
 requestsmail, updatesmail, or idconflictsmail).
 
 defdom (domain for which to retrieve configuration settings),
 
-origmail (scalar - email address of recipient from loncapa.conf,
+origmail (scalar - email address of recipient from loncapa.conf, 
 i.e., predates configuration by DC via domainprefs.pm
 
 $requname username of requester (if mailing type is helpdeskmail)
@@ -15571,6 +16258,7 @@ $requdom domain of requester (if mailing
 
 $reqemail e-mail address of requester (if mailing type is helpdeskmail)
 
+
 Returns: comma separated list of addresses to which to send e-mail.
 
 =back
@@ -15814,6 +16502,91 @@ sub build_recipient_list {
 
 =pod
 
+=over 4
+
+=item * &mime_email()
+
+Sends an email with a possible attachment
+
+Inputs:
+
+=over 4
+
+from -              Sender's email address
+
+replyto -           Reply-To email address
+
+to -                Email address of recipient
+
+subject -           Subject of email
+
+body -              Body of email
+
+cc_string -         Carbon copy email address
+
+bcc -               Blind carbon copy email address
+
+attachment_path -   Path of file to be attached
+
+file_name -         Name of file to be attached
+
+attachment_text -   The body of an attachment of type "TEXT"
+
+=back
+
+=back
+
+=cut
+
+############################################################
+############################################################
+
+sub mime_email {
+    my ($from,$replyto,$to,$subject,$body,$cc_string,$bcc,$attachment_path, 
+        $file_name,$attachment_text) = @_;
+ 
+    my $msg = MIME::Lite->new(
+             From    => $from,
+             To      => $to,
+             Subject => $subject,
+             Type    =>'TEXT',
+             Data    => $body,
+             );
+    if ($replyto ne '') {
+        $msg->add("Reply-To" => $replyto);
+    }
+    if ($cc_string ne '') {
+        $msg->add("Cc" => $cc_string);
+    }
+    if ($bcc ne '') {
+        $msg->add("Bcc" => $bcc);
+    }
+    $msg->attr("content-type"         => "text/plain");
+    $msg->attr("content-type.charset" => "UTF-8");
+    # Attach file if given
+    if ($attachment_path) {
+        unless ($file_name) {
+            if ($attachment_path =~ m-/([^/]+)$-) { $file_name = $1; }
+        }
+        my ($type, $encoding) = MIME::Types::by_suffix($attachment_path);
+        $msg->attach(Type     => $type,
+                     Path     => $attachment_path,
+                     Filename => $file_name
+                     );
+    # Otherwise attach text if given
+    } elsif ($attachment_text) {
+        $msg->attach(Type => 'TEXT',
+                     Data => $attachment_text);
+    }
+    # Send it
+    $msg->send('sendmail');
+}
+
+############################################################
+############################################################
+
+=pod
+
 =head1 Course Catalog Routines
 
 =over 4
@@ -15918,6 +16691,8 @@ sub extract_categories {
                     $trailstr = &mt('Official courses (with institutional codes)');
                 } elsif ($name eq 'communities') {
                     $trailstr = &mt('Communities');
+                } elsif ($name eq 'placement') {
+                    $trailstr = &mt('Placement Tests');
                 } else {
                     $trailstr = $name;
                 }
@@ -16067,8 +16842,10 @@ sub assign_categories_table {
                     next if ($parent eq 'instcode');
                     if ($type eq 'Community') {
                         next unless ($parent eq 'communities');
+                    } elsif ($type eq 'Placement') {
+                        next unless ($parent eq 'placement');
                     } else {
-                        next if ($parent eq 'communities');
+                        next if (($parent eq 'communities') || ($parent eq 'placement'));
                     }
                     my $css_class = $itemcount%2?' class="LC_odd_row"':'';
                     my $item = &escape($parent).'::0';
@@ -16081,6 +16858,8 @@ sub assign_categories_table {
                     my $parent_title = $parent;
                     if ($parent eq 'communities') {
                         $parent_title = &mt('Communities');
+                    } elsif ($parent eq 'placement') {
+                        $parent_title = &mt('Placement Tests');
                     }
                     $table .= '<tr '.$css_class.'><td><span class="LC_nobreak">'.
                               '<input type="checkbox" name="usecategory" value="'.
@@ -16186,27 +16965,33 @@ sub assign_category_rows {
 
 
 sub commit_customrole {
-    my ($udom,$uname,$url,$three,$four,$five,$start,$end,$context) = @_;
+    my ($udom,$uname,$url,$three,$four,$five,$start,$end,$context,$othdomby,$requester) = @_;
+    my $result = &Apache::lonnet::assigncustomrole(
+                     $udom,$uname,$url,$three,$four,$five,$end,$start,undef,undef,
+                     $context,$othdomby,$requester);
     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 ($udom,$uname,$url,$three,$start,$end,$one,$two,$sec,$context,$credits,
+        $othdomby,$requester) = @_;
+    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,$othdomby,
+                                      $requester);
         if (($result =~ /^error/) || ($result eq 'not_in_class') || 
             ($result eq 'unknown_course') || ($result eq 'refused')) {
             $output = $logmsg.' '.&mt('Error: ').$result."\n"; 
@@ -16226,19 +17011,24 @@ 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,$othdomby,$requester);
         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 {
     my ($logmsg,$udom,$uname,$url,$three,$start,$end,$one,$two,$sec,$context,
-        $credits) = @_;
+        $credits,$othdomby,$requester) = @_;
     my ($result,$linefeed,$oldsecurl,$newsecurl);
     if ($context eq 'auto') {
         $linefeed = "\n";
@@ -16262,8 +17052,9 @@ sub commit_studentrole {
                 }
                 $oldsecurl = $uurl;
                 $expire_role_result = 
-                    &Apache::lonnet::assignrole($udom,$uname,$uurl,'st',$now,'','','',$context);
-                if ($env{'request.course.sec'} ne '') { 
+                    &Apache::lonnet::assignrole($udom,$uname,$uurl,'st',$now,
+                                                '','','',$context,$othdomby,$requester);
+                if ($env{'request.course.sec'} ne '') {
                     if ($expire_role_result eq 'refused') {
                         my @roles = ('st');
                         my @statuses = ('previous');
@@ -16289,7 +17080,8 @@ sub commit_studentrole {
                 &Apache::lonnet::modify_student_enrollment($udom,$uname,undef,undef,
                                                            undef,undef,undef,$sec,
                                                            $end,$start,'','',$cid,
-                                                           '',$context,$credits);
+                                                           '',$context,$credits,'',
+                                                           $othdomby,$requester);
             if ($modify_section_result =~ /^ok/) {
                 if ($secchange == 1) {
                     if ($sec eq '') {
@@ -16311,7 +17103,7 @@ sub commit_studentrole {
                     }
                 }
             } else {
-                if ($secchange) {       
+                if ($secchange) { 
                     $$logmsg .= &mt('Error when attempting section change for [_1] from old section "[_2]" to new section: "[_3]" in course [_4] -error:',$uname,$oldsec,$sec,$cid).' '.$modify_section_result.$linefeed;
                 } else {
                     $$logmsg .= &mt('Error when attempting to modify role for [_1] for section: "[_2]" in course [_3] -error:',$uname,$sec,$cid).' '.$modify_section_result.$linefeed;
@@ -16431,7 +17223,7 @@ sub check_clone {
                             if ($args->{'ccdomain'} eq $args->{'clonedomain'}) {
                                 $can_clone = 1;
                             }
-                        } elsif (($clonehash{'internal.coursecode'}) && ($args->{'crscode'}) &&
+                        } elsif (($clonehash{'internal.coursecode'}) && ($args->{'crscode'}) && 
                                  ($args->{'clonedomain'} eq  $args->{'course_domain'})) {
                             if (&Apache::lonnet::default_instcode_cloning($args->{'clonedomain'},$domdefs{'canclone'},
                                                                           $clonehash{'internal.coursecode'},$args->{'crscode'})) {
@@ -16450,7 +17242,7 @@ sub check_clone {
                     $can_clone = 1;
                 }
                 unless ($can_clone) {
-                    if (($clonehash{'internal.coursecode'}) && ($args->{'crscode'}) &&
+                    if (($clonehash{'internal.coursecode'}) && ($args->{'crscode'}) && 
                         ($args->{'clonedomain'} eq  $args->{'course_domain'})) {
                         my (%gotdomdefaults,%gotcodedefaults);
                         foreach my $cloner (@cloners) {
@@ -16489,12 +17281,12 @@ sub check_clone {
                 if ($args->{'crstype'} eq 'Community') {
                     $ccrole = 'co';
                 }
-                my %roleshash =
-                    &Apache::lonnet::get_my_roles($args->{'ccuname'},
-                                                  $args->{'ccdomain'},
+	        my %roleshash =
+		    &Apache::lonnet::get_my_roles($args->{'ccuname'},
+					          $args->{'ccdomain'},
                                                   'userroles',['active'],[$ccrole],
-                                                  [$args->{'clonedomain'}]);
-                if ($roleshash{$args->{'clonecourse'}.':'.$args->{'clonedomain'}.':'.$ccrole}) {
+					          [$args->{'clonedomain'}]);
+	        if ($roleshash{$args->{'clonecourse'}.':'.$args->{'clonedomain'}.':'.$ccrole}) {
                     $can_clone = 1;
                 } elsif (&Apache::lonnet::is_course_owner($args->{'clonedomain'},$args->{'clonecourse'},
                                                           $args->{'ccuname'},$args->{'ccdomain'})) {
@@ -16520,7 +17312,7 @@ sub check_clone {
                                       mt => 'The new course could not be cloned from the existing course because the new course owner ([_1]) does not have cloning rights in the existing course ([_2]).',
                                       args => [$args->{'ccuname'}.':'.$args->{'ccdomain'},$clonedesc{'description'}],
                                     }));
-	        }
+                }
 	    }
         }
     }
@@ -16550,7 +17342,12 @@ sub construct_course {
 #
 # Open course
 #
-    my $crstype = lc($args->{'crstype'});
+    my $showncrstype;
+    if ($args->{'crstype'} eq 'Placement') {
+        $showncrstype = 'placement test'; 
+    } else {  
+        $showncrstype = lc($args->{'crstype'});
+    }
     my %cenv=();
     $$courseid=&Apache::lonnet::createcourse($args->{'course_domain'},
                                              $args->{'cdescr'},
@@ -16569,9 +17366,9 @@ sub construct_course {
     # if anyone ever decides to not show this, and Utils::Course::new
     # will need to be suitably modified.
     if (($callercontext eq 'auto') && ($user_lh ne '')) {
-        $outcome .= &mt_user($user_lh,'New LON-CAPA [_1] ID: [_2]',$crstype,$$courseid).$linefeed;
+        $outcome .= &mt_user($user_lh,'New LON-CAPA [_1] ID: [_2]',$showncrstype,$$courseid).$linefeed;
     } else {
-        $outcome .= &mt('New LON-CAPA [_1] ID: [_2]',$crstype,$$courseid).$linefeed;
+        $outcome .= &mt('New LON-CAPA [_1] ID: [_2]',$showncrstype,$$courseid).$linefeed;
     }
     if ($$courseid =~ /^error:/) {
         return (0,$outcome,$clonemsgref);
@@ -16596,19 +17393,19 @@ sub construct_course {
 
 #
 # Do the cloning
-#
+#   
     my @clonemsg;
     if ($can_clone && $cloneid) {
         push(@clonemsg,
                       {
                           mt => 'Created [_1] by cloning from [_2]',
-                          args => [$crstype,$clonetitle],
+                          args => [$showncrstype,$clonetitle],
                       });
 	my %oldcenv=&Apache::lonnet::dump('environment',$$crsudom,$$crsunum);
 # Copy all files
         my @info =
-            &Apache::lonclonecourse::copycoursefiles($cloneid,$$courseid,$args->{'datemode'},
-                                                     $args->{'dateshift'},$args->{'crscode'},
+	    &Apache::lonclonecourse::copycoursefiles($cloneid,$$courseid,$args->{'datemode'},
+	                                             $args->{'dateshift'},$args->{'crscode'},
                                                      $args->{'ccuname'}.':'.$args->{'ccdomain'},
                                                      $args->{'tinyurls'});
         if (@info) {
@@ -16785,14 +17582,14 @@ sub construct_course {
             $outcome .= $linefeed;
         } else {
             $outcome .= "</ul><br /><br /></div>\n";
-        }
+        } 
     }
     if ($args->{'no_end_date'}) {
         $args->{'endaccess'} = 0;
     }
-#  If an official course with institutional sections is created by cloning
+#  If an official course with institutional sections is created by cloning 
 #  an existing course, section-specific hiding of course totals in student's
-#  view of grades as copied from cloned course, will be checked for valid
+#  view of grades as copied from cloned course, will be checked for valid 
 #  sections.
     if (($can_clone && $cloneid) &&
         ($cenv{'internal.coursecode'} ne '') &&
@@ -16884,7 +17681,7 @@ sub construct_course {
             if (ref($crsinfo{$$crsudom.'_'.$$crsunum}) eq 'HASH') {
                 $crsinfo{$$crsudom.'_'.$$crsunum}{'uniquecode'} = $code;
                 my $putres = &Apache::lonnet::courseidput($$crsudom,\%crsinfo,$crsuhome,'notime');
-            }
+            } 
             if (ref($coderef)) {
                 $$coderef = $code;
             }
@@ -16960,6 +17757,30 @@ sub construct_course {
         $outcome .= ($fatal?$errtext:'write ok').$linefeed;
     }
 
+# 
+# Set params for Placement Tests
+#
+    if ($args->{'crstype'} eq 'Placement') {
+       my %storecontent; 
+       my $prefix=$$crsudom.'_'.$$crsunum.'.0.';
+       my %defaults = (
+                        buttonshide   => { value => 'yes',
+                                           type => 'string_yesno',},
+                        type          => { value => 'randomizetry',
+                                           type  => 'string_questiontype',},
+                        maxtries      => { value => 1,
+                                           type => 'int_pos',},
+                        problemstatus => { value => 'no',
+                                           type  => 'string_problemstatus',},
+                      );
+       foreach my $key (keys(%defaults)) {
+           $storecontent{$prefix.$key} = $defaults{$key}{'value'};
+           $storecontent{$prefix.$key.'.type'} = $defaults{$key}{'type'};
+       }
+       &Apache::lonnet::cput
+                 ('resourcedata',\%storecontent,$$crsudom,$$crsunum); 
+    }
+
     return (1,$outcome,\@clonemsg);
 }
 
@@ -16973,7 +17794,7 @@ sub make_unique_code {
     my $tries = 0;
     my $gotlock = &Apache::lonnet::newput_dom('uniquecodes',$lockhash,$cdom);
     my ($code,$error);
-
+  
     while (($gotlock ne 'ok') && ($tries<3)) {
         $tries ++;
         sleep 1;
@@ -17020,8 +17841,7 @@ sub generate_code {
 ############################################################
 ############################################################
 
-#SD
-# only Community and Course, or anything else?
+# Community, Course and Placement Test
 sub course_type {
     my ($cid) = @_;
     if (!defined($cid)) {
@@ -17039,17 +17859,19 @@ sub group_term {
     my %names = (
                   'Course' => 'group',
                   'Community' => 'group',
+                  'Placement' => 'group',
                 );
     return $names{$crstype};
 }
 
 sub course_types {
-    my @types = ('official','unofficial','community','textbook','lti');
+    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);
@@ -17155,8 +17977,6 @@ sub init_user_environment {
 
     my $public=($username eq 'public' && $domain eq 'public');
 
-# See if old ID present, if so, remove
-
     my ($filename,$cookie,$userroles,$firstaccenv,$timerintenv,
         $coauthorenv);
     my $now=time;
@@ -17179,7 +17999,8 @@ sub init_user_environment {
 	}
 	if (!$cookie) { $cookie="publicuser_$oldest"; }
     } else {
-	# if this isn't a robot, kill any existing non-robot sessions
+	# See if old ID present, if so, remove if this isn't a robot,
+	# killing any existing non-robot sessions
 	if (!$args->{'robot'}) {
 	    opendir(DIR,$lonids);
 	    while ($filename=readdir(DIR)) {
@@ -17235,8 +18056,7 @@ sub init_user_environment {
 
     my %userenv = &Apache::lonnet::dump('environment',$domain,$username);
     my ($tmp) = keys(%userenv);
-    if ($tmp !~ /^(con_lost|error|no_such_host)/i) {
-    } else {
+    if ($tmp =~ /^(con_lost|error|no_such_host)/i) {
 	undef(%userenv);
     }
     if (($userenv{'interface'}) && (!$form->{'interface'})) {
@@ -17251,7 +18071,7 @@ sub init_user_environment {
 # --------------------------------------------------------- Write first profile
 
     {
-        my $ip = &Apache::lonnet::get_requestor_ip();
+        my $ip = &Apache::lonnet::get_requestor_ip($r);
 	my %initial_env = 
 	    ("user.name"          => $username,
 	     "user.domain"        => $domain,
@@ -17302,13 +18122,13 @@ 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','timezone') {
-                $userenv{'availabletools.'.$tool} = 
+            foreach my $tool ('aboutme','blog','webdav','portfolio','portaccess','timezone') {
+                $userenv{'availabletools.'.$tool} =
                     &Apache::lonnet::usertools_access($username,$domain,$tool,'reload',
                                                       undef,\%userenv,\%domdef,\%is_adv);
             }
 
-            foreach my $crstype ('official','unofficial','community','textbook','lti') {
+            foreach my $crstype ('official','unofficial','community','textbook','placement','lti') {
                 $userenv{'canrequest.'.$crstype} =
                     &Apache::lonnet::usertools_access($username,$domain,$crstype,
                                                       'reload','requestcourses',
@@ -17339,14 +18159,42 @@ sub init_user_environment {
             my %reqauthor = &Apache::lonnet::get('requestauthor',['author_status','author'],
                                                  $domain,$username);
             my $reqstatus = $reqauthor{'author_status'};
-            if ($reqstatus eq 'approval' || $reqstatus eq 'approved') {
+            if ($reqstatus eq 'approval' || $reqstatus eq 'approved') { 
                 if (ref($reqauthor{'author'}) eq 'HASH') {
                     $userenv{'requestauthorqueued'} = $reqstatus.':'.
                                                       $reqauthor{'author'}{'timestamp'};
                 }
             }
+            my ($types,$typename) = &course_types();
+            if (ref($types) eq 'ARRAY') {
+                my @options = ('approval','validate','autolimit');
+                my $optregex = join('|',@options);
+                my (%willtrust,%trustchecked);
+                foreach my $type (@{$types}) {
+                    my $dom_str = $env{'environment.reqcrsotherdom.'.$type};
+                    if ($dom_str ne '') {
+                        my $updatedstr = '';
+                        my @possdomains = split(',',$dom_str);
+                        foreach my $entry (@possdomains) {
+                            my ($extdom,$extopt) = split(':',$entry);
+                            unless ($trustchecked{$extdom}) {
+                                $willtrust{$extdom} = &Apache::lonnet::will_trust('reqcrs',$domain,$extdom);
+                                $trustchecked{$extdom} = 1;
+                            }
+                            if ($willtrust{$extdom}) {
+                                $updatedstr .= $entry.',';
+                            }
+                        }
+                        $updatedstr =~ s/,$//;
+                        if ($updatedstr) {
+                            $userenv{'reqcrsotherdom.'.$type} = $updatedstr;
+                        } else {
+                            delete($userenv{'reqcrsotherdom.'.$type});
+                        }
+                    }
+                }
+            }
         }
-
 	$env{'user.environment'} = "$lonids/$cookie.id";
 
 	if (tie(my %disk_env,'GDBM_File',"$lonids/$cookie.id",
@@ -17455,12 +18303,12 @@ and quotacheck.pl
 
 Inputs:
 
-filterlist - anonymous array of fields to include as potential filters
+filterlist - anonymous array of fields to include as potential filters 
 
 crstype - course type
 
 roleelement - fifth arg in selectcourse_link() populates fifth arg in javascript: opencrsbrowser() function, used
-              to pop-open a course selector (will contain "extra element").
+              to pop-open a course selector (will contain "extra element"). 
 
 multelement - if multiple course selections will be allowed, this will be a hidden form element: name: multiple; value: 1
 
@@ -17476,19 +18324,19 @@ cloneruname - username of owner of new c
 
 clonerudom - domain of owner of new course who wants to clone
 
-typeelem - text to use for left column in row containing course type (i.e., Course, Community or Course/Community)
+typeelem - text to use for left column in row containing course type (i.e., Course, Community or Course/Community) 
 
 codetitlesref - reference to array of titles of components in institutional codes (official courses)
 
 codedom - domain
 
-formname - value of form element named "form".
+formname - value of form element named "form". 
 
 fixeddom - domain, if fixed.
 
-prevphase - value to assign to form element named "phase" when going back to the previous screen
+prevphase - value to assign to form element named "phase" when going back to the previous screen  
 
-cnameelement - name of form element in form on opener page which will receive title of selected course
+cnameelement - name of form element in form on opener page which will receive title of selected course 
 
 cnumelement - name of form element in form on opener page which will receive courseID  of selected course
 
@@ -17579,15 +18427,19 @@ sub build_filters {
         $createdfilterform = &timebased_select_form('createdfilter',$filter);
     }
 
+    my $prefix = $crstype;
+    if ($crstype eq 'Placement') {
+        $prefix = 'Placement Test'
+    }
     my %lt = &Apache::lonlocal::texthash(
-                'cac' => "$crstype Activity",
-                'ccr' => "$crstype Created",
-                'cde' => "$crstype Title",
-                'cdo' => "$crstype Domain",
+                'cac' => "$prefix Activity",
+                'ccr' => "$prefix Created",
+                'cde' => "$prefix Title",
+                'cdo' => "$prefix Domain",
                 'ins' => 'Institutional Code',
                 'inc' => 'Institutional Categorization',
-                'cow' => "$crstype Owner/Co-owner",
-                'cop' => "$crstype Personnel Includes",
+                'cow' => "$prefix Owner/Co-owner",
+                'cop' => "$prefix Personnel Includes",
                 'cog' => 'Type',
              );
 
@@ -17595,6 +18447,8 @@ sub build_filters {
         my $typeval = 'Course';
         if ($crstype eq 'Community') {
             $typeval = 'Community';
+        } elsif ($crstype eq 'Placement') {
+            $typeval = 'Placement';
         }
         $typeselectform = '<input type="hidden" name="type" value="'.$typeval.'" />';
     } else {
@@ -17603,9 +18457,15 @@ sub build_filters {
             $typeselectform .= ' onchange="'.$onchange.'"';
         }
         $typeselectform .= '>'."\n";
-        foreach my $posstype ('Course','Community') {
+        foreach my $posstype ('Course','Community','Placement') {
+            my $shown;
+            if ($posstype eq 'Placement') {
+                $shown = &mt('Placement Test');
+            } else {
+                $shown = &mt($posstype);
+            }
             $typeselectform.='<option value="'.$posstype.'"'.
-                ($posstype eq $crstype ? ' selected="selected" ' : ''). ">".&mt($posstype)."</option>\n";
+                ($posstype eq $crstype ? ' selected="selected" ' : ''). ">".$shown."</option>\n";
         }
         $typeselectform.="</select>";
     }
@@ -17630,7 +18490,7 @@ sub build_filters {
         if (exists($filter->{'instcodefilter'})) {
 #            if (($fixeddom) || ($formname eq 'requestcrs') ||
 #                ($formname eq 'modifycourse') || ($formname eq 'filterpicker')) {
-            if ($codedom) {
+            if ($codedom) { 
                 $officialjs = 1;
                 ($instcodeform,$jscript,$$numtitlesref) =
                     &Apache::courseclassifier::instcode_selectors($codedom,'filterpicker',
@@ -17759,7 +18619,7 @@ $typeelement
     return $jscript.$clonewarning.$output;
 }
 
-=pod
+=pod 
 
 =item * &timebased_select_form()
 
@@ -17774,7 +18634,7 @@ item - name of form element (sincefilter
 filter - anonymous hash of criteria and their values
 
 Returns: HTML for a select box contained a blank, then six time selections,
-         with value set in incoming form variables currently selected.
+         with value set in incoming form variables currently selected. 
 
 Side Effects: None
 
@@ -17811,7 +18671,7 @@ page load completion for page showing se
 
 Inputs: None
 
-Returns: markup containing updateFilters() and hideSearching() javascript functions.
+Returns: markup containing updateFilters() and hideSearching() javascript functions. 
 
 Side Effects: None
 
@@ -17850,7 +18710,7 @@ to retrieve a hash for which keys are co
 
 Inputs:
 
-dom - domain being searched
+dom - domain being searched 
 
 type - course type ('Course' or 'Community' or '.' if any).
 
@@ -17862,7 +18722,7 @@ cloneruname - optional username of new c
 
 clonerudom - optional domain of new course owner
 
-domcloner - optional "domcloner" flag; has value=1 if user has ccc priv in domain being filtered by,
+domcloner - optional "domcloner" flag; has value=1 if user has ccc priv in domain being filtered by, 
             (used when DC is using course creation form)
 
 codetitles - reference to array of titles of components in institutional codes (official courses).
@@ -17872,8 +18732,8 @@ cc_clone - escaped comma separated list
 
 reqcrsdom - domain of new course, where search_courses is used to identify potential courses to clone
 
-reqinstcode - institutional code of new course, where search_courses is used to identify potential
-              courses to clone
+reqinstcode - institutional code of new course, where search_courses is used to identify potential 
+              courses to clone 
 
 Returns: %courses - hash of courses satisfying search criteria, keys = course IDs, values are corresponding colon-separated escaped description, institutional code, owner and type.
 
@@ -17999,8 +18859,8 @@ $required - LON-CAPA version needed by c
 
 Returns:
 
-$switchserver - query string tp append to /adm/switchserver call (if
-                current server's LON-CAPA version is too old.
+$switchserver - query string tp append to /adm/switchserver call (if 
+                current server's LON-CAPA version is too old. 
 
 $warning - Message is displayed if no suitable server could be found.
 
@@ -18113,7 +18973,7 @@ Inputs:
 $loncaparev - Version on current server (format: Major.Minor.Subrelease-datestamp)
 
 $interval (optional) - Time which may elapse (in s) between last check for content
-                       change in current course. (default: 600 s).
+                       change in current course. (default: 600 s).  
 
 Returns: an array; first element is:
 
@@ -18121,9 +18981,9 @@ Returns: an array; first element is:
 
 'switch' - if content updates mean user's session
            needs to be switched to a server running a newer LON-CAPA version
-
+ 
 'update' - if course session needs to be refreshed (i.e., Big Hash needs to be reloaded)
-           on current server hosting user's session
+           on current server hosting user's session                
 
 ''       - if no action required.
 
@@ -18131,10 +18991,10 @@ Returns: an array; first element is:
 
 If first item element is 'switch':
 
-second item is $switchwarning - Warning message if no suitable server found to host session.
+second item is $switchwarning - Warning message if no suitable server found to host session. 
 
 third item is $switchserver - query string to append to /adm/switchserver containing lonHostID
-                              and current role.
+                              and current role. 
 
 otherwise: no other elements returned.
 
@@ -18207,19 +19067,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}));
@@ -18232,8 +19104,20 @@ 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);
     }
+    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);
+        }
+    }
     unless (($reqdmajor eq '') && ($reqdminor eq '')) {
         &Apache::lonnet::update_released_required($reqdmajor.'.'.$reqdminor,$cdom,$cnum,$chome,$cid);
     }
@@ -18568,7 +19452,7 @@ sub validate_folderpath {
 sub captcha_display {
     my ($context,$lonhost,$defdom) = @_;
     my ($output,$error);
-    my ($captcha,$pubkey,$privkey,$version) =
+    my ($captcha,$pubkey,$privkey,$version) = 
         &get_captcha_config($context,$lonhost,$defdom);
     if ($captcha eq 'original') {
         $output = &create_captcha();
@@ -18639,7 +19523,7 @@ sub get_captcha_config {
                 $captcha = 'recaptcha';
                 $version = $domconfhash{$serverhomedom.'.login.recaptchaversion'};
                 if ($version ne '2') {
-                    $version = 1;
+                    $version = 1; 
                 }
             } else {
                 $captcha = 'original';
@@ -18668,7 +19552,7 @@ sub get_captcha_config {
                 $captcha = 'original';
             }
         }
-    }
+    } 
     return ($captcha,$pubkey,$privkey,$version);
 }
 
@@ -18747,22 +19631,27 @@ sub create_recaptcha {
                &mt('If the text is hard to read, [_1] will replace them.',
                    '<img src="/res/adm/pages/refresh.gif" alt="reCAPTCHA refresh" />').
                '<br /><br />';
-     }
+    }
 }
 
 sub check_recaptcha {
     my ($privkey,$version) = @_;
     my $captcha_chk;
-    my $ip = &Apache::lonnet::get_requestor_ip(); 
+    my $ip = &Apache::lonnet::get_requestor_ip();
     if ($version >= 2) {
-        my $ua = LWP::UserAgent->new;
-        $ua->timeout(10);
         my %info = (
-                     secret   => $privkey,
+                     secret   => $privkey, 
                      response => $env{'form.g-recaptcha-response'},
                      remoteip => $ip,
                    );
-        my $response = $ua->post('https://www.google.com/recaptcha/api/siteverify',\%info);
+        my $request=new HTTP::Request('POST','https://www.google.com/recaptcha/api/siteverify');
+        $request->content(join('&',map {
+                         my $name = escape($_);
+                         "$name=" . ( ref($info{$_}) eq 'ARRAY'
+                         ? join("&$name=", map {escape($_) } @{$info{$_}})
+                         : &escape($info{$_}) );
+        } keys(%info)));
+        my $response = &LONCAPA::LWPReq::makerequest('',$request,'','',10,1);
         if ($response->is_success)  {
             my $data = JSON::DWIW->from_json($response->decoded_content);
             if (ref($data) eq 'HASH') {
@@ -18825,7 +19714,7 @@ sub cleanup_html {
 
 # Checks for critical messages and returns a redirect url if one exists.
 # $interval indicates how often to check for messages.
-# $context is the calling context -- roles, grades, contents, menu or flip.
+# $context is the calling context -- roles, grades, contents, menu or flip. 
 sub critical_redirect {
     my ($interval,$context) = @_;
     unless (($env{'user.domain'} ne '') && ($env{'user.name'} ne '')) {
@@ -18847,18 +19736,18 @@ sub critical_redirect {
                 }
             }
         }
-        my @what=&Apache::lonnet::dump('critical', $env{'user.domain'},
+        my @what=&Apache::lonnet::dump('critical', $env{'user.domain'}, 
                                         $env{'user.name'});
         &Apache::lonnet::appenv({'user.criticalcheck.time'=>time});
         my $redirecturl;
         if ($what[0]) {
-            if (($what[0] ne 'con_lost') && ($what[0] ne 'no_such_host') && ($what[0]!~/^error\:/)) {
-                $redirecturl='/adm/email?critical=display';
-                my $url=&Apache::lonnet::absolute_url().$redirecturl;
+	    if (($what[0] ne 'con_lost') && ($what[0] ne 'no_such_host') && ($what[0]!~/^error\:/)) {
+	        $redirecturl='/adm/email?critical=display';
+	        my $url=&Apache::lonnet::absolute_url().$redirecturl;
                 return (1, $url);
             }
         }
-    }
+    } 
     return ();
 }
 
@@ -19057,7 +19946,6 @@ sub shorten_symbs {
         } else {
             foreach my $key (keys(%collisions)) {
                 $failed->{$key} = 1;
-                $failed->{$key} = 1;
             }
         }
     }
@@ -19087,9 +19975,7 @@ sub is_nonframeable {
     }
     my $uselink;
     my $request = new HTTP::Request('HEAD',$url);
-    my $ua = LWP::UserAgent->new;
-    $ua->timeout(5);
-    my $response=$ua->request($request);
+    my $response = &LONCAPA::LWPReq::makerequest('',$request,'','',5);
     if ($response->is_success()) {
         my $secpolicy = lc($response->header('content-security-policy'));
         my $xframeop = lc($response->header('x-frame-options'));