--- loncom/interface/loncommon.pm 2024/12/29 01:49:56 1.1075.2.175 +++ loncom/interface/loncommon.pm 2025/01/06 00:22:57 1.1445 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # a pile of common routines # -# $Id: loncommon.pm,v 1.1075.2.175 2024/12/29 01:49:56 raeburn Exp $ +# $Id: loncommon.pm,v 1.1445 2025/01/06 00:22:57 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -61,6 +61,7 @@ use POSIX qw(strftime mktime); use Apache::lonmenu(); use Apache::lonenc(); use Apache::lonlocal; +use Apache::lonnavmaps(); use HTML::Entities; use Apache::lonhtmlcommon(); use Apache::loncoursedata(); @@ -70,18 +71,25 @@ 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(); +use Short::URL(); # ---------------------------------------------- Designs use vars qw(%defaultdesign); @@ -166,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; @@ -200,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); @@ -427,7 +437,7 @@ sub studentbrowser_javascript { <script type="text/javascript" language="Javascript"> // <![CDATA[ var stdeditbrowser; - function openstdbrowser(formname,uname,udom,clicker,roleflag,ignorefilter,courseadv) { + function openstdbrowser(formname,uname,udom,clicker,roleflag,ignorefilter,courseadv,uident) { var url = '/adm/pickstudent?'; var filter; if (!ignorefilter) { @@ -448,6 +458,7 @@ sub studentbrowser_javascript { } } if ((courseadv == 'only') || (courseadv == 'none')) { url+="&courseadv="+courseadv; } + if (uident !== '') { url+="&identelement="+uident; } var title = 'Student_Browser'; var options = 'scrollbars=1,resizable=1,menubar=0'; options += ',width=700,height=600'; @@ -479,7 +490,7 @@ ENDRESBRW } sub selectstudent_link { - my ($form,$unameele,$udomele,$courseadv,$clickerid)=@_; + my ($form,$unameele,$udomele,$courseadv,$clickerid,$identelem)=@_; my $callargs = "'".&Apache::lonhtmlcommon::entity_encode($form)."','". &Apache::lonhtmlcommon::entity_encode($unameele)."','". &Apache::lonhtmlcommon::entity_encode($udomele)."'"; @@ -496,6 +507,11 @@ sub selectstudent_link { $callargs .= ",'','','$courseadv'"; } elsif ($courseadv eq 'condition') { $callargs .= ",'','','$courseadv'"; + } elsif ($identelem ne '') { + $callargs .= ",'','',''"; + } + if ($identelem ne '') { + $callargs .= ",'".&Apache::lonhtmlcommon::entity_encode($identelem)."'"; } return '<span class="LC_nobreak">'. '<a href="javascript:openstdbrowser('.$callargs.');">'. @@ -686,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)); @@ -887,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 = ''; @@ -922,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; } } @@ -947,8 +965,8 @@ ENDSCRT } sub select_timezone { - my ($name,$selected,$onchange,$includeempty,$disabled)=@_; - my $output='<select name="'.$name.'" '.$onchange.$disabled.'>'."\n"; + my ($name,$selected,$onchange,$includeempty,$id,$disabled)=@_; + my $output='<select name="'.$name.'" '.$id.$onchange.$disabled.'>'."\n"; if ($includeempty) { $output .= '<option value=""'; if (($selected eq '') || ($selected eq 'local')) { @@ -1003,7 +1021,7 @@ sub select_datelocale { } $locale_names{$id} = Encode::encode('UTF-8',$locale_names{$id}); push(@possibles,$id); - } + } } } foreach my $item (sort(@possibles)) { @@ -1039,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 @@ -1071,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 @@ -1125,7 +1173,8 @@ sub linked_select_forms { $hashref, $menuorder, $onchangefirst, - $onchangesecond + $onchangesecond, + $suffix ) = @_; my $second = "document.$formname.$secondselectname"; my $first = "document.$formname.$firstselectname"; @@ -1133,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}); @@ -1156,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]; @@ -1182,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}; @@ -1193,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) { @@ -1218,7 +1270,7 @@ END =pod -=item * &help_open_topic($topic,$text,$stayOnPage,$width,$height,$imgid) +=item * &help_open_topic($topic,$text,$stayOnPage,$width,$height,$imgid,$links_target) Returns a string corresponding to an HTML link to the given help $topic, where $topic corresponds to the name of a .tex file in @@ -1242,10 +1294,12 @@ $imgid is the id of the img tag used for used in a javascript call to switch the image src. See lonhtmlcommon::htmlareaselectactive() for an example. +$links_target will optionally be set to a target (_top, _parent or _self). + =cut sub help_open_topic { - my ($topic, $text, $stayOnPage, $width, $height, $imgid) = @_; + my ($topic, $text, $stayOnPage, $width, $height, $imgid, $links_target) = @_; $text = "" if (not defined $text); $stayOnPage = 0 if (not defined $stayOnPage); $width = 500 if (not defined $width); @@ -1259,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 { @@ -1271,9 +1321,16 @@ sub help_open_topic { } # Add the text - if ($text ne "") { + my $target = ' target="_top"'; + if ($links_target) { + $target = ' target="'.$links_target.'"'; + } elsif ((($env{'request.lti.login'}) && ($env{'request.lti.target'} eq 'iframe')) || + (($env{'request.deeplink.login'}) && ($env{'request.deeplink.target'} eq '_self'))) { + $target = ''; + } + if ($text ne "") { $template.='<span class="LC_help_open_topic">' - .'<a target="_top" href="'.$link.'">' + .'<a'.$target.' href="'.$link.'">' .$text.'</a>'; } @@ -1283,7 +1340,7 @@ sub help_open_topic { if ($imgid ne '') { $imgid = ' id="'.$imgid.'"'; } - $template.=' <a target="_top" href="'.$link.'" title="'.$title.'">' + $template.=' <a'.$target.' href="'.$link.'" title="'.$title.'">' .'<img src="'.$helpicon.'" border="0"' .' alt="'.&mt('Help: [_1]',$topic).'"' .' title="'.$title.'" style="vertical-align:middle;"'.$imgid @@ -1312,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; @@ -1356,20 +1413,20 @@ ENDOUTPUT # now just updates the help link and generates a blue icon sub help_open_menu { - my ($topic,$component_help,$faq,$bug,$stayOnPage,$width,$height,$text) + my ($topic,$component_help,$faq,$bug,$stayOnPage,$width,$height,$text,$links_target) = @_; $stayOnPage = 1; my $output; if ($component_help) { if (!$text) { $output=&help_open_topic($component_help,undef,$stayOnPage, - $width,$height); + $width,$height,'',$links_target); } else { my $help_text; $help_text=&unescape($topic); $output='<table><tr><td>'. &help_open_topic($component_help,$help_text,$stayOnPage, - $width,$height).'</td></tr></table>'; + $width,$height,'',$links_target).'</td></tr></table>'; } } my $banner_link = &update_help_link($topic,$component_help,$faq,$bug,$stayOnPage); @@ -1379,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')" @@ -1414,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, @@ -1478,19 +1533,26 @@ sub help_open_bug { { $link = $url; } + + my $target = '_top'; + if ((($env{'request.lti.login'}) && ($env{'request.lti.target'} eq 'iframe')) || + (($env{'request.deeplink.login'}) && ($env{'request.deeplink.target'} eq '_self'))) { + $target = '_blank'; + } + # Add the text if ($text ne "") { $template .= "<table bgcolor='#AA3333' cellspacing='1' cellpadding='1' border='0'><tr>". - "<td bgcolor='#FF5555'><a target=\"_top\" href=\"$link\"><span style=\"color:#FFFFFF;font-size:10pt;\">$text</span></a>"; + "<td bgcolor='#FF5555'><a target=\"$target\" href=\"$link\"><span style=\"color:#FFFFFF;font-size:10pt;\">$text</span></a>"; } # Add the graphic my $title = &mt('Report a Bug'); my $bugicon=&lonhttpdurl("/adm/lonMisc/smallBug.gif"); $template .= <<"ENDTEMPLATE"; - <a target="_top" href="$link" title="$title"><img src="$bugicon" border="0" alt="(Bug: $topic)" /></a> + <a target="$target" href="$link" title="$title"><img src="$bugicon" border="0" alt="(Bug: $topic)" /></a> ENDTEMPLATE if ($text ne '') { $template.='</td></tr></table>' }; return $template; @@ -1702,8 +1764,6 @@ the id of the element to resize, second surrounds everything that comes after the textarea, this routine needs to be attached to the <body> for the onload and onresize events. -=back - =cut sub resize_textarea_js { @@ -1756,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[> @@ -1801,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); @@ -1838,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]+)\$/; @@ -1851,7 +2114,7 @@ sub colorfuleditor_js { } function getTagList () { - + var stringToSearch = document.lonhomework.innerHTML; var ret = new Array(); @@ -1859,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(/"/, '')); } } @@ -1896,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; @@ -1919,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); @@ -1938,7 +2201,7 @@ sub colorfuleditor_js { } } - +$browse_or_search // ]]> </script> @@ -1986,10 +2249,355 @@ 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'}.': '. + '<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'}.': <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() + +emits javascript containing two global vars to facilitate handling of resizing +by code in iframe_wrapper_resizejs() used when an iframe is present in a page +with standard LON-CAPA menus. + +=cut + +# +# Where iframe is in use, if window.onload() executes before the custom resize function +# has been defined (jQuery), two global javascript vars (LCnotready and LCresizedef) +# are used to ensure document.ready() triggers a call to resize, so the iframe contents +# do not obscure the Functions menu. +# + +sub iframe_wrapper_headjs { + return <<"ENDJS"; +<script type="text/javascript"> +// <![CDATA[ +var LCnotready = 0; +var LCresizedef = 0; +// ]]> +</script> + +ENDJS + +} + +=pod + +=item * &iframe_wrapper_resizejs() + +emits javascript used to handle resizing for a page containing +an iframe, to ensure that the iframe does not obscure any +standard LON-CAPA menu items. + +=back + +=cut + +# +# jQuery to use when iframe is in use and a page resize occurs. +# This script will ensure that the iframe does not obscure any +# standard LON-CAPA inline menus (primary, secondary, and/or +# breadcrumbs and Functions menus. Expects javascript from +# &iframe_wrapper_headjs() to be in head portion of the web page, +# e.g., by inclusion in second arg passed to &start_page(). +# + +sub iframe_wrapper_resizejs { + my $offset = 5; + &get_unprocessed_cgi($ENV{'QUERY_STRING'},['inhibitmenu']); + if (($env{'form.inhibitmenu'} eq 'yes') || ($env{'form.only_body'})) { + $offset = 0; + } + return &Apache::lonhtmlcommon::scripttag(<<SCRIPT); + \$(document).ready( function() { + \$(window).unbind('resize').resize(function(){ + var header = null; + var offset = $offset; + var height = 0; + var hdrtop = 0; + if (\$('div.LC_menus_content:first').length) { + if (\$('div.LC_menus_content:first').hasClass ("shown")) { + header = \$('div.LC_menus_content:first'); + offset = 12; + } + } else if (\$('div.LC_head_subbox:first').length) { + header = \$('div.LC_head_subbox:first'); + offset = 9; + } else { + if (\$('#LC_breadcrumbs').length) { + header = \$('#LC_breadcrumbs'); + } + } + if (header != null && header.length) { + height = header.height(); + hdrtop = header.position().top; + } + var pos = height + hdrtop + offset; + \$('.LC_iframecontainer').css('top', pos); + }); + LCresizedef = 1; + if (LCnotready == 1) { + LCnotready = 0; + \$(window).trigger('resize'); + } + }); + window.onload = function(){ + if (LCresizedef) { + LCnotready = 0; + \$(window).trigger('resize'); + } else { + LCnotready = 1; + } + }; +SCRIPT + +} =pod @@ -2178,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))]; @@ -2258,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. @@ -2326,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]', @@ -2461,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 @@ -2482,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) { @@ -2710,6 +3332,8 @@ This is not an optimal method, but it wo =item * authform_filesystem +=item * authform_lti + =back See loncreateuser.pm for invocation and use examples. @@ -2838,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'); @@ -3032,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'}) { @@ -3102,7 +3726,7 @@ sub authform_filesystem { } else { $result = &mt('Currently Filesystem Authenticated.'); return $result; - } + } } } else { if ($authnum == 1) { @@ -3134,6 +3758,64 @@ sub authform_filesystem { return $result; } +sub authform_lti { + my %in = ( + formname => 'document.cu', + kerb_def_dom => 'MSU.EDU', + @_, + ); + my ($lticheck,$result,$authtype,$autharg,$jscall,$disabled); + my ($authnum,%can_assign) = &get_assignable_auth($in{'domain'}); + if ($in{'readonly'}) { + $disabled = ' disabled="disabled"'; + } + if (defined($in{'curr_authtype'})) { + if ($in{'curr_authtype'} eq 'lti') { + if ($can_assign{'lti'}) { + $lticheck = 'checked="checked" '; + if (defined($in{'mode'})) { + if ($in{'mode'} eq 'modifyuser') { + $lticheck = ''; + } + } + } else { + $result = &mt('Currently LTI Authenticated.'); + return $result; + } + } + } else { + if ($authnum == 1) { + $authtype = '<input type="hidden" name="login" value="lti" />'; + } + } + if (!$can_assign{'lti'}) { + return; + } elsif ($authtype eq '') { + if (defined($in{'mode'})) { + if ($in{'mode'} eq 'modifycourse') { + if ($authnum == 1) { + $authtype = '<input type="radio" name="login" value="lti"'.$disabled.' />'; + } + } + } + } + $jscall = "javascript:changed_radio('lti',$in{'formname'});"; + if (($authtype eq '') && (($in{'mode'} eq 'modifycourse') || ($in{'curr_authtype'} ne 'lti'))) { + $authtype = '<input type="radio" name="login" value="lti" '. + $lticheck.' onchange="'.$jscall.'" onclick="'. + $jscall.'"'.$disabled.' />'; + } + $autharg = '<input type="hidden" name="ltiarg" value="" />'; + if ($authtype) { + $result = &mt('[_1] LTI Authenticated', + '<label>'.$authtype.'</label>'.$autharg); + } else { + $result = '<b>'.&mt('LTI Authenticated').'</b>'. + $autharg; + } + return $result; +} + sub get_assignable_auth { my ($dom) = @_; if ($dom eq '') { @@ -3144,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') { @@ -3249,6 +3932,170 @@ sub check_passwd_rules { return $warning; } +sub passwd_validation_js { + my ($currpasswdval,$domain,$context,$id) = @_; + my (%passwdconf,$alertmsg); + if ($context eq 'linkprot') { + my %domconfig = &Apache::lonnet::get_dom('configuration',['ltisec'],$domain); + if (ref($domconfig{'ltisec'}) eq 'HASH') { + if (ref($domconfig{'ltisec'}{'rules'}) eq 'HASH') { + %passwdconf = %{$domconfig{'ltisec'}{'rules'}}; + } + } + if ($id eq 'add') { + $alertmsg = &mt('Secret for added launcher did not satisfy requirement(s):').'\n\n'; + } elsif ($id =~ /^\d+$/) { + my $pos = $id+1; + $alertmsg = &mt('Secret for launcher [_1] did not satisfy requirement(s):','#'.$pos).'\n\n'; + } else { + $alertmsg = &mt('A secret did not satisfy requirement(s):').'\n\n'; + } + } 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'; + } + my ($min,$max,@chars,$numrules,$intargjs,%alert); + $numrules = 0; + $min = $Apache::lonnet::passwdmin; + if (ref($passwdconf{'chars'}) eq 'ARRAY') { + if ($passwdconf{'min'} =~ /^\d+$/) { + if ($passwdconf{'min'} > $min) { + $min = $passwdconf{'min'}; + } + } + if ($passwdconf{'max'} =~ /^\d+$/) { + $max = $passwdconf{'max'}; + $numrules ++; + } + @chars = @{$passwdconf{'chars'}}; + if (@chars) { + $numrules ++; + } + } + if ($min > 0) { + $numrules ++; + } + if (($min > 0) || ($max ne '') || (@chars > 0)) { + if ($min) { + $alert{'min'} = &mt('minimum [quant,_1,character]',$min).'\n'; + } + if ($max) { + $alert{'max'} = &mt('maximum [quant,_1,character]',$max).'\n'; + } + my (@charalerts,@charrules); + if (@chars) { + if (grep(/^uc$/,@chars)) { + push(@charalerts,&mt('contain at least one upper case letter')); + push(@charrules,'uc'); + } + if (grep(/^lc$/,@chars)) { + push(@charalerts,&mt('contain at least one lower case letter')); + push(@charrules,'lc'); + } + if (grep(/^num$/,@chars)) { + push(@charalerts,&mt('contain at least one number')); + push(@charrules,'num'); + } + if (grep(/^spec$/,@chars)) { + push(@charalerts,&mt('contain at least one non-alphanumeric')); + push(@charrules,'spec'); + } + } + $intargjs = qq| var rulesmsg = '';\n|. + qq| var currpwval = $currpasswdval;\n|; + if ($min) { + $intargjs .= qq| + if (currpwval.length < $min) { + rulesmsg += ' - $alert{min}'; + } +|; + } + if ($max) { + $intargjs .= qq| + if (currpwval.length > $max) { + rulesmsg += ' - $alert{max}'; + } +|; + } + if (@chars > 0) { + my $charrulestr = '"'.join('","',@charrules).'"'; + my $charalertstr = '"'.join('","',@charalerts).'"'; + $intargjs .= qq| var brokerules = new Array();\n|. + qq| var charrules = new Array($charrulestr);\n|. + qq| var charalerts = new Array($charalertstr);\n|; + my %rules; + map { $rules{$_} = 1; } @chars; + if ($rules{'uc'}) { + $intargjs .= qq| + var ucRegExp = /[A-Z]/; + if (!ucRegExp.test(currpwval)) { + brokerules.push('uc'); + } +|; + } + if ($rules{'lc'}) { + $intargjs .= qq| + var lcRegExp = /[a-z]/; + if (!lcRegExp.test(currpwval)) { + brokerules.push('lc'); + } +|; + } + if ($rules{'num'}) { + $intargjs .= qq| + var numRegExp = /[0-9]/; + if (!numRegExp.test(currpwval)) { + brokerules.push('num'); + } +|; + } + if ($rules{'spec'}) { + $intargjs .= q| + var specRegExp = /[!"#$%&'()*+,\-.\/:;<=>?@[\\^\]_`{\|}~]/; + if (!specRegExp.test(currpwval)) { + brokerules.push('spec'); + } +|; + } + $intargjs .= qq| + if (brokerules.length > 0) { + for (var i=0; i<brokerules.length; i++) { + for (var j=0; j<charrules.length; j++) { + if (brokerules[i] == charrules[j]) { + rulesmsg += ' - '+charalerts[j]+'\\n'; + break; + } + } + } + } +|; + } + $intargjs .= qq| + if (rulesmsg != '') { + rulesmsg = '$alertmsg'+rulesmsg; + alert(rulesmsg); + return false; + } +|; + } + return ($numrules,$intargjs); +} + ############################################################### ## Get Kerberos Defaults for Domain ## ############################################################### @@ -3416,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 @@ -3910,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 @@ -4077,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 @@ -4164,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 '') { @@ -4225,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'}; } @@ -4242,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'}; } @@ -4273,7 +5184,7 @@ sub get_previous_attempt { if ($key =~/$regexp$/ && (defined &$gradesub)) { $value = &$gradesub($value); } - $prevattempts.='<td>'.$value.' </td>'; + $prevattempts.='<td>'. $value.' </td>'; } else { $prevattempts.='<td> </td>'; } @@ -4289,14 +5200,20 @@ sub get_previous_attempt { if ($key =~/$regexp$/ && (defined &$gradesub)) { $value = &$gradesub($value); } - $prevattempts.='<td>'.$value.' </td>'; + $prevattempts.='<td>'.$value.' </td>'; } } $prevattempts.= &end_data_table_row().&end_data_table(); } else { + my $msg; + if ($symb =~ /ext\.tool$/) { + $msg = &mt('No grade passed back.'); + } else { + $msg = &mt('Nothing submitted - no attempts.'); + } $prevattempts= &start_data_table().&start_data_table_row(). - '<td>'.&mt('Nothing submitted - no attempts.').'</td>'. + '<td>'.$msg.'</td>'. &end_data_table_row().&end_data_table(); } } else { @@ -4310,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]}; @@ -4337,7 +5256,7 @@ sub format_previous_attempt_value { } } } else { - $value = &unescape($value); + $value = &HTML::Entities::encode(&unescape($value), '"<>&'); } return $value; } @@ -4399,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; @@ -4752,8 +5674,7 @@ sub findallcourses { sub blockcheck { my ($setters,$activity,$clientip,$uname,$udom,$url,$is_course,$symb,$caller) = @_; - - unless ($activity eq 'docs') { + unless (($activity eq 'docs') || ($activity eq 'reinit') || ($activity eq 'alert')) { my ($has_evb,$check_ipaccess); my $dom = $env{'user.domain'}; if ($env{'request.course.id'}) { @@ -4770,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; } @@ -4860,7 +5782,9 @@ sub blockcheck { # boards, chat or groups, check for blocking in current course only. if (($activity eq 'boards' || $activity eq 'chat' || - $activity eq 'groups' || $activity eq 'printout') && + $activity eq 'groups' || $activity eq 'printout' || + $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'}) { @@ -4968,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) && @@ -5158,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 .= "&udom=$udom" if ($udom =~ /^$match_domain$/); + $querystring .= "&udom=$udom" if ($udom =~ /^$match_domain$/); $querystring .= "&uname=$uname" if ($uname =~ /^$match_username$/); } elsif ($activity eq 'docs') { my $showurl = &Apache::lonenc::check_encrypt($url); @@ -5195,6 +6119,12 @@ 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') { + $text = &mt('Checking Course Update Blocked'); } elsif ($activity eq 'about') { $text = &mt('Access to User Information Pages Blocked'); } elsif ($activity eq 'wishlist') { @@ -5224,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; @@ -5235,13 +6164,35 @@ sub check_ip_acc { } my $name; - foreach my $pattern (split(',',$acc)) { - $pattern =~ s/^\s*//; - $pattern =~ s/\s*$//; + my %access = ( + allowfrom => 1, + denyfrom => 0, + ); + my @allows; + my @denies; + 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 $acctype = 'allowfrom'; + if ($count <= $numdenies) { + $acctype = 'denyfrom'; + } if ($pattern =~ /\*$/) { #35.8.* $pattern=~s/\*//; - if ($ip =~ /^\Q$pattern\E/) { $allowed=1; } + if ($ip =~ /^\Q$pattern\E/) { $allowed=$access{$acctype}; } } elsif ($pattern =~ /(\d+\.\d+\.\d+)\.\[(\d+)-(\d+)\]$/) { #35.8.3.[34-56] my $low=$2; @@ -5249,7 +6200,7 @@ sub check_ip_acc { $pattern=$1; if ($ip =~ /^\Q$pattern\E/) { my $last=(split(/\./,$ip))[3]; - if ($last <=$high && $last >=$low) { $allowed=1; } + if ($last <=$high && $last >=$low) { $allowed=$access{$acctype}; } } } elsif ($pattern =~ /^\*/) { #*.msu.edu @@ -5259,10 +6210,10 @@ sub check_ip_acc { my $netaddr=inet_aton($ip); ($name)=gethostbyaddr($netaddr,AF_INET); } - if ($name =~ /\Q$pattern\E$/i) { $allowed=1; } + if ($name =~ /\Q$pattern\E$/i) { $allowed=$access{$acctype}; } } elsif ($pattern =~ /\d+\.\d+\.\d+\.\d+/) { #127.0.0.1 - if ($ip =~ /^\Q$pattern\E/) { $allowed=1; } + if ($ip =~ /^\Q$pattern\E/) { $allowed=$access{$acctype}; } } else { #some.name.com if (!defined($name)) { @@ -5270,9 +6221,16 @@ sub check_ip_acc { my $netaddr=inet_aton($ip); ($name)=gethostbyaddr($netaddr,AF_INET); } - if ($name =~ /\Q$pattern\E$/i) { $allowed=1; } + if ($name =~ /\Q$pattern\E$/i) { $allowed=$access{$acctype}; } + } + if ($allowed =~ /^(0|1)$/) { last; } + } + if ($allowed eq '') { + if ($numdenies && !$numallows) { + $allowed = 1; + } else { + $allowed = 0; } - if ($allowed) { last; } } return $allowed; } @@ -5337,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'}; @@ -5358,7 +6317,7 @@ sub get_domainconf { foreach my $host (keys(%{$domconfig{'login'}{$key}})) { if (ref($domconfig{'login'}{$key}{$host}) eq 'HASH') { $designhash{$udom.'.login.'.$key.'_'.$host} = 1; - foreach my $item ('text','img','alt','url','title','notsso') { + foreach my $item ('text','img','alt','url','title','window','notsso') { $designhash{$udom.'.login.'.$key.'_'.$item.'_'.$host} = $domconfig{'login'}{$key}{$host}{$item}; } } @@ -5591,6 +6550,12 @@ sub head_subbox { Input: (optional) filename from which breadcrumb trail is built. In most cases no input as needed, as $env{'request.filename'} is appropriate for use in building the breadcrumb trail. + frameset flag + If page header is being requested for use in a frameset, then + the second (option) argument -- frameset will be true, and + the target attribute set for links should be target="_parent". + 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 To be included on Authoring Space pages @@ -5598,7 +6563,7 @@ Returns: HTML div with CSTR path and rec =cut sub CSTR_pageheader { - my ($trailfile) = @_; + my ($trailfile,$frameset,$title) = @_; if ($trailfile eq '') { $trailfile = $env{'request.filename'}; } @@ -5621,13 +6586,36 @@ sub CSTR_pageheader { $lastitem = $thisdisfn; } + 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'); + } + + my ($target,$crumbtarget) = (' target="_top"','_top'); + if ($frameset) { + $target = ' target="_parent"'; + $crumbtarget = '_parent'; + } elsif (($env{'request.lti.login'}) && ($env{'request.lti.target'} eq 'iframe')) { + $target = ''; + $crumbtarget = ''; + } elsif (($env{'request.deeplink.login'}) && ($env{'request.deeplink.target'})) { + $target = ' target="'.$env{'request.deeplink.target'}.'"'; + $crumbtarget = $env{'request.deeplink.target'}; + } + my $output = '<div>' .&Apache::loncommon::help_open_menu('','',3,'Authoring') #FIXME: Broken? Where is it? - .'<b>'.&mt('Authoring Space:').'</b> ' - .'<form name="dirs" method="post" action="'.$formaction - .'" target="_top">' #FIXME lonpubdir: target="_parent" - .&Apache::lonhtmlcommon::crumbs($uname.'/'.$parentpath,'_top','/priv/'.$udom,undef,undef); + .'<b>'.$title.'</b> ' + .'<form name="dirs" method="post" action="'.$formaction.'"'.$target.'>' + .&Apache::lonhtmlcommon::crumbs($uname.'/'.$parentpath,$crumbtarget,'/priv/'.$udom,undef,undef); if ($lastitem) { $output .= @@ -5635,17 +6623,163 @@ sub CSTR_pageheader { .$lastitem .'</span>'; } - $output .= - '<br />' - #FIXME lonpubdir: &Apache::lonhtmlcommon::crumbs($uname.$thisdisfn.'/','_top','/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() - .'</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; } +############################################## +=pod + +=item * &nocodemirror() + +Input: None + +Returns: 1 if CodeMirror is deactivated based on + user's preference, or domain default, + if user indicated use of default. + +=cut + +sub nocodemirror { + my $nocodem = $env{'environment.nocodemirror'}; + unless ($nocodem) { + my %domdefs = &Apache::lonnet::get_domain_defaults($env{'user.domain'}); + if ($domdefs{'nocodemirror'}) { + $nocodem = 'yes'; + } + } + if ($nocodem eq 'yes') { + return 1; + } + return; +} + +############################################## +=pod + +=item * &permitted_editors() + +Input: $uri (optional) + +Returns: %editors hash in which keys are editors + permitted in current Authoring Space, + or in current course for web pages + created in a course. + + Value for each key is 1. Possible keys + are: edit, xml, and daxe. + + For a regular Authoring Space, if no specific + set of editors has been set for the Author + who owns the Authoring Space, then the + domain default will be used. If no domain + default has been set, then the keys will be + edit and xml. + + For a course author, or for web pages created + in a course, if no specific set of editors has + been set for the course, then the domain + course default will be used. If no domain + course default has been set, then the keys + will be edit and xml. + +=cut + +sub permitted_editors { + my ($uri) = @_; + my ($is_author,$is_coauthor,$is_course,$auname,$audom,%editors); + if ($env{'request.role'} =~ m{^au\./}) { + $is_author = 1; + } elsif ($env{'request.role'} =~ m{^(?:ca|aa)\./($match_domain)/($match_username)}) { + ($audom,$auname) = ($1,$2); + if (($audom ne '') && ($auname ne '')) { + if (($env{'user.domain'} eq $audom) && + ($env{'user.name'} eq $auname)) { + $is_author = 1; + } else { + $is_coauthor = 1; + } + } + } elsif ($env{'request.course.id'}) { + my ($cdom,$cnum); + $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; + $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; + if (($env{'request.editurl'} =~ m{^/priv/\Q$cdom/$cnum\E/}) || + ($env{'request.editurl'} =~ m{^/uploaded/\Q$cdom/$cnum\E/(docs|supplemental)/}) || + ($uri =~ m{^/uploaded/\Q$cdom/$cnum\E/(docs|supplemental)/})) { + $is_course = 1; + } elsif ($env{'request.editurl'} =~ m{^/priv/($match_domain)/($match_username)/}) { + ($audom,$auname) = ($1,$2); + } elsif ($env{'request.uri'} =~ m{^/priv/($match_domain)/($match_username)/}) { + ($audom,$auname) = ($1,$2); + } elsif (($uri eq '/daxesave') && + (($env{'form.path'} =~ m{^/daxeopen/priv/\Q$cdom/$cnum\E/}) || + ($env{'form.path'} =~ m{^/daxeopen/uploaded/\Q$cdom/$cnum\E/(docs|supplemental)/}))) { + $is_course = 1; + } elsif (($uri eq '/daxesave') && + ($env{'form.path'} =~ m{^/daxeopen/priv/($match_domain)/($match_username)/})) { + ($audom,$auname) = ($1,$2); + } + unless ($is_course) { + if (($audom ne '') && ($auname ne '')) { + if (($env{'user.domain'} eq $audom) && + ($env{'user.name'} eq $auname)) { + $is_author = 1; + } else { + $is_coauthor = 1; + } + } + } + } + if ($is_author) { + if (exists($env{'environment.editors'})) { + map { $editors{$_} = 1; } split(/,/,$env{'environment.editors'}); + } else { + %editors = ( edit => 1, + xml => 1, + ); + } + } elsif ($is_coauthor) { + if (exists($env{"environment.internal.editors./$audom/$auname"})) { + map { $editors{$_} = 1; } split(/,/,$env{"environment.internal.editors./$audom/$auname"}); + } else { + %editors = ( edit => 1, + xml => 1, + ); + } + } elsif ($is_course) { + if (exists($env{'course.'.$env{'request.course.id'}.'.internal.crseditors'})) { + map { $editors{$_} = 1; } split(/,/,$env{'course.'.$env{'request.course.id'}.'.internal.crseditors'}); + } else { + my %domdefaults = &Apache::lonnet::get_domain_defaults($env{'course.'.$env{'request.course.id'}.'.domain'}); + if (exists($domdefaults{'crseditors'})) { + map { $editors{$_} = 1; } split(/,/,$domdefaults{'crseditors'}); + } else { + %editors = ( edit => 1, + xml => 1, + ); + } + } + } else { + %editors = ( edit => 1, + xml => 1, + ); + } + return %editors; +} + ############################################### ############################################### @@ -5683,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 @@ -5697,6 +6828,34 @@ Inputs: inlineremote items to be added in "Functions" menu below breadcrumbs. +=item * $ltiscope, optional argument, will be one of: resource, map or + course, if LON-CAPA is in LTI Provider context. Value is + the scope of use, i.e., launch was for access to a single, a map + or the entire course. + +=item * $ltiuri, optional argument, if LON-CAPA is in LTI Provider + context, this will contain the URL for the landing item in + the course, after launch from an LTI Consumer + +=item * $ltimenu, optional argument, if LON-CAPA is in LTI Provider + context, this will contain a reference to hash of items + to be included in the page header and/or inline menu. + +=item * $menucoll, optional argument, if specific menu collection is in + effect, either set as the default for the course, or set for + the deeplink paramater for $env{'request.deeplink.login'} + then $menucoll will be the number of that collection. + +=item * $menuref, optional argument, reference to a hash, containing the + menu options included for the menu in effect, based on the + configuration for the numbered menu collection in use. + +=item * $showncrumbsref, reference to a scalar. Calls to lonmenu::innerregister + within &bodytag() can result in calls to lonhtmlcommon::breadcrumbs(), + if so, $showncrumbsref is set there to 1, and will propagate back + via &bodytag() to &start_page(), to prevent lonhtmlcommon::breadcrumbs() + being called a second time. + =back Returns: A uniform header for LON-CAPA web pages. @@ -5708,7 +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)=@_; + $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')) @@ -5720,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); @@ -5764,7 +6923,7 @@ sub bodytag { $role = &mt('Helpdesk[_1]',' '.$2); } } else { - $role = (split(/\//,$role,4))[-1]; + $role = (split(/\//,$role,4))[-1]; } if ($sec) { $role .= (' 'x2).'- '.&mt('section:').' '.$sec; @@ -5792,38 +6951,52 @@ sub bodytag { undef($role); } + my $showcrstitle = 1; + if (($cid) && ($env{'request.lti.login'})) { + if (ref($ltimenu) eq 'HASH') { + unless ($ltimenu->{'role'}) { + undef($role); + } + unless ($ltimenu->{'coursetitle'}) { + $realm=' '; + $showcrstitle = 0; + } + } + } elsif (($cid) && ($menucoll)) { + if (ref($menuref) eq 'HASH') { + unless ($menuref->{'role'}) { + undef($role); + } + unless ($menuref->{'crs'}) { + $realm=' '; + $showcrstitle = 0; + } + } + } + my $titleinfo = '<h1>'.$title.'</h1>'; # # Extra info if you are the DC my $dc_info = ''; - if (($env{'user.adv'}) && ($env{'request.course.id'}) && + if (($env{'user.adv'}) && ($env{'request.course.id'}) && $showcrstitle && (exists($env{'user.role.dc./'.$env{'course.'.$cid.'.domain'}.'/'}))) { $dc_info = $cid.' '.$env{'course.'.$cid.'.internal.coursecode'}; $dc_info =~ s/\s+$//; } - $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')) { - unless ($args->{'switchserver'}) { - $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; - } - $bodytag .= Apache::lonhtmlcommon::scripttag('', 'end'); - } + my $crstype; + if ($cid) { + $crstype = $env{'course.'.$cid.'.type'}; + } elsif ($args->{'crstype'}) { + $crstype = $args->{'crstype'}; + } + 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 @@ -5836,12 +7009,33 @@ sub bodytag { $need_endlcint = 1; } - if (($env{'environment.remote'} eq 'on') && ($env{'request.state'} eq 'construct')) { - unless ($env{'form.inhibitmenu'}) { - $bodytag .= &inline_for_remote($public,$role,$realm,$dc_info,$no_inline_link); - } - } else { - my ($left,$right) = Apache::lonmenu::primary_menu($args->{'links_disabled'}); + my $collapsible; + if ($args->{'collapsible_header'} ne '') { + $collapsible = 1; + my ($menustate,$tiptext,$divclass); + if ($args->{'start_collapsed'}) { + $menustate = 'collapsed'; + $tiptext = 'display'; + $divclass = 'hidden'; + } else { + $menustate = 'expanded'; + $tiptext = 'hide'; + $divclass = 'shown'; + } + my $alttext = &mt('menu state: '.$menustate); + my $tooltip = &mt($tiptext.' standard menus'); + $bodytag .= <<"END"; +<div id="LC_expandingContainer" style="display:inline;"> +<div id="LC_collapsible" class="LC_collapse_trigger" style="position: absolute;top: -5px;left: 0px; z-index:101; display:inline;"> +<a href="#" style="text-decoration:none;"><img class="LC_collapsible_indicator" alt="$alttext" title="$tooltip" src="/res/adm/pages/$menustate.png" style="border:0;margin:0;padding:0;max-width:100%;height:auto" /></a></div> +<div class="LC_menus_content $divclass"> +END + } + unless ($args->{'no_primary_menu'}) { + my ($left,$right) = Apache::lonmenu::primary_menu($crstype,$ltimenu,$menucoll,$menuref, + $args->{'links_disabled'}, + $args->{'links_target'}, + $collapsible); if ($env{'request.noversionuri'} =~ m{^/res/adm/pages/}) { if ($dc_info) { @@ -5867,7 +7061,7 @@ sub bodytag { $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'); @@ -5876,9 +7070,12 @@ sub bodytag { } #don't show menus for public users if (!$public){ - unless (($env{'environment.remote'} eq 'on') && - ($env{'request.state'} eq 'construct')) { - $bodytag .= Apache::lonmenu::secondary_menu($httphost,$args->{'links_disabled'}); + unless ($args->{'no_inline_menu'}) { + $bodytag .= Apache::lonmenu::secondary_menu($httphost,$ltiscope,$ltimenu, + $args->{'no_primary_menu'}, + $menucoll,$menuref, + $args->{'links_disabled'}, + $args->{'links_target'}); } $bodytag .= Apache::lonmenu::serverform(); if ($need_endlcint) { @@ -5886,95 +7083,33 @@ sub bodytag { } if ($env{'request.state'} eq 'construct') { $bodytag .= &Apache::lonmenu::innerregister($forcereg, - $args->{'bread_crumbs'},'','',$hostname); + $args->{'bread_crumbs'},'','',$hostname, + $ltiscope,$ltiuri,$showncrumbsref); } elsif ($forcereg) { $bodytag .= &Apache::lonmenu::innerregister($forcereg,undef, - $args->{'group'}, - $args->{'hide_buttons'}, - $hostname); + $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 separate menu from content when there's no secondary - # menu. Especially needed for public accessible ressources. + # menu. Especially needed for publicly accessible resources. $bodytag .= '<hr style="clear:both" />'; if ($need_endlcint) { $bodytag .= Apache::lonhtmlcommon::scripttag('', 'end'); } } - - return $bodytag; - } - -# -# Top frame rendering, Remote is up -# - - my $linkattr; - if ($args->{'links_disabled'}) { - $linkattr = 'class="LCisDisabled" aria-disabled="true"'; - } - - my $help=($no_inline_link?'' - :&top_nav_help('Help',$linkattr)); - - # Explicit link to get inline menu - my $menu= ($no_inline_link?'' - :'<a href="/adm/remote?action=collapse" $linkattr 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) { - my $class = 'LC_menubuttons_link'; - if ($args->{'links_disabled'}) { - $class .= ' LCisDisabled'; + if ($args->{'collapsible_header'} ne '') { + $bodytag .= $args->{'collapsible_header'}. + '<div id="LC_collapsible_separator"></div>'. + '</div></div>'; } - $name = &aboutmewrapper($name,$env{'user.name'},$env{'user.domain'}, - undef,$class); - } - - unless ($env{'form.inhibitmenu'}) { - $bodytag .= &inline_for_remote($public,$role,$realm,$dc_info,$no_inline_link); - } - return $bodytag."\n".$funclist; -} - -sub inline_for_remote { - my ($public,$role,$realm,$dc_info,$no_inline_link) = @_; - 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'); - } - - return 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>|; + return $bodytag; } sub dc_courseid_toggle { @@ -6006,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; @@ -6050,8 +7178,38 @@ sub endbodytag { } if ( exists( $env{'internal.head.redirect'} ) ) { if (!(ref($args) eq 'HASH' && $args->{'noredirectlink'})) { + my ($endbodyjs,$idattr); + if ($env{'internal.head.to_opener'}) { + my $linkid = 'LC_continue_link'; + $idattr = ' id="'.$linkid.'"'; + my $redirect_for_js = &js_escape($env{'internal.head.redirect'}); + $endbodyjs=<<ENDJS; +<script type="text/javascript"> +// <![CDATA[ +function ebFunction(evt) { + evt.preventDefault(); + var dest = '$redirect_for_js'; + if (window.opener != null && !window.opener.closed) { + window.opener.location.href=dest; + window.close(); + } else { + window.location.href=dest; + } + return false; +} + +\$(document).ready(function () { + if (document.getElementById('$linkid')) { + var clickelem = document.getElementById('$linkid'); + clickelem.addEventListener('click',ebFunction,false); + } +}); +// ]]> +</script> +ENDJS + } $endbodytag= - "<br /><a href=\"$env{'internal.head.redirect'}\">". + "$endbodyjs<br /><a href=\"$env{'internal.head.redirect'}\"$idattr>". &mt('Continue').'</a>'. $endbodytag; } @@ -6079,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); @@ -6132,6 +7289,7 @@ body { line-height:130%; font-size:0.83em; color:$font; + background-color: $pgbg_or_bgcolor; } a:focus, @@ -6143,6 +7301,24 @@ 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; +} + +.LC_menus_content.hidden { + display: none; +} + .LC_right { text-align:right; } @@ -6163,6 +7339,12 @@ form, .inline { width:400px; } +#LC_collapsible_separator { + border: 1px solid black; + width: 99.9%; + height: 0px; +} + .LC_iframecontainer { width: 98%; margin: 0; @@ -6341,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; } @@ -6432,7 +7620,7 @@ td.LC_menubuttons_text { } td.LC_zero_height { - line-height: 0; + line-height: 0; cellpadding: 0; } @@ -6757,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; @@ -7109,7 +8303,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; @@ -7358,6 +8557,11 @@ fieldset { /* overflow: hidden; */ } +fieldset#LC_selectuser { + margin: 0; + padding: 0; +} + article.geogebraweb div { margin: 0; } @@ -7406,7 +8610,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; @@ -7421,7 +8625,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; @@ -7456,9 +8660,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 { @@ -7901,6 +9105,10 @@ a#LC_content_toolbar_edittoplevel { background-image:url(/res/adm/pages/edittoplevel.gif); } +a#LC_content_toolbar_printout { + background-image:url(/res/adm/pages/printout.gif); +} + ul#LC_toolbar li a:hover { background-position: bottom center; } @@ -8039,6 +9247,16 @@ pre.LC_wordwrap { } /* + styles used for response display +*/ +div.LC_radiofoil, div.LC_rankfoil { + margin: .5em 0em .5em 0em; +} +table.LC_itemgroup { + margin-top: 1em; +} + +/* styles used by TTH when "Default set of options to pass to tth/m when converting TeX" in course settings has been set @@ -8059,6 +9277,87 @@ span.roman {font-family: serif; font-sty span.overacc2 {position: relative; left: .8em; top: -1.2ex;} span.overacc1 {position: relative; left: .6em; top: -1.2ex;} +/* + sections with roles, for content only +*/ +section[class^="role-"] { + padding-left: 10px; + padding-right: 5px; + margin-top: 8px; + margin-bottom: 8px; + border: 1px solid #2A4; + border-radius: 5px; + box-shadow: 0px 1px 1px #BBB; +} +section[class^="role-"]>h1 { + position: relative; + margin: 0px; + padding-top: 10px; + padding-left: 40px; +} +section[class^="role-"]>h1:before { + position: absolute; + left: -5px; + top: 5px; +} +section.role-activity>h1:before { + content:url('/adm/daxe/images/section_icons/activity.png'); +} +section.role-advice>h1:before { + content:url('/adm/daxe/images/section_icons/advice.png'); +} +section.role-bibliography>h1:before { + content:url('/adm/daxe/images/section_icons/bibliography.png'); +} +section.role-citation>h1:before { + content:url('/adm/daxe/images/section_icons/citation.png'); +} +section.role-conclusion>h1:before { + content:url('/adm/daxe/images/section_icons/conclusion.png'); +} +section.role-definition>h1:before { + content:url('/adm/daxe/images/section_icons/definition.png'); +} +section.role-demonstration>h1:before { + content:url('/adm/daxe/images/section_icons/demonstration.png'); +} +section.role-example>h1:before { + content:url('/adm/daxe/images/section_icons/example.png'); +} +section.role-explanation>h1:before { + content:url('/adm/daxe/images/section_icons/explanation.png'); +} +section.role-introduction>h1:before { + content:url('/adm/daxe/images/section_icons/introduction.png'); +} +section.role-method>h1:before { + content:url('/adm/daxe/images/section_icons/method.png'); +} +section.role-more_information>h1:before { + content:url('/adm/daxe/images/section_icons/more_information.png'); +} +section.role-objectives>h1:before { + content:url('/adm/daxe/images/section_icons/objectives.png'); +} +section.role-prerequisites>h1:before { + content:url('/adm/daxe/images/section_icons/prerequisites.png'); +} +section.role-remark>h1:before { + content:url('/adm/daxe/images/section_icons/remark.png'); +} +section.role-reminder>h1:before { + content:url('/adm/daxe/images/section_icons/reminder.png'); +} +section.role-summary>h1:before { + content:url('/adm/daxe/images/section_icons/summary.png'); +} +section.role-syntax>h1:before { + content:url('/adm/daxe/images/section_icons/syntax.png'); +} +section.role-warning>h1:before { + content:url('/adm/daxe/images/section_icons/warning.png'); +} + #LC_minitab_header { float:left; width:100%; @@ -8112,8 +9411,13 @@ Inputs: $title - optional title for the 3- whether the side effect should occur (side effect of setting $env{'internal.head.redirect'} to the url - redirected too) - 4- whether encrypt check should be skipped + redirected to) + 4- whether the redirect target should be + the opener of the current (pop-up) + window (side effect of setting + $env{'internal.head.to_opener'} to + 1, if true. + 5- whether encrypt check should be skipped domain -> force to color decorate a page for a specific domain function -> force usage of a specific rolish color scheme @@ -8150,8 +9454,8 @@ sub headtag { 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'} @@ -8177,17 +9481,45 @@ sub headtag { } } if (ref($args->{'redirect'})) { - my ($time,$url,$inhibit_continue,$skip_enc_check) = @{$args->{'redirect'}}; + my ($time,$url,$inhibit_continue,$to_opener,$skip_enc_check) = @{$args->{'redirect'}}; if (!$skip_enc_check) { $url = &Apache::lonenc::check_encrypt($url); } if (!$inhibit_continue) { $env{'internal.head.redirect'} = $url; } - $result.=<<ADDMETA + $result.=<<"ADDMETA"; <meta http-equiv="pragma" content="no-cache" /> +ADDMETA + if ($to_opener) { + $env{'internal.head.to_opener'} = 1; + my $dest = &js_escape($url); + my $timeout = int($time * 1000); + $result .=<<"ENDJS"; +<script type="text/javascript"> +// <![CDATA[ +function LC_To_Opener() { + var dest = '$dest'; + if (dest != '') { + if (window.opener != null && !window.opener.closed) { + window.opener.location.href=dest; + window.close(); + } else { + window.location.href=dest; + } + } +} +\$(document).ready(function () { + setTimeout('LC_To_Opener()',$timeout); +}); +// ]]> +</script> +ENDJS + } else { + $result.=<<"ADDMETA"; <meta http-equiv="Refresh" content="$time; url=$url" /> ADDMETA + } } else { unless (($args->{'frameset'}) || ($args->{'js_ready'}) || ($args->{'only_body'}) || ($args->{'no_nav_bar'})) { my $requrl = $env{'request.uri'}; @@ -8333,7 +9665,7 @@ OFFLOAD if (!$args->{'frameset'}) { $result .= ' /'; } - $result .= '>' + $result .= '>' .$inhibitprint .$head_extra; my $clientmobile; @@ -8344,7 +9676,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"; @@ -8366,12 +9698,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; } @@ -8525,8 +9857,6 @@ $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 @@ -8537,7 +9867,7 @@ $args - additional optional args support 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 @@ -8546,6 +9876,7 @@ $args - additional optional args support links_disabled -> Links in primary and secondary menus are disabled (Can enable them once page has loaded - see lonroles.pm for an example). + links_target -> Target for links, e.g., _parent (optional). =back @@ -8558,12 +9889,83 @@ sub start_page { #&Apache::lonnet::logthis("start_page ".join(':',caller(0))); $env{'internal.start_page'}++; - my ($result,@advtools); + my ($result,@advtools,$ltiscope,$ltiuri,%ltimenu,$menucoll,%menu); if (! exists($args->{'skip_phases'}{'head'}) ) { $result .= &xml_begin($args->{'frameset'}) . &headtag($title, $head_extra, $args); } - + + if (($env{'request.course.id'}) && ($env{'request.lti.login'})) { + if ($env{'course.'.$env{'request.course.id'}.'.lti.override'}) { + unless ($env{'course.'.$env{'request.course.id'}.'.lti.topmenu'}) { + $args->{'no_primary_menu'} = 1; + } + unless ($env{'course.'.$env{'request.course.id'}.'.lti.inlinemenu'}) { + $args->{'no_inline_menu'} = 1; + } + if ($env{'course.'.$env{'request.course.id'}.'.lti.lcmenu'}) { + map { $ltimenu{$_} = 1; } split(/,/,$env{'course.'.$env{'request.course.id'}.'.lti.lcmenu'}); + } + } else { + my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; + my %lti = &Apache::lonnet::get_domain_lti($cdom,'provider'); + if (ref($lti{$env{'request.lti.login'}}) eq 'HASH') { + unless ($lti{$env{'request.lti.login'}}{'topmenu'}) { + $args->{'no_primary_menu'} = 1; + } + unless ($lti{$env{'request.lti.login'}}{'inlinemenu'}) { + $args->{'no_inline_menu'} = 1; + } + if (ref($lti{$env{'request.lti.login'}}{'lcmenu'}) eq 'ARRAY') { + map { $ltimenu{$_} = 1; } @{$lti{$env{'request.lti.login'}}{'lcmenu'}}; + } + } + } + ($ltiscope,$ltiuri) = &LONCAPA::ltiutils::lti_provider_scope($env{'request.lti.uri'}, + $env{'course.'.$env{'request.course.id'}.'.domain'}, + $env{'course.'.$env{'request.course.id'}.'.num'}); + } elsif ($env{'request.course.id'}) { + my $expiretime=600; + if ((time-$env{'course.'.$env{'request.course.id'}.'.last_cache'}) > $expiretime) { + &Apache::lonnet::coursedescription($env{'request.course.id'},{'freshen_cache' => 1}); + } + my ($deeplinkmenu,$menuref); + ($menucoll,$deeplinkmenu,$menuref) = &menucoll_in_effect(); + if ($menucoll) { + if (ref($menuref) eq 'HASH') { + %menu = %{$menuref}; + } + if ($menu{'top'} eq 'n') { + $args->{'no_primary_menu'} = 1; + } + if ($menu{'inline'} eq 'n') { + unless (&Apache::lonnet::allowed('opa')) { + my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; + my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; + my $crstype = &course_type(); + my $now = time; + my $ccrole; + if ($crstype eq 'Community') { + $ccrole = 'co'; + } else { + $ccrole = 'cc'; + } + if ($env{'user.role.'.$ccrole.'./'.$cdom.'/'.$cnum}) { + my ($start,$end) = split(/\./,$env{'user.role.'.$ccrole.'./'.$cdom.'/'.$cnum}); + if ((($start) && ($start<0)) || + (($end) && ($end<$now)) || + (($start) && ($now<$start))) { + $args->{'no_inline_menu'} = 1; + } + } else { + $args->{'no_inline_menu'} = 1; + } + } + } + } + } + + my $showncrumbs; if (! exists($args->{'skip_phases'}{'body'}) ) { if ($args->{'frameset'}) { my $attr_string = &make_attr_string($args->{'force_register'}, @@ -8575,8 +9977,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); + $args->{'bgcolor'}, $args, + \@advtools,$ltiscope,$ltiuri,\%ltimenu,$menucoll, + \%menu,\$showncrumbs); } } @@ -8598,6 +10001,7 @@ sub start_page { #Breadcrumbs if (exists($args->{'bread_crumbs'}) or exists($args->{'bread_crumbs_component'})) { + unless ($showncrumbs) { &Apache::lonhtmlcommon::clear_breadcrumbs(); #if any br links exists, add them to the breadcrumbs if (exists($args->{'bread_crumbs'}) and ref($args->{'bread_crumbs'}) eq 'ARRAY') { @@ -8611,25 +10015,32 @@ sub start_page { } my $menulink; # if arg: bread_crumbs_nomenu is true pass 0 as $menulink item. - if (exists($args->{'bread_crumbs_nomenu'})) { + if ((exists($args->{'bread_crumbs_nomenu'})) || + ($ltiscope eq 'map') || ($ltiscope eq 'resource') || + ((($args->{'crstype'} eq 'Placement') || (($env{'request.course.id'}) && + ($env{'course.'.$env{'request.course.id'}.'.type'} eq 'Placement'))) && + (!$env{'request.role.adv'}))) { $menulink = 0; } else { undef($menulink); } + my $linkprotout; + if ($env{'request.deeplink.login'}) { + my $linkprotout = &Apache::lonmenu::linkprot_exit(); + if ($linkprotout) { + &Apache::lonhtmlcommon::add_breadcrumb_tool('tools',$linkprotout); + } + } #if bread_crumbs_component exists show it as headline else show only the breadcrumbs if(exists($args->{'bread_crumbs_component'})){ $result .= &Apache::lonhtmlcommon::breadcrumbs($args->{'bread_crumbs_component'}, '',$menulink,'', $args->{'bread_crumbs_style'}); - }else{ + } 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; } @@ -8666,6 +10077,147 @@ sub end_page { return $result; } +sub menucoll_in_effect { + my ($menucoll,$deeplinkmenu,%menu); + if ($env{'request.course.id'}) { + $menucoll = $env{'course.'.$env{'request.course.id'}.'.menudefault'}; + if ($env{'request.deeplink.login'}) { + my ($deeplink_symb,$deeplink,$check_login_symb); + my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; + my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; + if ($env{'request.noversionuri'} =~ m{^/(res|uploaded)/}) { + if ($env{'request.noversionuri'} =~ /\.(page|sequence)$/) { + my $navmap = Apache::lonnavmaps::navmap->new(); + if (ref($navmap)) { + $deeplink = $navmap->get_mapparam(undef, + &Apache::lonnet::declutter($env{'request.noversionuri'}), + '0.deeplink'); + } else { + $check_login_symb = 1; + } + } else { + my $symb = &Apache::lonnet::symbread(); + if ($symb) { + $deeplink = &Apache::lonnet::EXT('resource.0.deeplink',$symb); + } else { + $check_login_symb = 1; + } + } + } else { + $check_login_symb = 1; + } + if ($check_login_symb) { + $deeplink_symb = &deeplink_login_symb($cnum,$cdom); + if ($deeplink_symb =~ /\.(page|sequence)$/) { + my $mapname = &Apache::lonnet::deversion((&Apache::lonnet::decode_symb($deeplink_symb))[2]); + my $navmap = Apache::lonnavmaps::navmap->new(); + if (ref($navmap)) { + $deeplink = $navmap->get_mapparam(undef,$mapname,'0.deeplink'); + } + } else { + $deeplink = &Apache::lonnet::EXT('resource.0.deeplink',$deeplink_symb); + } + } + if ($deeplink ne '') { + my ($state,$others,$listed,$scope,$protect,$display,$target) = split(/,/,$deeplink); + if ($display =~ /^\d+$/) { + $deeplinkmenu = 1; + $menucoll = $display; + } + } + } + if ($menucoll) { + %menu = &page_menu($env{'course.'.$env{'request.course.id'}.'.menucollections'},$menucoll); + } + } + return ($menucoll,$deeplinkmenu,\%menu); +} + +sub deeplink_login_symb { + my ($cnum,$cdom) = @_; + my $login_symb; + if ($env{'request.deeplink.login'}) { + $login_symb = &symb_from_tinyurl($env{'request.deeplink.login'},$cnum,$cdom); + } + return $login_symb; +} + +sub symb_from_tinyurl { + my ($url,$cnum,$cdom) = @_; + if ($url =~ m{^\Q/tiny/$cdom/\E(\w+)$}) { + my $key = $1; + my ($tinyurl,$login); + my ($result,$cached)=&Apache::lonnet::is_cached_new('tiny',$cdom."\0".$key); + if (defined($cached)) { + $tinyurl = $result; + } else { + my $configuname = &Apache::lonnet::get_domainconfiguser($cdom); + my %currtiny = &Apache::lonnet::get('tiny',[$key],$cdom,$configuname); + if ($currtiny{$key} ne '') { + $tinyurl = $currtiny{$key}; + &Apache::lonnet::do_cache_new('tiny',$cdom."\0".$key,$currtiny{$key},600); + } + } + if ($tinyurl ne '') { + my ($cnumreq,$symb) = split(/\&/,$tinyurl); + if (wantarray) { + return ($cnumreq,$symb); + } elsif ($cnumreq eq $cnum) { + return $symb; + } + } + } + if (wantarray) { + return (); + } else { + return; + } +} + +sub usable_exttools { + my %tooltypes; + if ($env{'request.course.id'}) { + if ($env{'course.'.$env{'request.course.id'}.'.internal.exttool'}) { + if ($env{'course.'.$env{'request.course.id'}.'.internal.exttool'} eq 'both') { + %tooltypes = ( + crs => 1, + dom => 1, + ); + } elsif ($env{'course.'.$env{'request.course.id'}.'.internal.exttool'} eq 'crs') { + $tooltypes{'crs'} = 1; + } elsif ($env{'course.'.$env{'request.course.id'}.'.internal.exttool'} eq 'dom') { + $tooltypes{'dom'} = 1; + } + } else { + my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; + my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; + my $crstype = lc($env{'course.'.$env{'request.course.id'}.'.type'}); + if ($crstype eq '') { + $crstype = 'course'; + } + if ($crstype eq 'course') { + if ($env{'course.'.$env{'request.course.id'}.'internal.coursecode'}) { + $crstype = 'official'; + } elsif ($env{'course.'.$env{'request.course.id'}.'.internal.textbook'}) { + $crstype = 'textbook'; + } elsif ($env{'course.'.$env{'request.course.id'}.'.internal.lti'}) { + $crstype = 'lti'; + } else { + $crstype = 'unofficial'; + } + } + my %domdefaults = &Apache::lonnet::get_domain_defaults($cdom); + if ($domdefaults{$crstype.'domexttool'}) { + $tooltypes{'dom'} = 1; + } + if ($domdefaults{$crstype.'exttool'}) { + $tooltypes{'crs'} = 1; + } + } + } + return %tooltypes; +} + sub wishlist_window { return(<<'ENDWISHLIST'); <script type="text/javascript"> @@ -8785,7 +10337,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(). @@ -9157,14 +10709,21 @@ function expand_div(caller) { sub simple_error_page { my ($r,$title,$msg,$args) = @_; + my %displayargs; if (ref($args) eq 'HASH') { if (!$args->{'no_auto_mt_msg'}) { $msg = &mt($msg); } + if ($args->{'only_body'}) { + $displayargs{'only_body'} = 1; + } + if ($args->{'no_nav_bar'}) { + $displayargs{'no_nav_bar'} = 1; + } } else { $msg = &mt($msg); } my $page = - &Apache::loncommon::start_page($title). + &Apache::loncommon::start_page($title,'',\%displayargs). '<p class="LC_error">'.$msg.'</p>'. &Apache::loncommon::end_page(); if (ref($r)) { @@ -9347,6 +10906,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)) { @@ -9470,7 +11039,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(); @@ -9792,8 +11361,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. @@ -9866,8 +11435,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 '') { @@ -10015,7 +11585,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. @@ -10051,6 +11621,8 @@ sub excess_filesize_warning { ############################################### + + sub get_secgrprole_info { my ($cdom,$cnum,$needroles,$type) = @_; my %sections_count = &get_sections($cdom,$cnum); @@ -10159,7 +11731,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">'; @@ -10364,7 +11945,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; @@ -10375,7 +11956,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} = {}; @@ -10445,7 +12026,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 '') { @@ -10468,7 +12049,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'}; } } @@ -10491,7 +12072,7 @@ sub user_rule_check { $id = $inst_results->{$user}->{'id'}; } } - if ($id eq '') { + if ($id eq '') { if (ref($usershash->{$user})) { $id = $usershash->{$user}->{'id'}; } @@ -10810,7 +12391,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. @@ -10820,13 +12401,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)) { @@ -10879,7 +12482,7 @@ sub get_future_slots { $reservable_now{$slot} = { symb => $symb, endreserve => $lastres, - uniqueperiod => $uniqueperiod, + uniqueperiod => $uniqueperiod, }; } elsif (($startreserve > $now) && (!$endreserve || $endreserve > $startreserve)) { @@ -11044,7 +12647,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); @@ -11060,6 +12679,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(); @@ -11067,7 +12689,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'}) { @@ -11099,18 +12721,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"; } @@ -11144,6 +12766,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(.+)$})) { @@ -11186,11 +12818,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') { @@ -11261,6 +12906,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')) { @@ -11299,6 +12947,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) { @@ -11327,17 +12977,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+://}); @@ -11561,7 +13219,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. @@ -11580,7 +13238,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; @@ -11919,7 +13577,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 { @@ -12054,7 +13712,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{/([^/]+)$}); @@ -12580,7 +14238,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.'); @@ -12590,11 +14248,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 }); @@ -12634,7 +14292,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; @@ -12689,7 +14347,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; @@ -13141,7 +14799,7 @@ sub process_extracted_files { $folders{'0'} = $items[-2]; if ($env{'form.folderpath'} =~ /\:1$/) { $containers{'0'}='page'; - } else { + } else { $containers{'0'}='sequence'; } } @@ -13222,7 +14880,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"; } } @@ -13249,7 +14907,7 @@ sub process_extracted_files { $fetch =~ s/^\Q$prefix$dir\E//; $prompttofetch{$fetch} = 1; } - } + } } $LONCAPA::map::resources[$newidx]= $docstitle.':'.$url.':false:normal:res'; @@ -13274,7 +14932,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++) { @@ -13296,7 +14954,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}}"; @@ -13345,13 +15003,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; } } @@ -14567,13 +16225,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) @@ -14582,6 +16240,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 @@ -14825,6 +16484,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 @@ -14929,6 +16673,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; } @@ -15078,8 +16824,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'; @@ -15092,6 +16840,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="'. @@ -15197,27 +16947,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"; @@ -15237,19 +16993,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"; @@ -15274,8 +17035,8 @@ sub commit_studentrole { $oldsecurl = $uurl; $expire_role_result = &Apache::lonnet::assignrole($udom,$uname,$uurl,'st',$now, - '','','',$context); - if ($env{'request.course.sec'} ne '') { + '','','',$context,$othdomby,$requester); + if ($env{'request.course.sec'} ne '') { if ($expire_role_result eq 'refused') { my @roles = ('st'); my @statuses = ('previous'); @@ -15301,7 +17062,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 '') { @@ -15323,7 +17085,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; @@ -15386,7 +17148,8 @@ sub check_clone { my $cloneid='/'.$args->{'clonedomain'}.'/'.$args->{'clonecourse'}; my ($clonecrsudom,$clonecrsunum)= &LONCAPA::split_courseid($cloneid); my $clonehome=&Apache::lonnet::homeserver($clonecrsunum,$clonecrsudom); - my $clonemsg; + my $clonetitle; + my @clonemsg; my $can_clone = 0; my $lctype = lc($args->{'crstype'}); if ($lctype ne 'community') { @@ -15394,16 +17157,38 @@ sub check_clone { } if ($clonehome eq 'no_host') { if ($args->{'crstype'} eq 'Community') { - $clonemsg = &mt('No new community created.').$linefeed.&mt('A new community could not be cloned from the specified original - [_1] - because it is a non-existent community.',$args->{'clonecourse'}.':'.$args->{'clonedomain'}); + push(@clonemsg,({ + mt => 'No new community created.', + args => [], + }, + { + mt => 'A new community could not be cloned from the specified original - [_1] - because it is a non-existent community.', + args => [$args->{'clonedomain'}.':'.$args->{'clonedomain'}], + })); } else { - $clonemsg = &mt('No new course created.').$linefeed.&mt('A new course could not be cloned from the specified original - [_1] - because it is a non-existent course.',$args->{'clonecourse'}.':'.$args->{'clonedomain'}); - } + push(@clonemsg,({ + mt => 'No new course created.', + args => [], + }, + { + mt => 'A new course could not be cloned from the specified original - [_1] - because it is a non-existent course.', + args => [$args->{'clonecourse'}.':'.$args->{'clonedomain'}], + })); + } } else { my %clonedesc = &Apache::lonnet::coursedescription($cloneid,{'one_time' => 1}); + $clonetitle = $clonedesc{'description'}; if ($args->{'crstype'} eq 'Community') { if ($clonedesc{'type'} ne 'Community') { - $clonemsg = &mt('No new community created.').$linefeed.&mt('A new community could not be cloned from the specified original - [_1] - because it is a course not a community.',$args->{'clonecourse'}.':'.$args->{'clonedomain'}); - return ($can_clone, $clonemsg, $cloneid, $clonehome); + push(@clonemsg,({ + mt => 'No new community created.', + args => [], + }, + { + mt => 'A new community could not be cloned from the specified original - [_1] - because it is a course not a community.', + args => [$args->{'clonecourse'}.':'.$args->{'clonedomain'}], + })); + return ($can_clone,\@clonemsg,$cloneid,$clonehome); } } if (($env{'request.role.domain'} eq $args->{'clonedomain'}) && @@ -15420,7 +17205,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'})) { @@ -15439,7 +17224,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) { @@ -15478,12 +17263,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'})) { @@ -15492,20 +17277,34 @@ sub check_clone { } unless ($can_clone) { if ($args->{'crstype'} eq 'Community') { - $clonemsg = &mt('No new community created.').$linefeed.&mt('The new community could not be cloned from the existing community because the new community owner ([_1]) does not have cloning rights in the existing community ([_2]).',$args->{'ccuname'}.':'.$args->{'ccdomain'},$clonedesc{'description'}); + push(@clonemsg,({ + mt => 'No new community created.', + args => [], + }, + { + mt => 'The new community could not be cloned from the existing community because the new community owner ([_1]) does not have cloning rights in the existing community ([_2]).', + args => [$args->{'ccuname'}.':'.$args->{'ccdomain'},$clonedesc{'description'}], + })); } else { - $clonemsg = &mt('No new course created.').$linefeed.&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->{'ccuname'}.':'.$args->{'ccdomain'},$clonedesc{'description'}); - } + push(@clonemsg,({ + mt => 'No new course created.', + args => [], + }, + { + 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'}], + })); + } } } } - return ($can_clone, $clonemsg, $cloneid, $clonehome); + return ($can_clone,\@clonemsg,$cloneid,$clonehome,$clonetitle); } sub construct_course { my ($args,$logmsg,$courseid,$crsudom,$crsunum,$udom,$uname,$context, - $cnum,$category,$coderef) = @_; - my $outcome; + $cnum,$category,$coderef,$callercontext,$user_lh) = @_; + my ($outcome,$msgref,$clonemsgref); my $linefeed = '<br />'."\n"; if ($context eq 'auto') { $linefeed = "\n"; @@ -15514,25 +17313,23 @@ sub construct_course { # # Are we cloning? # - my ($can_clone, $clonemsg, $cloneid, $clonehome); + my ($can_clone,$cloneid,$clonehome,$clonetitle); if (($args->{'clonecourse'}) && ($args->{'clonedomain'})) { - ($can_clone, $clonemsg, $cloneid, $clonehome) = &check_clone($args,$linefeed); - if ($context ne 'auto') { - if ($clonemsg ne '') { - $clonemsg = '<span class="LC_error">'.$clonemsg.'</span>'; - } - } - $outcome .= $clonemsg.$linefeed; - + ($can_clone,$clonemsgref,$cloneid,$clonehome,$clonetitle) = &check_clone($args,$linefeed); if (!$can_clone) { - return (0,$outcome); + return (0,$outcome,$clonemsgref); } } # # 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'}, @@ -15543,15 +17340,20 @@ sub construct_course { $args->{'ccuname'}.':'. $args->{'ccdomain'}, $args->{'crstype'}, - $cnum,$context,$category); + $cnum,$context,$category, + $callercontext); # Note: The testing routines depend on this being output; see # Utils::Course. This needs to at least be output as a comment # if anyone ever decides to not show this, and Utils::Course::new # will need to be suitably modified. - $outcome .= &mt('New LON-CAPA [_1] ID: [_2]',$crstype,$$courseid).$linefeed; + if (($callercontext eq 'auto') && ($user_lh ne '')) { + $outcome .= &mt_user($user_lh,'New LON-CAPA [_1] ID: [_2]',$showncrstype,$$courseid).$linefeed; + } else { + $outcome .= &mt('New LON-CAPA [_1] ID: [_2]',$showncrstype,$$courseid).$linefeed; + } if ($$courseid =~ /^error:/) { - return (0,$outcome); + return (0,$outcome,$clonemsgref); } # @@ -15560,23 +17362,37 @@ sub construct_course { ($$crsudom,$$crsunum)= &LONCAPA::split_courseid($$courseid); my $crsuhome=&Apache::lonnet::homeserver($$crsunum,$$crsudom); if ($crsuhome eq 'no_host') { - $outcome .= &mt('Course creation failed, unrecognized course home server.').$linefeed; - return (0,$outcome); + if (($callercontext eq 'auto') && ($user_lh ne '')) { + $outcome .= &mt_user($user_lh, + 'Course creation failed, unrecognized course home server.'); + } else { + $outcome .= &mt('Course creation failed, unrecognized course home server.'); + } + $outcome .= $linefeed; + return (0,$outcome,$clonemsgref); } $outcome .= &mt('Created on').': '.$crsuhome.$linefeed; # # Do the cloning # + my @clonemsg; if ($can_clone && $cloneid) { - $clonemsg = &mt('Cloning [_1] from [_2]',$crstype,$clonehome); - if ($context ne 'auto') { - $clonemsg = '<span class="LC_success">'.$clonemsg.'</span>'; - } - $outcome .= $clonemsg.$linefeed; + push(@clonemsg, + { + mt => 'Created [_1] by cloning from [_2]', + args => [$showncrstype,$clonetitle], + }); my %oldcenv=&Apache::lonnet::dump('environment',$$crsudom,$$crsunum); # Copy all files - &Apache::lonclonecourse::copycoursefiles($cloneid,$$courseid,$args->{'datemode'},$args->{'dateshift'}); + my @info = + &Apache::lonclonecourse::copycoursefiles($cloneid,$$courseid,$args->{'datemode'}, + $args->{'dateshift'},$args->{'crscode'}, + $args->{'ccuname'}.':'.$args->{'ccdomain'}, + $args->{'tinyurls'}); + if (@info) { + push(@clonemsg,@info); + } # Restore URL $cenv{'url'}=$oldcenv{'url'}; # Restore title @@ -15616,6 +17432,9 @@ sub construct_course { if ($args->{'crstype'}) { $cenv{'type'}=$args->{'crstype'}; } + if ($args->{'lti'}) { + $cenv{'internal.lti'}=$args->{'lti'}; + } if ($args->{'crsid'}) { $cenv{'courseid'}=$args->{'crsid'}; } @@ -15745,14 +17564,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 '') && @@ -15844,7 +17663,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; } @@ -15898,7 +17717,6 @@ sub construct_course { # unless (($args->{'nonstandard'}) || ($args->{'firstres'} eq 'blank') || ($cloneid)) { - use LONCAPA::map; $outcome .= &mt('Setting first resource').': '; my $map = '/uploaded/'.$$crsudom.'/'.$$crsunum.'/default.sequence'; @@ -15921,7 +17739,31 @@ sub construct_course { $outcome .= ($fatal?$errtext:'write ok').$linefeed; } - return (1,$outcome); +# +# 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); } sub make_unique_code { @@ -15934,7 +17776,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; @@ -15981,8 +17823,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)) { @@ -16000,17 +17841,20 @@ sub group_term { my %names = ( 'Course' => 'group', 'Community' => 'group', + 'Placement' => 'group', ); return $names{$crstype}; } sub course_types { - my @types = ('official','unofficial','community','textbook'); + 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); } @@ -16115,9 +17959,8 @@ 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); + my ($filename,$cookie,$userroles,$firstaccenv,$timerintenv, + $coauthorenv); my $now=time; if ($public) { @@ -16138,7 +17981,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)) { @@ -16182,7 +18026,7 @@ sub init_user_environment { # Initialize roles - ($userroles,$firstaccenv,$timerintenv) = + ($userroles,$firstaccenv,$timerintenv,$coauthorenv) = &Apache::lonnet::rolesinit($domain,$username,$authhost); } # ------------------------------------ Check browser type and MathML capability @@ -16194,8 +18038,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'})) { @@ -16210,7 +18053,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, @@ -16261,19 +18104,36 @@ 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') { - $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') { + foreach my $crstype ('official','unofficial','community','textbook','placement','lti') { $userenv{'canrequest.'.$crstype} = &Apache::lonnet::usertools_access($username,$domain,$crstype, 'reload','requestcourses', \%userenv,\%domdef,\%is_adv); } + if ((ref($userroles) eq 'HASH') && ($userroles->{'user.author'}) && + (exists($userroles->{"user.role.au./$domain/"}))) { + if ($userenv{'authoreditors'}) { + $userenv{'editors'} = $userenv{'authoreditors'}; + } elsif ($domdef{'editors'} ne '') { + $userenv{'editors'} = $domdef{'editors'}; + } else { + $userenv{'editors'} = 'edit,xml'; + } + if ($userenv{'authorarchive'}) { + $userenv{'canarchive'} = 1; + } elsif (($userenv{'authorarchive'} eq '') && + ($domdef{'archive'})) { + $userenv{'canarchive'} = 1; + } + } + $userenv{'canrequest.author'} = &Apache::lonnet::usertools_access($username,$domain,'requestauthor', 'reload','requestauthor', @@ -16281,14 +18141,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", @@ -16302,6 +18190,11 @@ sub init_user_environment { if (ref($timerintenv) eq 'HASH') { &_add_to_env(\%disk_env,$timerintenv); } + if (ref($coauthorenv) eq 'HASH') { + if (keys(%{$coauthorenv})) { + &_add_to_env(\%disk_env,$coauthorenv); + } + } if (ref($args->{'extra_env'})) { &_add_to_env(\%disk_env,$args->{'extra_env'}); } @@ -16392,12 +18285,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 @@ -16413,19 +18306,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 @@ -16516,15 +18409,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', ); @@ -16532,6 +18429,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 { @@ -16540,9 +18439,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>"; } @@ -16567,7 +18472,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', @@ -16696,7 +18601,7 @@ $typeelement return $jscript.$clonewarning.$output; } -=pod +=pod =item * &timebased_select_form() @@ -16711,7 +18616,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 @@ -16748,7 +18653,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 @@ -16787,7 +18692,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). @@ -16799,7 +18704,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). @@ -16809,8 +18714,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. @@ -16936,8 +18841,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. @@ -17050,7 +18955,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: @@ -17058,9 +18963,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. @@ -17068,10 +18973,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. @@ -17089,24 +18994,52 @@ sub needs_coursereinit { $interval = 600; } if (($now-$env{'request.course.timechecked'})>$interval) { - my $lastchange = &Apache::lonnet::get_coursechange($cdom,$cnum); &Apache::lonnet::appenv({'request.course.timechecked'=>$now}); - if ($lastchange > $env{'request.course.tied'}) { - my %curr_reqd_hash = &Apache::lonnet::userenvironment($cdom,$cnum,'internal.releaserequired'); - if ($curr_reqd_hash{'internal.releaserequired'} ne '') { - my $required = $env{'course.'.$cdom.'_'.$cnum.'.internal.releaserequired'}; - if ($curr_reqd_hash{'internal.releaserequired'} ne $required) { - &Apache::lonnet::appenv({'course.'.$cdom.'_'.$cnum.'.internal.releaserequired' => - $curr_reqd_hash{'internal.releaserequired'}}); - my ($switchserver,$switchwarning) = - &check_release_required($loncaparev,$cdom.'_'.$cnum,$env{'request.role'}, - $curr_reqd_hash{'internal.releaserequired'}); - if ($switchwarning ne '' || $switchserver ne '') { - return ('switch',$switchwarning,$switchserver); - } + my $blocked = &blocking_status('reinit',undef,$cnum,$cdom,undef,1); + if ($blocked) { + return (); + } + my $update; + my $lastmainchange = &Apache::lonnet::get_coursechange($cdom,$cnum); + my $lastsuppchange = &Apache::lonnet::get_suppchange($cdom,$cnum); + if ($lastmainchange > $env{'request.course.tied'}) { + my ($needswitch,$switchwarning,$switchserver) = &switch_for_update($loncaparev,$cdom,$cnum); + if ($needswitch) { + return ('switch',$switchwarning,$switchserver); + } + $update = 'main'; + } + if ($lastsuppchange > $env{'request.course.suppupdated'}) { + if ($update) { + $update = 'both'; + } else { + my ($needswitch,$switchwarning,$switchserver) = &switch_for_update($loncaparev,$cdom,$cnum); + if ($needswitch) { + return ('switch',$switchwarning,$switchserver); + } else { + $update = 'supp'; } } - return ('update'); + return ($update); + } + } + return (); +} + +sub switch_for_update { + my ($loncaparev,$cdom,$cnum) = @_; + my %curr_reqd_hash = &Apache::lonnet::userenvironment($cdom,$cnum,'internal.releaserequired'); + if ($curr_reqd_hash{'internal.releaserequired'} ne '') { + my $required = $env{'course.'.$cdom.'_'.$cnum.'.internal.releaserequired'}; + if ($curr_reqd_hash{'internal.releaserequired'} ne $required) { + &Apache::lonnet::appenv({'course.'.$cdom.'_'.$cnum.'.internal.releaserequired' => + $curr_reqd_hash{'internal.releaserequired'}}); + my ($switchserver,$switchwarning) = + &check_release_required($loncaparev,$cdom.'_'.$cnum,$env{'request.role'}, + $curr_reqd_hash{'internal.releaserequired'}); + if ($switchwarning ne '' || $switchserver ne '') { + return ('switch',$switchwarning,$switchserver); + } } } return (); @@ -17116,19 +19049,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})); @@ -17141,8 +19086,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); } @@ -17189,8 +19146,10 @@ sub parse_supplemental_title { my $name = &plainname($uname,$udom); $name = &HTML::Entities::encode($name,'"<>&\''); $renametitle = &HTML::Entities::encode($renametitle,'"<>&\''); - $title='<i>'.&Apache::lonlocal::locallocaltime($time).'</i> '. - $name.': <br />'.$foldertitle; + $title='<i>'.&Apache::lonlocal::locallocaltime($time).'</i> '.$name; + if ($foldertitle ne '') { + $title .= ': <br />'.$foldertitle; + } } if (wantarray) { return ($title,$foldertitle,$renametitle); @@ -17198,9 +19157,51 @@ sub parse_supplemental_title { return $title; } +sub get_supplemental { + my ($cnum,$cdom,$ignorecache,$possdel)=@_; + my $hashid=$cnum.':'.$cdom; + my ($supplemental,$cached,$set_httprefs); + unless ($ignorecache) { + ($supplemental,$cached) = &Apache::lonnet::is_cached_new('supplemental',$hashid); + } + unless (defined($cached)) { + my $chome=&Apache::lonnet::homeserver($cnum,$cdom); + unless ($chome eq 'no_host') { + my @order = @LONCAPA::map::order; + my @resources = @LONCAPA::map::resources; + my @resparms = @LONCAPA::map::resparms; + my @zombies = @LONCAPA::map::zombies; + my ($errors,%ids,%hidden); + $errors = + &recurse_supplemental($cnum,$cdom,'supplemental.sequence', + $errors,$possdel,\%ids,\%hidden); + @LONCAPA::map::order = @order; + @LONCAPA::map::resources = @resources; + @LONCAPA::map::resparms = @resparms; + @LONCAPA::map::zombies = @zombies; + $set_httprefs = 1; + if ($env{'request.course.id'} eq $cdom.'_'.$cnum) { + &Apache::lonnet::appenv({'request.course.suppupdated' => time}); + } + $supplemental = { + ids => \%ids, + hidden => \%hidden, + }; + &Apache::lonnet::do_cache_new('supplemental',$hashid,$supplemental,600); + } + } + return ($supplemental,$set_httprefs); +} + sub recurse_supplemental { - my ($cnum,$cdom,$suppmap,$numfiles,$errors) = @_; - if ($suppmap) { + my ($cnum,$cdom,$suppmap,$errors,$possdel,$suppids,$hiddensupp,$hidden) = @_; + if (($suppmap) && (ref($suppids) eq 'HASH') && (ref($hiddensupp) eq 'HASH')) { + my $mapnum; + if ($suppmap eq 'supplemental.sequence') { + $mapnum = 0; + } else { + ($mapnum) = ($suppmap =~ /^supplemental_(\d+)\.sequence$/); + } my ($errtext,$fatal) = &LONCAPA::map::mapread('/uploaded/'.$cdom.'/'.$cnum.'/'.$suppmap); if ($fatal) { $errors ++; @@ -17212,17 +19213,91 @@ sub recurse_supplemental { foreach my $idx (@order) { my ($title,$src,$ext,$type,$status)=split(/\:/,$resources[$idx]); if (($src ne '') && ($status eq 'res')) { + my $id = $mapnum.':'.$idx; + push(@{$suppids->{$src}},$id); + if (($hidden) || (&get_supp_parameter($resparms[$idx],'parameter_hiddenresource') =~ /^yes/i)) { + $hiddensupp->{$id} = 1; + } if ($src =~ m{^\Q/uploaded/$cdom/$cnum/\E(supplemental_\d+\.sequence)$}) { - ($numfiles,$errors) = &recurse_supplemental($cnum,$cdom,$1,$numfiles,$errors); + $errors = &recurse_supplemental($cnum,$cdom,$1,$errors,$possdel,$suppids, + $hiddensupp,$hiddensupp->{$id}); } else { - $numfiles ++; + my $allowed; + if (($env{'request.role.adv'}) || (!$hiddensupp->{$id})) { + $allowed = 1; + } elsif ($possdel) { + foreach my $item (@{$suppids->{$src}}) { + next if ($item eq $id); + unless ($hiddensupp->{$item}) { + $allowed = 1; + last; + } + } + if ((!$allowed) && (exists($env{'httpref.'.$src}))) { + &Apache::lonnet::delenv('httpref.'.$src); + } + } + if ($allowed && (!exists($env{'httpref.'.$src}))) { + &Apache::lonnet::allowuploaded('/adm/coursedoc',$src); + } + } + } + } + } + } + } + return $errors; +} + +sub set_supp_httprefs { + my ($cnum,$cdom,$supplemental,$possdel) = @_; + if (ref($supplemental) eq 'HASH') { + if ((ref($supplemental->{'ids'}) eq 'HASH') && (ref($supplemental->{'hidden'}) eq 'HASH')) { + foreach my $src (keys(%{$supplemental->{'ids'}})) { + next if ($src =~ /\.sequence$/); + if (ref($supplemental->{'ids'}->{$src}) eq 'ARRAY') { + my $allowed; + if ($env{'request.role.adv'}) { + $allowed = 1; + } else { + foreach my $id (@{$supplemental->{'ids'}->{$src}}) { + unless ($supplemental->{'hidden'}->{$id}) { + $allowed = 1; + last; + } + } + } + if (exists($env{'httpref.'.$src})) { + if ($possdel) { + unless ($allowed) { + &Apache::lonnet::delenv('httpref.'.$src); + } } + } elsif ($allowed) { + &Apache::lonnet::allowuploaded('/adm/coursedoc',$src); } } } + if ($env{'request.course.id'} eq $cdom.'_'.$cnum) { + &Apache::lonnet::appenv({'request.course.suppupdated' => time}); + } } } - return ($numfiles,$errors); +} + +sub get_supp_parameter { + my ($resparm,$name)=@_; + return if ($resparm eq ''); + my $value=undef; + my $ptype=undef; + foreach (split('&&&',$resparm)) { + my ($thistype,$thisname,$thisvalue)=split('___',$_); + if ($thisname eq $name) { + $value=$thisvalue; + $ptype=$thistype; + } + } + return $value; } sub symb_to_docspath { @@ -17295,10 +19370,71 @@ sub symb_to_docspath { return $path; } +sub validate_folderpath { + my ($supplementalflag,$allowed,$coursenum,$coursedom) = @_; + if ($env{'form.folderpath'} ne '') { + my @items = split(/\&/,$env{'form.folderpath'}); + my ($badpath,$changed,$got_supp,$supppath,%supphidden,%suppids); + for (my $i=0; $i<@items; $i++) { + my $odd = $i%2; + if (($odd) && (!$supplementalflag) && ($items[$i] !~ /^[^:]*:(|\d+):(|1):(|1):(|1):(|1)$/)) { + $badpath = 1; + } elsif ($odd && $supplementalflag) { + my $idx = $i-1; + if ($items[$i] =~ /^([^:]*)::(|1):::$/) { + my $esc_name = $1; + if ((!$allowed) || ($items[$idx] eq 'supplemental')) { + $supppath .= '&'.$esc_name; + $changed = 1; + } else { + $supppath .= '&'.$items[$i]; + } + } elsif (($allowed) && ($items[$idx] ne 'supplemental')) { + $changed = 1; + my $is_hidden; + unless ($got_supp) { + my ($supplemental) = &get_supplemental($coursenum,$coursedom); + if (ref($supplemental) eq 'HASH') { + if (ref($supplemental->{'hidden'}) eq 'HASH') { + %supphidden = %{$supplemental->{'hidden'}}; + } + if (ref($supplemental->{'ids'}) eq 'HASH') { + %suppids = %{$supplemental->{'ids'}}; + } + } + $got_supp = 1; + } + if (ref($suppids{"/uploaded/$coursedom/$coursenum/$items[$idx].sequence"}) eq 'ARRAY') { + my $mapid = $suppids{"/uploaded/$coursedom/$coursenum/$items[$idx].sequence"}->[0]; + if ($supphidden{$mapid}) { + $is_hidden = 1; + } + } + $supppath .= '&'.$items[$i].'::'.$is_hidden.':::'; + } else { + $supppath .= '&'.$items[$i]; + } + } elsif ((!$odd) && ($items[$i] !~ /^(default|supplemental)(|_\d+)$/)) { + $badpath = 1; + } elsif ($supplementalflag) { + $supppath .= '&'.$items[$i]; + } + last if ($badpath); + } + if ($badpath) { + delete($env{'form.folderpath'}); + } elsif ($changed && $supplementalflag) { + $supppath =~ s/^\&//; + $env{'form.folderpath'} = $supppath; + } + } + return; +} + sub captcha_display { my ($context,$lonhost,$defdom) = @_; my ($output,$error); - my ($captcha,$pubkey,$privkey,$version) = + my ($captcha,$pubkey,$privkey,$version) = &get_captcha_config($context,$lonhost,$defdom); if ($captcha eq 'original') { $output = &create_captcha(); @@ -17369,7 +19505,7 @@ sub get_captcha_config { $captcha = 'recaptcha'; $version = $domconfhash{$serverhomedom.'.login.recaptchaversion'}; if ($version ne '2') { - $version = 1; + $version = 1; } } else { $captcha = 'original'; @@ -17398,7 +19534,7 @@ sub get_captcha_config { $captcha = 'original'; } } - } + } return ($captcha,$pubkey,$privkey,$version); } @@ -17477,22 +19613,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') { @@ -17555,24 +19696,40 @@ 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. sub critical_redirect { - my ($interval) = @_; + my ($interval,$context) = @_; unless (($env{'user.domain'} ne '') && ($env{'user.name'} ne '')) { return (); } if ((time-$env{'user.criticalcheck.time'})>$interval) { - my @what=&Apache::lonnet::dump('critical', $env{'user.domain'}, + if (($env{'request.course.id'}) && (($context eq 'flip') || ($context eq 'contents'))) { + my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; + my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; + my $blocked = &blocking_status('alert',undef,$cnum,$cdom,undef,1); + if ($blocked) { + my $checkrole = "cm./$cdom/$cnum"; + if ($env{'request.course.sec'} ne '') { + $checkrole .= "/$env{'request.course.sec'}"; + } + unless ((&Apache::lonnet::allowed('evb',undef,undef,$checkrole)) && + ($env{'request.role'} !~ m{^st\./$cdom/$cnum})) { + return; + } + } + } + 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 (); } @@ -17626,6 +19783,157 @@ sub des_decrypt { return $plaintext; } +sub get_requested_shorturls { + my ($cdom,$cnum,$navmap) = @_; + return unless (ref($navmap)); + my ($numnew,$errors); + my @toshorten = &Apache::loncommon::get_env_multiple('form.addtiny'); + if (@toshorten) { + my (%maps,%resources,%titles); + &Apache::loncourserespicker::enumerate_course_contents($navmap,\%maps,\%resources,\%titles, + 'shorturls',$cdom,$cnum); + if (keys(%resources)) { + my %tocreate; + foreach my $item (sort {$a <=> $b} (@toshorten)) { + my $symb = $resources{$item}; + if ($symb) { + $tocreate{$cnum.'&'.$symb} = 1; + } + } + if (keys(%tocreate)) { + ($numnew,$errors) = &make_short_symbs($cdom,$cnum, + \%tocreate); + } + } + } + return ($numnew,$errors); +} + +sub make_short_symbs { + my ($cdom,$cnum,$tocreateref,$lockuser) = @_; + my ($numnew,@errors); + if (ref($tocreateref) eq 'HASH') { + my %tocreate = %{$tocreateref}; + if (keys(%tocreate)) { + my %coursetiny = &Apache::lonnet::dump('tiny',$cdom,$cnum); + my $su = Short::URL->new(no_vowels => 1); + my $init = ''; + my (%newunique,%addcourse,%courseonly,%failed); + # get lock on tiny db + my $now = time; + if ($lockuser eq '') { + $lockuser = $env{'user.name'}.':'.$env{'user.domain'}; + } + my $lockhash = { + "lock\0$now" => $lockuser, + }; + my $tries = 0; + my $gotlock = &Apache::lonnet::newput_dom('tiny',$lockhash,$cdom); + my ($code,$error); + while (($gotlock ne 'ok') && ($tries<3)) { + $tries ++; + sleep 1; + $gotlock = &Apache::lonnet::newput_dom('tiny',$lockhash,$cdom); + } + if ($gotlock eq 'ok') { + $init = &shorten_symbs($cdom,$init,$su,\%coursetiny,\%tocreate,\%newunique, + \%addcourse,\%courseonly,\%failed); + if (keys(%failed)) { + my $numfailed = scalar(keys(%failed)); + push(@errors,&mt('error: could not obtain unique six character URL for [quant,_1,resource]',$numfailed)); + } + if (keys(%newunique)) { + my $putres = &Apache::lonnet::newput_dom('tiny',\%newunique,$cdom); + if ($putres eq 'ok') { + $numnew = scalar(keys(%newunique)); + my $newputres = &Apache::lonnet::newput('tiny',\%addcourse,$cdom,$cnum); + unless ($newputres eq 'ok') { + push(@errors,&mt('error: could not store course look-up of short URLs')); + } + } else { + push(@errors,&mt('error: could not store unique six character URLs')); + } + } + my $dellockres = &Apache::lonnet::del_dom('tiny',["lock\0$now"],$cdom); + unless ($dellockres eq 'ok') { + push(@errors,&mt('error: could not release lockfile')); + } + } else { + push(@errors,&mt('error: could not obtain lockfile')); + } + if (keys(%courseonly)) { + my $result = &Apache::lonnet::newput('tiny',\%courseonly,$cdom,$cnum); + if ($result ne 'ok') { + push(@errors,&mt('error: could not update course look-up of short URLs')); + } + } + } + } + return ($numnew,\@errors); +} + +sub shorten_symbs { + my ($cdom,$init,$su,$coursetiny,$tocreate,$newunique,$addcourse,$courseonly,$failed) = @_; + return unless ((ref($su)) && (ref($coursetiny) eq 'HASH') && (ref($tocreate) eq 'HASH') && + (ref($newunique) eq 'HASH') && (ref($addcourse) eq 'HASH') && + (ref($courseonly) eq 'HASH') && (ref($failed) eq 'HASH')); + my (%possibles,%collisions); + foreach my $key (keys(%{$tocreate})) { + my $num = String::CRC32::crc32($key); + my $tiny = $su->encode($num,$init); + if ($tiny) { + $possibles{$tiny} = $key; + } + } + if (!$init) { + $init = 1; + } else { + $init ++; + } + if (keys(%possibles)) { + my @posstiny = keys(%possibles); + my $configuname = &Apache::lonnet::get_domainconfiguser($cdom); + my %currtiny = &Apache::lonnet::get('tiny',\@posstiny,$cdom,$configuname); + if (keys(%currtiny)) { + foreach my $key (keys(%currtiny)) { + next if ($currtiny{$key} eq ''); + if ($currtiny{$key} eq $possibles{$key}) { + my ($tcnum,$tsymb) = split(/\&/,$currtiny{$key}); + unless (($coursetiny->{$tsymb} eq $key) || ($addcourse->{$tsymb} eq $key) || ($courseonly->{$tsymb} eq $key)) { + $courseonly->{$tsymb} = $key; + } + } else { + $collisions{$possibles{$key}} = 1; + } + delete($possibles{$key}); + } + } + foreach my $key (keys(%possibles)) { + $newunique->{$key} = $possibles{$key}; + my ($tcnum,$tsymb) = split(/\&/,$possibles{$key}); + unless (($coursetiny->{$tsymb} eq $key) || ($addcourse->{$tsymb} eq $key) || ($courseonly->{$tsymb} eq $key)) { + $addcourse->{$tsymb} = $key; + } + } + } + if (keys(%collisions)) { + if ($init <5) { + if (!$init) { + $init = 1; + } else { + $init ++; + } + $init = &shorten_symbs($cdom,$init,$su,$coursetiny,\%collisions, + $newunique,$addcourse,$courseonly,$failed); + } else { + foreach my $key (keys(%collisions)) { + $failed->{$key} = 1; + } + } + } + return $init; +} + sub is_nonframeable { my ($url,$absolute,$hostname,$ip,$nocache) = @_; my ($remprotocol,$remhost) = ($url =~ m{^(https?)\://(([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,})}i); @@ -17649,9 +19957,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')); @@ -17779,6 +20085,37 @@ sub is_nonframeable { return $uselink; } +sub page_menu { + my ($menucolls,$menunum) = @_; + my %menu; + foreach my $item (split(/;/,$menucolls)) { + my ($num,$value) = split(/\%/,$item); + if ($num eq $menunum) { + my @entries = split(/\&/,$value); + foreach my $entry (@entries) { + my ($name,$fields) = split(/=/,$entry); + if (($name eq 'top') || ($name eq 'inline') || ($name eq 'foot') || ($name eq 'main')) { + $menu{$name} = $fields; + } else { + my @shown; + if ($fields =~ /,/) { + @shown = split(/,/,$fields); + } else { + @shown = ($fields); + } + if (@shown) { + foreach my $field (@shown) { + next if ($field eq ''); + $menu{$field} = 1; + } + } + } + } + } + } + return %menu; +} + 1; __END__;