--- loncom/interface/loncommon.pm 2016/06/15 17:20:44 1.1245 +++ 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.1245 2016/06/15 17:20:44 raeburn Exp $ +# $Id: loncommon.pm,v 1.1445 2025/01/06 00:22:57 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -61,7 +61,7 @@ use POSIX qw(strftime mktime); use Apache::lonmenu(); use Apache::lonenc(); use Apache::lonlocal; -use Apache::lonnet(); +use Apache::lonnavmaps(); use HTML::Entities; use Apache::lonhtmlcommon(); use Apache::loncoursedata(); @@ -71,6 +71,10 @@ 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(); @@ -78,11 +82,14 @@ 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); @@ -198,7 +205,7 @@ BEGIN { { my $langtabfile = $Apache::lonnet::perlvar{'lonTabDir'}. '/language.tab'; - if ( open(my $fh,"<$langtabfile") ) { + if ( open(my $fh,'<',$langtabfile) ) { while (my $line = <$fh>) { next if ($line=~/^\#/); chomp($line); @@ -220,7 +227,7 @@ BEGIN { { my $copyrightfile = $Apache::lonnet::perlvar{'lonIncludes'}. '/copyright.tab'; - if ( open (my $fh,"<$copyrightfile") ) { + if ( open (my $fh,'<',$copyrightfile) ) { while (my $line = <$fh>) { next if ($line=~/^\#/); chomp($line); @@ -234,7 +241,7 @@ BEGIN { { my $sourcecopyrightfile = $Apache::lonnet::perlvar{'lonIncludes'}. '/source_copyright.tab'; - if ( open (my $fh,"<$sourcecopyrightfile") ) { + if ( open (my $fh,'<',$sourcecopyrightfile) ) { while (my $line = <$fh>) { next if ($line =~ /^\#/); chomp($line); @@ -248,7 +255,7 @@ BEGIN { # -------------------------------------------------------------- default domain designs my $designdir=$Apache::lonnet::perlvar{'lonTabDir'}.'/lonDomColors'; my $designfile = $designdir.'/default.tab'; - if ( open (my $fh,"<$designfile") ) { + if ( open (my $fh,'<',$designfile) ) { while (my $line = <$fh>) { next if ($line =~ /^\#/); chomp($line); @@ -262,12 +269,12 @@ BEGIN { { my $categoryfile = $Apache::lonnet::perlvar{'lonTabDir'}. '/filecategories.tab'; - if ( open (my $fh,"<$categoryfile") ) { + if ( open (my $fh,'<',$categoryfile) ) { while (my $line = <$fh>) { next if ($line =~ /^\#/); chomp($line); my ($extension,$category)=(split(/\s+/,$line,2)); - push @{$category_extensions{lc($category)}},$extension; + push(@{$category_extensions{lc($category)}},$extension); } close($fh); } @@ -277,7 +284,7 @@ BEGIN { { my $typesfile = $Apache::lonnet::perlvar{'lonTabDir'}. '/filetypes.tab'; - if ( open (my $fh,"<$typesfile") ) { + if ( open (my $fh,'<',$typesfile) ) { while (my $line = <$fh>) { next if ($line =~ /^\#/); chomp($line); @@ -430,7 +437,7 @@ sub studentbrowser_javascript { <script type="text/javascript" language="Javascript"> // <![CDATA[ var stdeditbrowser; - function openstdbrowser(formname,uname,udom,clicker,roleflag,ignorefilter,courseadvonly) { + function openstdbrowser(formname,uname,udom,clicker,roleflag,ignorefilter,courseadv,uident) { var url = '/adm/pickstudent?'; var filter; if (!ignorefilter) { @@ -445,7 +452,13 @@ sub studentbrowser_javascript { '&udomelement='+udom+ '&clicker='+clicker; if (roleflag) { url+="&roles=1"; } - if (courseadvonly) { url+="&courseadvonly=1"; } + if (courseadv == 'condition') { + if (document.getElementById('courseadv')) { + courseadv = document.getElementById('courseadv').value; + } + } + 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'; @@ -477,7 +490,7 @@ ENDRESBRW } sub selectstudent_link { - my ($form,$unameele,$udomele,$courseadvonly,$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)."'"; @@ -488,8 +501,17 @@ sub selectstudent_link { return ''; } $callargs.=",'".&Apache::lonhtmlcommon::entity_encode($clickerid)."'"; - if ($courseadvonly) { - $callargs .= ",'',1,1"; + if ($courseadv eq 'only') { + $callargs .= ",'',1,'$courseadv'"; + } elsif ($courseadv eq 'none') { + $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.');">'. @@ -943,8 +965,8 @@ ENDSCRT } sub select_timezone { - my ($name,$selected,$onchange,$includeempty)=@_; - my $output='<select name="'.$name.'" '.$onchange.'>'."\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')) { @@ -965,8 +987,8 @@ sub select_timezone { } sub select_datelocale { - my ($name,$selected,$onchange,$includeempty)=@_; - my $output='<select name="'.$name.'" '.$onchange.'>'."\n"; + my ($name,$selected,$onchange,$includeempty,$disabled)=@_; + my $output='<select name="'.$name.'" '.$onchange.$disabled.'>'."\n"; if ($includeempty) { $output .= '<option value=""'; if ($selected eq '') { @@ -1018,7 +1040,7 @@ sub select_datelocale { } sub select_language { - my ($name,$selected,$includeempty) = @_; + my ($name,$selected,$includeempty,$noedit) = @_; my %langchoices; if ($includeempty) { %langchoices = ('' => 'No language preference'); @@ -1030,7 +1052,7 @@ sub select_language { } } %langchoices = &Apache::lonlocal::texthash(%langchoices); - return &select_form($selected,$name,\%langchoices); + return &select_form($selected,$name,\%langchoices,undef,$noedit); } =pod @@ -1054,7 +1076,7 @@ sub list_languages { if ($code) { my $selector = $supported_codes{$id}; my $description = &plainlanguagedescription($id); - push (@lang_choices, [$selector, $description]); + push(@lang_choices, [$selector, $description]); } } return \@lang_choices; @@ -1176,7 +1198,7 @@ sub linked_select_forms { $result.="select2data${suffix}['d_$s1'].texts = new Array("; my @s2texts; foreach my $value (@s2values) { - push @s2texts, $hashref->{$s1}->{'select2'}->{$value}; + push(@s2texts, $hashref->{$s1}->{'select2'}->{$value}); } $result.="\"@s2texts\");\n"; } @@ -1218,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) { @@ -1243,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 @@ -1267,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); @@ -1292,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>'; } @@ -1304,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 @@ -1336,7 +1372,7 @@ sub helpLatexCheatsheet { $out .= '<span>' .&help_open_topic('Authoring_Output_Tags',&mt('Output Tags'),$stayOnPage,undef,600) .'</span> <span>' - .&help_open_topic('Authoring_Multilingual_Problems',&mt('How to create problems in different languages'),$stayOnPage,undef,600) + .&help_open_topic('Authoring_Multilingual_Problems',&mt('Languages'),$stayOnPage,undef,600) .'</span>'; } $out .= '</span>'; # End cheatsheet @@ -1377,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); @@ -1398,7 +1434,7 @@ sub help_open_menu { } sub top_nav_help { - my ($text) = @_; + my ($text,$linkattr) = @_; $text = &mt($text); my $stay_on_page = 1; @@ -1412,7 +1448,7 @@ sub top_nav_help { if ($link) { return <<"END"; $banner_link -<a href="$link" title="$title">$text</a> +<a href="$link" title="$title" $linkattr>$text</a> END } else { return ' '.$text.' '; @@ -1497,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; @@ -1721,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 { @@ -1775,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[> @@ -1957,7 +2201,7 @@ sub colorfuleditor_js { } } - +$browse_or_search // ]]> </script> @@ -2009,6 +2253,352 @@ sub insert_folding_button { 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 =head1 Excel and CSV file utility routines @@ -2196,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))]; @@ -2268,12 +2872,15 @@ sub multiple_select_form { =pod -=item * &select_form($defdom,$name,$hashref,$onchange) +=item * &select_form($defdom,$name,$hashref,$onchange,$readonly) Returns a string containing a <select name='$name' size='1'> form to allow a user to select options from a ref to a hash containing: option_name => displayed text. An optional $onchange can include -a javascript onchange item, e.g., onchange="this.form.submit();" +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. See lonrights.pm for an example invocation and use. @@ -2341,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]', @@ -2459,7 +3066,7 @@ sub select_level_form { =pod -=item * &select_dom_form($defdom,$name,$includeempty,$showdomdesc,$onchange,$incdoms,$excdoms) +=item * &select_dom_form($defdom,$name,$includeempty,$showdomdesc,$onchange,$incdoms,$excdoms,$disabled) Returns a string containing a <select name='$name' size='1'> form to allow a user to select the domain to preform an operation in. @@ -2476,14 +3083,19 @@ 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. + =cut #------------------------------------------- sub select_dom_form { - my ($defdom,$name,$includeempty,$showdomdesc,$onchange,$incdoms,$excdoms) = @_; + my ($defdom,$name,$includeempty,$showdomdesc,$onchange,$incdoms,$excdoms,$disabled) = @_; if ($onchange) { $onchange = ' onchange="'.$onchange.'"'; } + if ($disabled) { + $disabled = ' disabled="disabled"'; + } my (@domains,%exclude); if (ref($incdoms) eq 'ARRAY') { @domains = sort {lc($a) cmp lc($b)} (@{$incdoms}); @@ -2494,7 +3106,7 @@ sub select_dom_form { if (ref($excdoms) eq 'ARRAY') { map { $exclude{$_} = 1; } @{$excdoms}; } - my $selectdomain = "<select name=\"$name\" size=\"1\"$onchange>\n"; + my $selectdomain = "<select name=\"$name\" size=\"1\"$onchange$disabled>\n"; foreach my $dom (@domains) { next if ($exclude{$dom}); $selectdomain.="<option value=\"$dom\" ". @@ -2720,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. @@ -2870,13 +3484,16 @@ sub authform_kerberos { @_, ); my ($check4,$check5,$krbcheck,$krbarg,$krbver,$result,$authtype, - $autharg,$jscall); + $autharg,$jscall,$disabled); my ($authnum,%can_assign) = &get_assignable_auth($in{'domain'}); if ($in{'kerb_def_auth'} eq 'krb5') { $check5 = ' checked="checked"'; } else { $check4 = ' checked="checked"'; } + if ($in{'readonly'}) { + $disabled = ' disabled="disabled"'; + } $krbarg = $in{'kerb_def_dom'}; if (defined($in{'curr_authtype'})) { if ($in{'curr_authtype'} eq 'krb') { @@ -2921,7 +3538,7 @@ sub authform_kerberos { if (defined($in{'mode'})) { if ($in{'mode'} eq 'modifycourse') { if ($authnum == 1) { - $authtype = '<input type="radio" name="login" value="krb" />'; + $authtype = '<input type="radio" name="login" value="krb"'.$disabled.' />'; } } } @@ -2930,7 +3547,7 @@ sub authform_kerberos { if ($authtype eq '') { $authtype = '<input type="radio" name="login" value="krb" '. 'onclick="'.$jscall.'" onchange="'.$jscall.'"'. - $krbcheck.' />'; + $krbcheck.$disabled.' />'; } if (($can_assign{'krb4'} && $can_assign{'krb5'}) || ($can_assign{'krb4'} && !$can_assign{'krb5'} && @@ -2943,9 +3560,9 @@ sub authform_kerberos { '<label>'.$authtype, '</label><input type="text" size="10" name="krbarg" '. 'value="'.$krbarg.'" '. - 'onchange="'.$jscall.'" />', - '<label><input type="radio" name="krbver" value="4" '.$check4.' />', - '</label><label><input type="radio" name="krbver" value="5" '.$check5.' />', + 'onchange="'.$jscall.'"'.$disabled.' />', + '<label><input type="radio" name="krbver" value="4" '.$check4.$disabled.' />', + '</label><label><input type="radio" name="krbver" value="5" '.$check5.$disabled.' />', '</label>'); } elsif ($can_assign{'krb4'}) { $result .= &mt @@ -2954,7 +3571,7 @@ sub authform_kerberos { '<label>'.$authtype, '</label><input type="text" size="10" name="krbarg" '. 'value="'.$krbarg.'" '. - 'onchange="'.$jscall.'" />', + 'onchange="'.$jscall.'"'.$disabled.' />', '<label><input type="hidden" name="krbver" value="4" />', '</label>'); } elsif ($can_assign{'krb5'}) { @@ -2964,7 +3581,7 @@ sub authform_kerberos { '<label>'.$authtype, '</label><input type="text" size="10" name="krbarg" '. 'value="'.$krbarg.'" '. - 'onchange="'.$jscall.'" />', + 'onchange="'.$jscall.'"'.$disabled.' />', '<label><input type="hidden" name="krbver" value="5" />', '</label>'); } @@ -2977,8 +3594,11 @@ sub authform_internal { kerb_def_dom => 'MSU.EDU', @_, ); - my ($intcheck,$intarg,$result,$authtype,$autharg,$jscall); + my ($intcheck,$intarg,$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 'int') { if ($can_assign{'int'}) { @@ -3007,7 +3627,7 @@ sub authform_internal { if (defined($in{'mode'})) { if ($in{'mode'} eq 'modifycourse') { if ($authnum == 1) { - $authtype = '<input type="radio" name="login" value="int" />'; + $authtype = '<input type="radio" name="login" value="int"'.$disabled.' />'; } } } @@ -3015,14 +3635,14 @@ sub authform_internal { $jscall = "javascript:changed_radio('int',$in{'formname'});"; if ($authtype eq '') { $authtype = '<input type="radio" name="login" value="int" '.$intcheck. - ' onchange="'.$jscall.'" onclick="'.$jscall.'" />'; + ' onchange="'.$jscall.'" onclick="'.$jscall.'"'.$disabled.' />'; } $autharg = '<input type="password" size="10" name="intarg" value="'. - $intarg.'" onchange="'.$jscall.'" />'; + $intarg.'" onchange="'.$jscall.'"'.$disabled.' />'; $result = &mt ('[_1] Internally authenticated (with initial password [_2])', '<label>'.$authtype,'</label>'.$autharg); - $result.="<label><input type=\"checkbox\" name=\"visible\" onclick='if (this.checked) { this.form.intarg.type=\"text\" } else { this.form.intarg.type=\"password\" }' />".&mt('Visible input').'</label>'; + $result.='<label><input type="checkbox" name="visible" onclick="if (this.checked) { this.form.intarg.type='."'text'".' } else { this.form.intarg.type='."'password'".' }"'.$disabled.' />'.&mt('Visible input').'</label>'; return $result; } @@ -3032,8 +3652,11 @@ sub authform_local { kerb_def_dom => 'MSU.EDU', @_, ); - my ($loccheck,$locarg,$result,$authtype,$autharg,$jscall); + my ($loccheck,$locarg,$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 'loc') { if ($can_assign{'loc'}) { @@ -3062,7 +3685,7 @@ sub authform_local { if (defined($in{'mode'})) { if ($in{'mode'} eq 'modifycourse') { if ($authnum == 1) { - $authtype = '<input type="radio" name="login" value="loc" />'; + $authtype = '<input type="radio" name="login" value="loc"'.$disabled.' />'; } } } @@ -3071,10 +3694,10 @@ sub authform_local { if ($authtype eq '') { $authtype = '<input type="radio" name="login" value="loc" '. $loccheck.' onchange="'.$jscall.'" onclick="'. - $jscall.'" />'; + $jscall.'"'.$disabled.' />'; } $autharg = '<input type="text" size="10" name="locarg" value="'. - $locarg.'" onchange="'.$jscall.'" />'; + $locarg.'" onchange="'.$jscall.'"'.$disabled.' />'; $result = &mt('[_1] Local Authentication with argument [_2]', '<label>'.$authtype,'</label>'.$autharg); return $result; @@ -3086,8 +3709,11 @@ sub authform_filesystem { kerb_def_dom => 'MSU.EDU', @_, ); - my ($fsyscheck,$result,$authtype,$autharg,$jscall); + my ($fsyscheck,$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 'fsys') { if ($can_assign{'fsys'}) { @@ -3100,7 +3726,7 @@ sub authform_filesystem { } else { $result = &mt('Currently Filesystem Authenticated.'); return $result; - } + } } } else { if ($authnum == 1) { @@ -3113,7 +3739,7 @@ sub authform_filesystem { if (defined($in{'mode'})) { if ($in{'mode'} eq 'modifycourse') { if ($authnum == 1) { - $authtype = '<input type="radio" name="login" value="fsys" />'; + $authtype = '<input type="radio" name="login" value="fsys"'.$disabled.' />'; } } } @@ -3122,16 +3748,71 @@ sub authform_filesystem { if ($authtype eq '') { $authtype = '<input type="radio" name="login" value="fsys" '. $fsyscheck.' onchange="'.$jscall.'" onclick="'. - $jscall.'" />'; + $jscall.'"'.$disabled.' />'; } - $autharg = '<input type="text" size="10" name="fsysarg" value=""'. - ' onchange="'.$jscall.'" />'; + $autharg = '<input type="password" size="10" name="fsysarg" value=""'. + ' onchange="'.$jscall.'"'.$disabled.' />'; $result = &mt ('[_1] Filesystem Authenticated (with initial password [_2])', - '<label><input type="radio" name="login" value="fsys" '. - $fsyscheck.'onchange="'.$jscall.'" onclick="'.$jscall.'" />', - '</label><input type="password" size="10" name="fsysarg" value="" '. - 'onchange="'.$jscall.'" />'); + '<label>'.$authtype,'</label>'.$autharg); + return $result; +} + +sub authform_lti { + my %in = ( + formname => 'document.cu', + kerb_def_dom => 'MSU.EDU', + @_, + ); + my ($lticheck,$result,$authtype,$autharg,$jscall,$disabled); + my ($authnum,%can_assign) = &get_assignable_auth($in{'domain'}); + if ($in{'readonly'}) { + $disabled = ' disabled="disabled"'; + } + if (defined($in{'curr_authtype'})) { + if ($in{'curr_authtype'} eq 'lti') { + if ($can_assign{'lti'}) { + $lticheck = 'checked="checked" '; + if (defined($in{'mode'})) { + if ($in{'mode'} eq 'modifyuser') { + $lticheck = ''; + } + } + } else { + $result = &mt('Currently LTI Authenticated.'); + return $result; + } + } + } else { + if ($authnum == 1) { + $authtype = '<input type="hidden" name="login" value="lti" />'; + } + } + if (!$can_assign{'lti'}) { + return; + } elsif ($authtype eq '') { + if (defined($in{'mode'})) { + if ($in{'mode'} eq 'modifycourse') { + if ($authnum == 1) { + $authtype = '<input type="radio" name="login" value="lti"'.$disabled.' />'; + } + } + } + } + $jscall = "javascript:changed_radio('lti',$in{'formname'});"; + if (($authtype eq '') && (($in{'mode'} eq 'modifycourse') || ($in{'curr_authtype'} ne 'lti'))) { + $authtype = '<input type="radio" name="login" value="lti" '. + $lticheck.' onchange="'.$jscall.'" onclick="'. + $jscall.'"'.$disabled.' />'; + } + $autharg = '<input type="hidden" name="ltiarg" value="" />'; + if ($authtype) { + $result = &mt('[_1] LTI Authenticated', + '<label>'.$authtype.'</label>'.$autharg); + } else { + $result = '<b>'.&mt('LTI Authenticated').'</b>'. + $autharg; + } return $result; } @@ -3145,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') { @@ -3153,7 +3835,7 @@ sub get_assignable_auth { my $context; if ($env{'request.role'} =~ /^au/) { $context = 'author'; - } elsif ($env{'request.role'} =~ /^dc/) { + } elsif ($env{'request.role'} =~ /^(dc|dh)/) { $context = 'domain'; } elsif ($env{'request.course.id'}) { $context = 'course'; @@ -3177,6 +3859,243 @@ sub get_assignable_auth { return ($authnum,%can_assign); } +sub check_passwd_rules { + my ($domain,$plainpass) = @_; + my %passwdconf = &Apache::lonnet::get_passwdconf($domain); + my ($min,$max,@chars,@brokerule,$warning); + $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'}; + } + @chars = @{$passwdconf{'chars'}}; + } + if (($min) && (length($plainpass) < $min)) { + push(@brokerule,'min'); + } + if (($max) && (length($plainpass) > $max)) { + push(@brokerule,'max'); + } + if (@chars) { + my %rules; + map { $rules{$_} = 1; } @chars; + if ($rules{'uc'}) { + unless ($plainpass =~ /[A-Z]/) { + push(@brokerule,'uc'); + } + } + if ($rules{'lc'}) { + unless ($plainpass =~ /[a-z]/) { + push(@brokerule,'lc'); + } + } + if ($rules{'num'}) { + unless ($plainpass =~ /\d/) { + push(@brokerule,'num'); + } + } + if ($rules{'spec'}) { + unless ($plainpass =~ /[!"#$%&'()*+,\-.\/:;<=>?@[\\\]^_`{|}~]/) { + push(@brokerule,'spec'); + } + } + } + if (@brokerule) { + my %rulenames = &Apache::lonlocal::texthash( + uc => 'At least one upper case letter', + lc => 'At least one lower case letter', + num => 'At least one number', + spec => 'At least one non-alphanumeric', + ); + $rulenames{'uc'} .= ': ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + $rulenames{'lc'} .= ': abcdefghijklmnopqrstuvwxyz'; + $rulenames{'num'} .= ': 0123456789'; + $rulenames{'spec'} .= ': !"\#$%&\'()*+,-./:;<=>?@[\]^_\`{|}~'; + $rulenames{'min'} = &mt('Minimum password length: [_1]',$min); + $rulenames{'max'} = &mt('Maximum password length: [_1]',$max); + $warning = &mt('Password did not satisfy the following:').'<ul>'; + foreach my $rule ('min','max','uc','lc','num','spec') { + if (grep(/^$rule$/,@brokerule)) { + $warning .= '<li>'.$rulenames{$rule}.'</li>'; + } + } + $warning .= '</ul>'; + } + if (wantarray) { + return @brokerule; + } + 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 ## ############################################################### @@ -3657,6 +4576,30 @@ sub syllabuswrapper { # ----------------------------------------------------------------------------- +sub aboutme_on { + my ($uname,$udom)=@_; + unless ($uname) { $uname=$env{'user.name'}; } + unless ($udom) { $udom=$env{'user.domain'}; } + return if ($udom eq 'public' && $uname eq 'public'); + my $hashkey=$uname.':'.$udom; + my ($aboutme,$cached)=&Apache::lonnet::is_cached_new('aboutme',$hashkey); + if ($cached) { + return $aboutme; + } + $aboutme = &Apache::lonnet::usertools_access($uname,$udom,'aboutme'); + &Apache::lonnet::do_cache_new('aboutme',$hashkey,$aboutme,3600); + return $aboutme; +} + +sub devalidate_aboutme_cache { + my ($uname,$udom)=@_; + if (!$udom) { $udom =$env{'user.domain'}; } + if (!$uname) { $uname=$env{'user.name'}; } + return if ($udom eq 'public' && $uname eq 'public'); + my $id=$uname.':'.$udom; + &Apache::lonnet::devalidate_cache_new('aboutme',$id); +} + sub track_student_link { my ($linktext,$sname,$sdom,$target,$start,$only_body) = @_; my $link ="/adm/trackstudent?"; @@ -3874,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 @@ -4258,9 +5205,15 @@ sub get_previous_attempt { } $prevattempts.= &end_data_table_row().&end_data_table(); } else { + my $msg; + if ($symb =~ /ext\.tool$/) { + $msg = &mt('No grade passed back.'); + } else { + $msg = &mt('Nothing submitted - no attempts.'); + } $prevattempts= &start_data_table().&start_data_table_row(). - '<td>'.&mt('Nothing submitted - no attempts.').'</td>'. + '<td>'.$msg.'</td>'. &end_data_table_row().&end_data_table(); } } else { @@ -4365,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; @@ -4409,6 +5365,59 @@ sub get_student_view_with_retries { } } +sub css_links { + my ($currsymb,$level) = @_; + my ($links,@symbs,%cssrefs,%httpref); + if ($level eq 'map') { + my $navmap = Apache::lonnavmaps::navmap->new(); + if (ref($navmap)) { + my ($map,undef,$url)=&Apache::lonnet::decode_symb($currsymb); + my @resources = $navmap->retrieveResources($map,sub { $_[0]->is_problem() },0,0); + foreach my $res (@resources) { + if (ref($res) && $res->symb()) { + push(@symbs,$res->symb()); + } + } + } + } else { + @symbs = ($currsymb); + } + foreach my $symb (@symbs) { + my $css_href = &Apache::lonnet::EXT('resource.0.cssfile',$symb); + if ($css_href =~ /\S/) { + unless ($css_href =~ m{https?://}) { + my $url = (&Apache::lonnet::decode_symb($symb))[-1]; + my $proburl = &Apache::lonnet::clutter($url); + my ($probdir) = ($proburl =~ m{(.+)/[^/]+$}); + unless ($css_href =~ m{^/}) { + $css_href = &Apache::lonnet::hreflocation($probdir,$css_href); + } + if ($css_href =~ m{^/(res|uploaded)/}) { + unless (($httpref{'httpref.'.$css_href}) || + (&Apache::lonnet::is_on_map($css_href))) { + my $thisurl = $proburl; + if ($env{'httpref.'.$proburl}) { + $thisurl = $env{'httpref.'.$proburl}; + } + $httpref{'httpref.'.$css_href} = $thisurl; + } + } + } + $cssrefs{$css_href} = 1; + } + } + if (keys(%httpref)) { + &Apache::lonnet::appenv(\%httpref); + } + if (keys(%cssrefs)) { + foreach my $css_href (keys(%cssrefs)) { + next unless ($css_href =~ m{^(/res/|/uploaded/|https?://)}); + $links .= '<link rel="stylesheet" type="text/css" href="'.$css_href.'" />'."\n"; + } + } + return $links; +} + =pod =item * &get_student_answers() @@ -4664,13 +5673,96 @@ sub findallcourses { ############################################### sub blockcheck { - my ($setters,$activity,$uname,$udom,$url,$is_course) = @_; - + my ($setters,$activity,$clientip,$uname,$udom,$url,$is_course,$symb,$caller) = @_; + unless (($activity eq 'docs') || ($activity eq 'reinit') || ($activity eq 'alert')) { + my ($has_evb,$check_ipaccess); + my $dom = $env{'user.domain'}; + if ($env{'request.course.id'}) { + my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; + my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; + my $checkrole = "cm./$cdom/$cnum"; + my $sec = $env{'request.course.sec'}; + if ($sec ne '') { + $checkrole .= "/$sec"; + } + if ((&Apache::lonnet::allowed('evb',undef,undef,$checkrole)) && + ($env{'request.role'} !~ /^st/)) { + $has_evb = 1; + } + unless ($has_evb) { + if (($activity eq 'printout') || ($activity eq 'grades') || ($activity eq 'search') || + ($activity eq 'index') || ($activity eq 'boards') || ($activity eq 'groups') || + ($activity eq 'chat')) { + if ($udom eq $cdom) { + $check_ipaccess = 1; + } + } + } + } elsif (($activity eq 'com') || ($activity eq 'port') || ($activity eq 'blogs') || + ($activity eq 'about') || ($activity eq 'wishlist') || ($activity eq 'passwd')) { + my $checkrole; + if ($env{'request.role.domain'} eq '') { + $checkrole = "cm./$env{'user.domain'}/"; + } else { + $checkrole = "cm./$env{'request.role.domain'}/"; + } + if (($checkrole) && (&Apache::lonnet::allowed('evb',undef,undef,$checkrole))) { + $has_evb = 1; + } + } + unless ($has_evb || $check_ipaccess) { + my @machinedoms = &Apache::lonnet::current_machine_domains(); + if (($dom eq 'public') && ($activity eq 'port')) { + $dom = $udom; + } + if (($dom ne '') && (grep(/^\Q$dom\E$/,@machinedoms))) { + $check_ipaccess = 1; + } else { + my $lonhost = $Apache::lonnet::perlvar{'lonHostID'}; + my $internet_names = &Apache::lonnet::get_internet_names($lonhost); + my $prim = &Apache::lonnet::domain($dom,'primary'); + my $intdom = &Apache::lonnet::internet_dom($prim); + if (($intdom ne '') && (ref($internet_names) eq 'ARRAY')) { + if (grep(/^\Q$intdom\E$/,@{$internet_names})) { + $check_ipaccess = 1; + } + } + } + } + if ($check_ipaccess) { + my ($ipaccessref,$cached)=&Apache::lonnet::is_cached_new('ipaccess',$dom); + unless (defined($cached)) { + my %domconfig = + &Apache::lonnet::get_dom('configuration',['ipaccess'],$dom); + $ipaccessref = &Apache::lonnet::do_cache_new('ipaccess',$dom,$domconfig{'ipaccess'},1800); + } + if ((ref($ipaccessref) eq 'HASH') && ($clientip)) { + foreach my $id (keys(%{$ipaccessref})) { + if (ref($ipaccessref->{$id}) eq 'HASH') { + my $range = $ipaccessref->{$id}->{'ip'}; + if ($range) { + if (&Apache::lonnet::ip_match($clientip,$range)) { + if (ref($ipaccessref->{$id}->{'commblocks'}) eq 'HASH') { + if ($ipaccessref->{$id}->{'commblocks'}->{$activity} eq 'on') { + return ('','','',$id,$dom); + last; + } + } + } + } + } + } + } + } + if (($activity eq 'wishlist') || ($activity eq 'annotate')) { + return (); + } + } if (defined($udom) && defined($uname)) { # If uname and udom are for a course, check for blocks in the course. if (($is_course) || (&Apache::lonnet::is_course($udom,$uname))) { my ($startblock,$endblock,$triggerblock) = - &get_blocks($setters,$activity,$udom,$uname,$url); + &get_blocks($setters,$activity,$udom,$uname,$url,$symb,$caller); return ($startblock,$endblock,$triggerblock); } } else { @@ -4681,13 +5773,18 @@ sub blockcheck { my $startblock = 0; my $endblock = 0; my $triggerblock = ''; - my %live_courses = &findallcourses(undef,$uname,$udom); + my %live_courses; + unless (($activity eq 'wishlist') || ($activity eq 'annotate')) { + %live_courses = &findallcourses(undef,$uname,$udom); + } # If uname is for a user, and activity is course-specific, i.e., # boards, chat or groups, check for blocking in current course only. 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'}) { @@ -4771,7 +5868,7 @@ sub blockcheck { $tdom,$spec,$trest,$area); } } - my ($author,$adv) = &Apache::lonnet::set_userprivs(\%userroles,\%allroles); + my ($author,$adv,$rar) = &Apache::lonnet::set_userprivs(\%userroles,\%allroles); if ($userroles{'user.priv.'.$checkrole} =~ /evb\&([^\:]*)/) { if ($1) { $no_userblock = 1; @@ -4793,11 +5890,11 @@ sub blockcheck { ($env{'request.role'} !~ m{^st\./\Q$cdom\E/\Q$cnum\E})); next if ($no_userblock); - # Retrieve blocking times and identity of locker for course + # 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); + &get_blocks($setters,$activity,$cdom,$cnum,$url,$symb,$caller); if (($start != 0) && (($startblock == 0) || ($startblock > $start))) { $startblock = $start; @@ -4817,7 +5914,7 @@ sub blockcheck { } sub get_blocks { - my ($setters,$activity,$cdom,$cnum,$url) = @_; + my ($setters,$activity,$cdom,$cnum,$url,$symb,$caller) = @_; my $startblock = 0; my $endblock = 0; my $triggerblock = ''; @@ -4830,7 +5927,13 @@ sub get_blocks { my $now = time; my %commblocks = &Apache::lonnet::get_comm_blocks($cdom,$cnum); if ($activity eq 'docs') { - @blockers = &Apache::lonnet::has_comm_blocking('bre',undef,$url,\%commblocks); + my ($blocked,$nosymbcache,$noenccheck); + if (($caller eq 'blockedaccess') || ($caller eq 'blockingstatus')) { + $blocked = 1; + $nosymbcache = 1; + $noenccheck = 1; + } + @blockers = &Apache::lonnet::has_comm_blocking('bre',$symb,$url,$nosymbcache,$noenccheck,$blocked,\%commblocks); foreach my $block (@blockers) { if ($block =~ /^firstaccess____(.+)$/) { my $item = $1; @@ -4882,13 +5985,19 @@ sub get_blocks { my $end = $start + $env{'course.'.$cdom.'_'.$cnum.'.timerinterval.'.$timersymb}; if ($start && $end) { if (($start <= time) && ($end >= time)) { - unless (grep(/^\Q$block\E$/,@blockers)) { - push(@blockers,$block); - $triggered{$block} = { - start => $start, - end => $end, - type => $type, - }; + if (ref($commblocks{$block}) eq 'HASH') { + if (ref($commblocks{$block}{'blocks'}) eq 'HASH') { + if ($commblocks{$block}{'blocks'}{$activity} eq 'on') { + unless(grep(/^\Q$block\E$/,@blockers)) { + push(@blockers,$block); + $triggered{$block} = { + start => $start, + end => $end, + type => $type, + }; + } + } + } } } } @@ -4952,14 +6061,17 @@ sub parse_block_record { } sub blocking_status { - my ($activity,$uname,$udom,$url,$is_course) = @_; + my ($activity,$clientip,$uname,$udom,$url,$is_course,$symb,$caller) = @_; my %setters; # check for active blocking - my ($startblock,$endblock,$triggerblock) = - &blockcheck(\%setters,$activity,$uname,$udom,$url,$is_course); + if ($clientip eq '') { + $clientip = &Apache::lonnet::get_requestor_ip(); + } + my ($startblock,$endblock,$triggerblock,$by_ip,$blockdom) = + &blockcheck(\%setters,$activity,$clientip,$uname,$udom,$url,$is_course,$symb,$caller); my $blocked = 0; - if ($startblock && $endblock) { + if (($startblock && $endblock) || ($by_ip)) { $blocked = 1; } @@ -4968,12 +6080,17 @@ sub blocking_status { # build a link to a popup window containing the details my $querystring = "?activity=$activity"; -# $uname and $udom decide whose portfolio the user is trying to look at - if (($activity eq 'port') || ($activity eq 'passwd')) { +# $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 .= "&uname=$uname" if ($uname =~ /^$match_username$/); } elsif ($activity eq 'docs') { - $querystring .= '&url='.&HTML::Entities::encode($url,'&"'); + my $showurl = &Apache::lonenc::check_encrypt($url); + $querystring .= '&url='.&HTML::Entities::encode($showurl,'\'&"<>'); + if ($symb) { + my $showsymb = &Apache::lonenc::check_encrypt($symb); + $querystring .= '&symb='.&HTML::Entities::encode($showsymb,'\'&"<>'); + } } my $output .= <<'END_MYBLOCK'; @@ -4998,6 +6115,22 @@ END_MYBLOCK $text = &mt('Printing Blocked'); } elsif ($activity eq 'passwd') { $text = &mt('Password Changing Blocked'); + } elsif ($activity eq 'grades') { + $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') { + $text = &mt('Access to Stored Links Blocked'); + } elsif ($activity eq 'annotate') { + $text = &mt('Access to Annotations Blocked'); } $output .= <<"END_BLOCK"; <div class='$class'> @@ -5021,8 +6154,14 @@ sub check_ip_acc { if (!defined($acc) || $acc =~ /^\s*$/ || $acc =~/^\s*no\s*$/i) { return 1; } - my $allowed; - my $ip=$env{'request.host'} || $ENV{'REMOTE_ADDR'} || $clientip; + 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; + } else { + my $remote_ip = &Apache::lonnet::get_requestor_ip(); + $ip = $remote_ip || $env{'request.host'} || $clientip; + } my $name; my %access = ( @@ -5173,6 +6312,17 @@ sub get_domainconf { } } } + } elsif ($key eq 'saml') { + if (ref($domconfig{'login'}{$key}) eq 'HASH') { + foreach my $host (keys(%{$domconfig{'login'}{$key}})) { + if (ref($domconfig{'login'}{$key}{$host}) eq 'HASH') { + $designhash{$udom.'.login.'.$key.'_'.$host} = 1; + foreach my $item ('text','img','alt','url','title','window','notsso') { + $designhash{$udom.'.login.'.$key.'_'.$item.'_'.$host} = $domconfig{'login'}{$key}{$host}{$item}; + } + } + } + } } else { foreach my $img (keys(%{$domconfig{'login'}{$key}})) { $designhash{$udom.'.login.'.$key.'_'.$img} = @@ -5237,7 +6387,7 @@ sub get_legacy_domconf { my $designdir=$Apache::lonnet::perlvar{'lonTabDir'}.'/lonDomColors'; my $designfile = $designdir.'/'.$udom.'.tab'; if (-e $designfile) { - if ( open (my $fh,"<$designfile") ) { + if ( open (my $fh,'<',$designfile) ) { while (my $line = <$fh>) { next if ($line =~ /^\#/); chomp($line); @@ -5277,8 +6427,12 @@ sub domainlogo { &Apache::lonnet::repcopy($local_name); } $imgsrc = &lonhttpdurl($imgsrc); - } - return '<img src="'.$imgsrc.'" alt="'.$domain.'" />'; + } + my $alttext = $domain; + if ($designhash{$domain.'.login.alttext_domlogo'} ne '') { + $alttext = $designhash{$domain.'.login.alttext_domlogo'}; + } + return '<img src="'.$imgsrc.'" alt="'.$alttext.'" id="lclogindomlogo" />'; } elsif (defined(&Apache::lonnet::domain($domain,'description'))) { return &Apache::lonnet::domain($domain,'description'); } else { @@ -5396,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 @@ -5403,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'}; } @@ -5426,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 .= @@ -5441,17 +6624,162 @@ sub CSTR_pageheader { .'</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; +} + ############################################### ############################################### @@ -5491,11 +6819,43 @@ Inputs: =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 + contain https://<hostname> if server uses + https (as per hosts.tab), but request is for http + hostname -> hostname, from $r->hostname(). =item * $advtoolsref, optional argument, ref to an array containing 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. @@ -5507,7 +6867,8 @@ other decorations will be returned. sub bodytag { my ($title,$function,$addentries,$bodyonly,$domain,$forcereg, - $no_nav_bar,$bgcolor,$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')) @@ -5516,9 +6877,9 @@ sub bodytag { } if (!$args->{'no_auto_mt_title'}) { $title = &mt($title); } my $httphost = $args->{'use_absolute'}; + 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); @@ -5535,19 +6896,39 @@ sub bodytag { if ($realm) { $realm = '/'.$realm; } - if ($role eq 'ca') { + if ($role eq 'ca') { my ($rdom,$rname) = ($realm =~ m{^/($match_domain)/($match_username)$}); $realm = &plainname($rname,$rdom); } # realm + my ($cid,$sec); if ($env{'request.course.id'}) { + $cid = $env{'request.course.id'}; + if ($env{'request.course.sec'}) { + $sec = $env{'request.course.sec'}; + } + } elsif ($realm =~ m{^/($match_domain)/($match_courseid)(?:|/(\w+))$}) { + if (&Apache::lonnet::is_course($1,$2)) { + $cid = $1.'_'.$2; + $sec = $3; + } + } + if ($cid) { if ($env{'request.role'} !~ /^cr/) { $role = &Apache::lonnet::plaintext($role,&course_type()); + } elsif ($role =~ m{^cr/($match_domain)/\1-domainconfig/(\w+)$}) { + if ($env{'request.role.desc'}) { + $role = $env{'request.role.desc'}; + } else { + $role = &mt('Helpdesk[_1]',' '.$2); + } + } else { + $role = (split(/\//,$role,4))[-1]; } - if ($env{'request.course.sec'}) { - $role .= (' 'x2).'- '.&mt('section:').' '.$env{'request.course.sec'}; + if ($sec) { + $role .= (' 'x2).'- '.&mt('section:').' '.$sec; } - $realm = $env{'course.'.$env{'request.course.id'}.'.description'}; + $realm = $env{'course.'.$cid.'.description'}; } else { $role = &Apache::lonnet::plaintext($role); } @@ -5569,22 +6950,43 @@ sub bodytag { if ($public) { 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'} && exists($env{'user.role.dc./'. - $env{'course.'.$env{'request.course.id'}. - '.domain'}.'/'})) { - my $cid = $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+$//; } my $crstype; - if ($env{'request.course.id'}) { - $crstype = $env{'course.'.$env{'request.course.id'}.'.type'}; + if ($cid) { + $crstype = $env{'course.'.$cid.'.type'}; } elsif ($args->{'crstype'}) { $crstype = $args->{'crstype'}; } @@ -5600,60 +7002,113 @@ sub bodytag { # $titleinfo = &CSTR_pageheader(); #FIXME: Will be removed once all scripts have their own calls # } - $bodytag .= Apache::lonhtmlcommon::scripttag( - Apache::lonmenu::utilityfunctions($httphost), 'start'); - - my ($left,$right) = Apache::lonmenu::primary_menu($crstype); - - if ($env{'request.noversionuri'} =~ m{^/res/adm/pages/}) { - if ($dc_info) { - $dc_info = qq|<span class="LC_cusr_subheading">$dc_info</span>|; - } - $bodytag .= qq|<div id="LC_nav_bar">$left $role<br /> - <em>$realm</em> $dc_info</div>|; - return $bodytag; + my $need_endlcint; + unless ($args->{'switchserver'}) { + $bodytag .= Apache::lonhtmlcommon::scripttag( + Apache::lonmenu::utilityfunctions($httphost), 'start'); + $need_endlcint = 1; } - unless ($env{'request.symb'} =~ m/\.page___\d+___/) { - $bodytag .= qq|<div id="LC_nav_bar">$left $role</div>|; + 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) { + $dc_info = qq|<span class="LC_cusr_subheading">$dc_info</span>|; + } + $bodytag .= qq|<div id="LC_nav_bar">$left $role<br /> + <em>$realm</em> $dc_info</div>|; + if ($need_endlcint) { + $bodytag .= Apache::lonhtmlcommon::scripttag('', 'end'); + } + return $bodytag; + } + + unless ($env{'request.symb'} =~ m/\.page___\d+___/) { + $bodytag .= qq|<div id="LC_nav_bar">$left $role</div>|; + } - $bodytag .= $right; + $bodytag .= $right; - if ($dc_info) { - $dc_info = &dc_courseid_toggle($dc_info); + if ($dc_info) { + $dc_info = &dc_courseid_toggle($dc_info); + } + $bodytag .= qq|<div id="LC_realm">$realm $dc_info</div>|; } - $bodytag .= qq|<div id="LC_realm">$realm $dc_info</div>|; #if directed to not display the secondary menu, don't. if ($args->{'no_secondary_menu'}) { + if ($need_endlcint) { + $bodytag .= Apache::lonhtmlcommon::scripttag('', 'end'); + } return $bodytag; } #don't show menus for public users if (!$public){ - $bodytag .= Apache::lonmenu::secondary_menu($httphost); + 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(); - $bodytag .= Apache::lonhtmlcommon::scripttag('', 'end'); + if ($need_endlcint) { + $bodytag .= Apache::lonhtmlcommon::scripttag('', 'end'); + } if ($env{'request.state'} eq 'construct') { $bodytag .= &Apache::lonmenu::innerregister($forcereg, - $args->{'bread_crumbs'}); + $args->{'bread_crumbs'},'','',$hostname, + $ltiscope,$ltiuri,$showncrumbsref); } elsif ($forcereg) { $bodytag .= &Apache::lonmenu::innerregister($forcereg,undef, - $args->{'group'}); + $args->{'group'},$args->{'hide_buttons'}, + $hostname,$ltiscope,$ltiuri,$showncrumbsref); } else { $bodytag .= &Apache::lonmenu::prepare_functions($env{'request.noversionuri'}, $forcereg,$args->{'group'}, $args->{'bread_crumbs'}, - $advtoolsref); + $advtoolsref,'',$hostname); } - }else{ - # this is to seperate menu from content when there's no secondary - # menu. Especially needed for public accessible ressources. + } else { + # this is to separate menu from content when there's no secondary + # menu. Especially needed for publicly accessible resources. $bodytag .= '<hr style="clear:both" />'; - $bodytag .= Apache::lonhtmlcommon::scripttag('', 'end'); + if ($need_endlcint) { + $bodytag .= Apache::lonhtmlcommon::scripttag('', 'end'); + } + } + if ($args->{'collapsible_header'} ne '') { + $bodytag .= $args->{'collapsible_header'}. + '<div id="LC_collapsible_separator"></div>'. + '</div></div>'; } - return $bodytag; } @@ -5723,12 +7178,45 @@ 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; } } + if ((ref($args) eq 'HASH') && ($args->{'dashjs'})) { + $endbodytag = &Apache::lonhtmlcommon::dash_to_minus_js().$endbodytag; + } return $endbodytag; } @@ -5749,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); @@ -5802,6 +7289,7 @@ body { line-height:130%; font-size:0.83em; color:$font; + background-color: $pgbg_or_bgcolor; } a:focus, @@ -5813,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; } @@ -5833,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; @@ -6107,6 +7619,11 @@ td.LC_menubuttons_text { background: $tabbg; } +td.LC_zero_height { + line-height: 0; + cellpadding: 0; +} + table.LC_data_table { border: 1px solid #000000; border-collapse: separate; @@ -6428,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; @@ -6697,7 +8220,8 @@ table.LC_prior_tries td { padding: 6px; } -.LC_answer_unknown { +.LC_answer_unknown, +.LC_answer_warning { background: orange; color: black; padding: 6px; @@ -6779,6 +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; @@ -7027,6 +8557,11 @@ fieldset { /* overflow: hidden; */ } +fieldset#LC_selectuser { + margin: 0; + padding: 0; +} + article.geogebraweb div { margin: 0; } @@ -7570,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; } @@ -7687,6 +9226,26 @@ ul.LC_funclist li { cursor:pointer; } +.LCisDisabled { + cursor: not-allowed; + opacity: 0.5; +} + +a[aria-disabled="true"] { + color: currentColor; + display: inline-block; /* For IE11/ MS Edge bug */ + pointer-events: none; + text-decoration: none; +} + +pre.LC_wordwrap { + white-space: pre-wrap; + white-space: -moz-pre-wrap; + white-space: -pre-wrap; + white-space: -o-pre-wrap; + word-wrap: break-word; +} + /* styles used for response display */ @@ -7799,6 +9358,39 @@ section.role-warning>h1:before { content:url('/adm/daxe/images/section_icons/warning.png'); } +#LC_minitab_header { + float:left; + width:100%; + background:#DAE0D2 url("/res/adm/pages/minitabmenu_bg.gif") repeat-x bottom; + font-size:93%; + line-height:normal; + margin: 0.5em 0 0.5em 0; +} +#LC_minitab_header ul { + margin:0; + padding:10px 10px 0; + list-style:none; +} +#LC_minitab_header li { + float:left; + background:url("/res/adm/pages/minitabmenu_left.gif") no-repeat left top; + margin:0; + padding:0 0 0 9px; +} +#LC_minitab_header a { + display:block; + background:url("/res/adm/pages/minitabmenu_right.gif") no-repeat right top; + padding:5px 15px 4px 6px; +} +#LC_minitab_header #LC_current_minitab { + background-image:url("/res/adm/pages/minitabmenu_left_on.gif"); +} +#LC_minitab_header #LC_current_minitab a { + background-image:url("/res/adm/pages/minitabmenu_right_on.gif"); + padding-bottom:5px; +} + + END } @@ -7819,7 +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) + 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 @@ -7853,7 +9451,7 @@ sub headtag { $inhibitprint = &print_suppression(); } - if (!$args->{'frameset'}) { + if (!$args->{'frameset'} && !$args->{'switchserver'}) { $result .= &Apache::lonhtmlcommon::htmlareaheaders(); } if ($args->{'force_register'} && $env{'request.noversionuri'} !~ m{^/res/adm/pages/}) { @@ -7861,7 +9459,8 @@ sub headtag { } if (!$args->{'no_nav_bar'} && !$args->{'only_body'} - && !$args->{'frameset'}) { + && !$args->{'frameset'} + && !$args->{'switchserver'}) { $result .= &help_menu_js($httphost); $result.=&modal_window(); $result.=&togglebox_script(); @@ -7882,15 +9481,45 @@ sub headtag { } } if (ref($args->{'redirect'})) { - my ($time,$url,$inhibit_continue) = @{$args->{'redirect'}}; - $url = &Apache::lonenc::check_encrypt($url); + my ($time,$url,$inhibit_continue,$to_opener,$skip_enc_check) = @{$args->{'redirect'}}; + if (!$skip_enc_check) { + $url = &Apache::lonenc::check_encrypt($url); + } if (!$inhibit_continue) { $env{'internal.head.redirect'} = $url; } - $result.=<<ADDMETA + $result.=<<"ADDMETA"; <meta http-equiv="pragma" content="no-cache" /> +ADDMETA + if ($to_opener) { + $env{'internal.head.to_opener'} = 1; + my $dest = &js_escape($url); + my $timeout = int($time * 1000); + $result .=<<"ENDJS"; +<script type="text/javascript"> +// <![CDATA[ +function LC_To_Opener() { + var dest = '$dest'; + if (dest != '') { + if (window.opener != null && !window.opener.closed) { + window.opener.location.href=dest; + window.close(); + } else { + window.location.href=dest; + } + } +} +\$(document).ready(function () { + setTimeout('LC_To_Opener()',$timeout); +}); +// ]]> +</script> +ENDJS + } else { + $result.=<<"ADDMETA"; <meta http-equiv="Refresh" content="$time; url=$url" /> ADDMETA + } } else { unless (($args->{'frameset'}) || ($args->{'js_ready'}) || ($args->{'only_body'}) || ($args->{'no_nav_bar'})) { my $requrl = $env{'request.uri'}; @@ -7904,43 +9533,99 @@ ADDMETA my $dom_in_use = $Apache::lonnet::perlvar{'lonDefDomain'}; unless (&Apache::lonnet::allowed('mau',$dom_in_use)) { my %domdefs = &Apache::lonnet::get_domain_defaults($dom_in_use); + my $lonhost = $Apache::lonnet::perlvar{'lonHostID'}; + my ($offload,$offloadoth); if (ref($domdefs{'offloadnow'}) eq 'HASH') { - my $lonhost = $Apache::lonnet::perlvar{'lonHostID'}; if ($domdefs{'offloadnow'}{$lonhost}) { - my $newserver = &Apache::lonnet::spareserver(30000,undef,1,$dom_in_use); - if (($newserver) && ($newserver ne $lonhost)) { - my $numsec = 5; - my $timeout = $numsec * 1000; - my ($newurl,$locknum,%locks,$msg); - if ($env{'request.role.adv'}) { - ($locknum,%locks) = &Apache::lonnet::get_locks(); + $offload = 1; + if (($env{'user.domain'} ne '') && ($env{'user.domain'} ne $dom_in_use) && + (!(($env{'user.name'} eq 'public') && ($env{'user.domain'} eq 'public')))) { + unless (&Apache::lonnet::shared_institution($env{'user.domain'})) { + $offloadoth = 1; + $dom_in_use = $env{'user.domain'}; } - my $disable_submit = 0; - if ($requrl =~ /$LONCAPA::assess_re/) { - $disable_submit = 1; + } + } + } + unless ($offload) { + if (ref($domdefs{'offloadoth'}) eq 'HASH') { + if ($domdefs{'offloadoth'}{$lonhost}) { + if (($env{'user.domain'} ne '') && ($env{'user.domain'} ne $dom_in_use) && + (!(($env{'user.name'} eq 'public') && ($env{'user.domain'} eq 'public')))) { + unless (&Apache::lonnet::shared_institution($env{'user.domain'})) { + $offload = 1; + $offloadoth = 1; + $dom_in_use = $env{'user.domain'}; + } } - if ($locknum) { - my @lockinfo = sort(values(%locks)); - $msg = &mt('Once the following tasks are complete: ')."\\n". - join(", ",sort(values(%locks)))."\\n". - &mt('your session will be transferred to a different server, after you click "Roles".'); + } + } + } + if ($offload) { + my $newserver = &Apache::lonnet::spareserver(undef,30000,undef,1,$dom_in_use); + if (($newserver eq '') && ($offloadoth)) { + my @domains = &Apache::lonnet::current_machine_domains(); + if (($dom_in_use ne '') && (!grep(/^\Q$dom_in_use\E$/,@domains))) { + ($newserver) = &Apache::lonnet::choose_server($dom_in_use); + } + } + if (($newserver) && ($newserver ne $lonhost)) { + my $numsec = 5; + my $timeout = $numsec * 1000; + my ($newurl,$locknum,%locks,$msg); + if ($env{'request.role.adv'}) { + ($locknum,%locks) = &Apache::lonnet::get_locks(); + } + my $disable_submit = 0; + if ($requrl =~ /$LONCAPA::assess_re/) { + $disable_submit = 1; + } + if ($locknum) { + my @lockinfo = sort(values(%locks)); + $msg = &mt('Once the following tasks are complete:')." \n". + join(", ",sort(values(%locks)))."\n"; + if (&show_course()) { + $msg .= &mt('your session will be transferred to a different server, after you click "Courses".'); } else { - if (($requrl =~ m{^/res/}) && ($env{'form.submitted'} =~ /^part_/)) { - $msg = &mt('Your LON-CAPA submission has been recorded')."\\n"; - } - $msg .= &mt('Your current LON-CAPA session will be transferred to a different server in [quant,_1,second].',$numsec); - $newurl = '/adm/switchserver?otherserver='.$newserver; - if (($env{'request.role'}) && ($env{'request.role'} ne 'cm')) { - $newurl .= '&role='.$env{'request.role'}; + $msg .= &mt('your session will be transferred to a different server, after you click "Roles".'); + } + } else { + if (($requrl =~ m{^/res/}) && ($env{'form.submitted'} =~ /^part_/)) { + $msg = &mt('Your LON-CAPA submission has been recorded')."\n"; + } + $msg .= &mt('Your current LON-CAPA session will be transferred to a different server in [quant,_1,second].',$numsec); + $newurl = '/adm/switchserver?otherserver='.$newserver; + if (($env{'request.role'}) && ($env{'request.role'} ne 'cm')) { + $newurl .= '&role='.$env{'request.role'}; + } + if ($env{'request.symb'}) { + my $shownsymb = &Apache::lonenc::check_encrypt($env{'request.symb'}); + if ($shownsymb =~ m{^/enc/}) { + my $reqdmajor = 2; + my $reqdminor = 11; + my $reqdsubminor = 3; + my $newserverrev = &Apache::lonnet::get_server_loncaparev('',$newserver); + my $remoterev = &Apache::lonnet::get_server_loncaparev(undef,$newserver); + my ($major,$minor,$subminor) = ($remoterev =~ /^\'?(\d+)\.(\d+)\.(\d+|)[\w.\-]+\'?$/); + if (($major eq '' && $minor eq '') || + (($reqdmajor > $major) || (($reqdmajor == $major) && ($reqdminor > $minor)) || + (($reqdmajor == $major) && ($reqdminor == $minor) && (($subminor eq '') || + ($reqdsubminor > $subminor))))) { + undef($shownsymb); + } } - if ($env{'request.symb'}) { - $newurl .= '&symb='.$env{'request.symb'}; - } else { - $newurl .= '&origurl='.$requrl; + if ($shownsymb) { + &js_escape(\$shownsymb); + $newurl .= '&symb='.$shownsymb; } + } else { + my $shownurl = &Apache::lonenc::check_encrypt($requrl); + &js_escape(\$shownurl); + $newurl .= '&origurl='.$shownurl; } - &js_escape(\$msg); - $result.=<<OFFLOAD + } + &js_escape(\$msg); + $result.=<<OFFLOAD <meta http-equiv="pragma" content="no-cache" /> <script type="text/javascript"> // <![CDATA[ @@ -7961,7 +9646,6 @@ function LC_Offload_Now() { // ]]> </script> OFFLOAD - } } } } @@ -7972,8 +9656,12 @@ OFFLOAD $title = 'The LearningOnline Network with CAPA'; } if (!$args->{'no_auto_mt_title'}) { $title = &mt($title); } - $result .= '<title> LON-CAPA '.$title.'</title>' - .'<link rel="stylesheet" type="text/css" href="'.$url.'"'; + if ($title =~ /^LON-CAPA\s+/) { + $result .= '<title> '.$title.'</title>'; + } else { + $result .= '<title> LON-CAPA '.$title.'</title>'; + } + $result .= "\n".'<link rel="stylesheet" type="text/css" href="'.$url.'"'; if (!$args->{'frameset'}) { $result .= ' /'; } @@ -7988,9 +9676,10 @@ 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"; return $result.'</head>'; } @@ -8059,7 +9748,8 @@ sub print_suppression { } my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; - my $blocked = &blocking_status('printout',$cnum,$cdom,undef,1); + my $clientip = &Apache::lonnet::get_requestor_ip(); + my $blocked = &blocking_status('printout',$clientip,$cnum,$cdom,undef,1); if ($blocked) { my $checkrole = "cm./$cdom/$cnum"; if ($env{'request.course.sec'} ne '') { @@ -8170,8 +9860,23 @@ $args - additional optional args support no_auto_mt_title -> prevent &mt()ing the title arg bread_crumbs -> Array containing breadcrumbs bread_crumbs_component -> if exists show it as headline else show only the breadcrumbs + bread_crumbs_style -> breadcrumbs are contained within <div id="LC_breadcrumbs">, + and &standard_css() contains CSS for #LC_breadcrumbs, if you want + to override those values, or add to them, specify the value to + include in the style attribute to include in the div tag by using + bread_crumbs_style (e.g., overflow: visible) + bread_crumbs_nomenu -> if true will pass false as the value of $menulink + to lonhtmlcommon::breadcrumbs group -> includes the current group, if page is for a - specific group + specific group + use_absolute -> for request for external resource or syllabus, this + will contain https://<hostname> if server uses + https (as per hosts.tab), but request is for http + hostname -> hostname, originally from $r->hostname(), (optional). + links_disabled -> Links in primary and secondary menus are disabled + (Can enable them once page has loaded - see lonroles.pm + for an example). + links_target -> Target for links, e.g., _parent (optional). =back @@ -8184,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'}, @@ -8202,7 +9978,8 @@ sub start_page { $args->{'only_body'}, $args->{'domain'}, $args->{'force_register'}, $args->{'no_nav_bar'}, $args->{'bgcolor'}, $args, - \@advtools); + \@advtools,$ltiscope,$ltiuri,\%ltimenu,$menucoll, + \%menu,\$showncrumbs); } } @@ -8224,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') { @@ -8235,16 +10013,34 @@ sub start_page { if (@advtools > 0) { &Apache::lonmenu::advtools_crumbs(@advtools); } - + my $menulink; + # if arg: bread_crumbs_nomenu is true pass 0 as $menulink item. + if ((exists($args->{'bread_crumbs_nomenu'})) || + ($ltiscope eq 'map') || ($ltiscope eq 'resource') || + ((($args->{'crstype'} eq 'Placement') || (($env{'request.course.id'}) && + ($env{'course.'.$env{'request.course.id'}.'.type'} eq 'Placement'))) && + (!$env{'request.role.adv'}))) { + $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'}); - } elsif ($args->{'crstype'} eq 'Placement') { - $result .= &Apache::lonhtmlcommon::breadcrumbs('','','','','','','','','', - $args->{'crstype'}); + $result .= &Apache::lonhtmlcommon::breadcrumbs($args->{'bread_crumbs_component'}, + '',$menulink,'', + $args->{'bread_crumbs_style'}); } else { - $result .= &Apache::lonhtmlcommon::breadcrumbs(); + $result .= &Apache::lonhtmlcommon::breadcrumbs('','',$menulink,'', + $args->{'bread_crumbs_style'}); } + } } return $result; } @@ -8281,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"> @@ -8340,7 +10277,7 @@ var modalWindow = { }; var openMyModal = function(source,width,height,scrolling,transparency,style) { - source = source.replace("'","'"); + source = source.replace(/'/g,"'"); modalWindow.windowId = "myModal"; modalWindow.width = width; modalWindow.height = height; @@ -8365,13 +10302,20 @@ sub modal_link { $target_attr = 'target="'.$target.'"'; } return <<"ENDLINK"; -<a href="$link" $target_attr title="$title" onclick="javascript:openMyModal('$link',$width,$height,'$scrolling','$transparency','$style'); return false;"> - $linktext</a> +<a href="$link" $target_attr title="$title" onclick="javascript:openMyModal('$link',$width,$height,'$scrolling','$transparency','$style'); return false;">$linktext</a> ENDLINK } sub modal_adhoc_script { - my ($funcname,$width,$height,$content)=@_; + my ($funcname,$width,$height,$content,$possmathjax)=@_; + my $mathjax; + if ($possmathjax) { + $mathjax = <<'ENDJAX'; + if (typeof MathJax == 'object') { + MathJax.Hub.Queue(["Typeset",MathJax.Hub]); + } +ENDJAX + } return (<<ENDADHOC); <script type="text/javascript"> // <![CDATA[ @@ -8382,6 +10326,7 @@ sub modal_adhoc_script { modalWindow.height = $height; modalWindow.content = '$content'; modalWindow.open(); + $mathjax }; // ]]> </script> @@ -8389,7 +10334,7 @@ ENDADHOC } sub modal_adhoc_inner { - my ($funcname,$width,$height,$content)=@_; + my ($funcname,$width,$height,$content,$possmathjax)=@_; my $innerwidth=$width-20; $content=&js_ready( &start_page('Dialog',undef,{'only_body'=>1,'bgcolor'=>'#FFFFFF'}). @@ -8398,12 +10343,12 @@ sub modal_adhoc_inner { &end_scrollbox(). &end_page() ); - return &modal_adhoc_script($funcname,$width,$height,$content); + return &modal_adhoc_script($funcname,$width,$height,$content,$possmathjax); } sub modal_adhoc_window { - my ($funcname,$width,$height,$content,$linktext)=@_; - return &modal_adhoc_inner($funcname,$width,$height,$content). + my ($funcname,$width,$height,$content,$linktext,$possmathjax)=@_; + return &modal_adhoc_inner($funcname,$width,$height,$content,$possmathjax). "<a href=\"javascript:$funcname();void(0);\">".$linktext."</a>"; } @@ -8469,8 +10414,9 @@ sub end_togglebox { } sub LCprogressbar_script { - my ($id)=@_; - return(<<ENDPROGRESS); + my ($id,$number_to_do)=@_; + if ($number_to_do) { + return(<<ENDPROGRESS); <script type="text/javascript"> // <![CDATA[ \$('#progressbar$id').progressbar({ @@ -8483,21 +10429,43 @@ sub LCprogressbar_script { // ]]> </script> ENDPROGRESS + } else { + return(<<ENDPROGRESS); +<script type="text/javascript"> +// <![CDATA[ +\$('#progressbar$id').progressbar({ + value: false, + create: function(event, ui) { + \$('.ui-widget-header', this).css({'background':'#F0F0F0'}); + \$('.ui-progressbar-overlay', this).css({'margin':'0'}); + } +}); +// ]]> +</script> +ENDPROGRESS + } } sub LCprogressbarUpdate_script { return(<<ENDPROGRESSUPDATE); <style type="text/css"> .ui-progressbar { position:relative; } +.progress-label {position: absolute; width: 100%; text-align: center; top: 1px; font-weight: bold; text-shadow: 1px 1px 0 #fff;margin: 0; line-height: 200%; } .pblabel { position: absolute; width: 100%; text-align: center; line-height: 1.9em; } </style> <script type="text/javascript"> // <![CDATA[ var LCprogressTxt='---'; -function LCupdateProgress(percent,progresstext,id) { +function LCupdateProgress(percent,progresstext,id,maxnum) { LCprogressTxt=progresstext; - \$('#progressbar'+id).progressbar('value',percent); + if ((maxnum == '') || (maxnum == undefined) || (maxnum == null)) { + \$('#progressbar'+id).find('.progress-label').text(LCprogressTxt); + } else if (percent === \$('#progressbar'+id).progressbar( "value" )) { + \$('#progressbar'+id).find('.pblabel').text(LCprogressTxt); + } else { + \$('#progressbar'+id).progressbar('value',percent); + } } // ]]> </script> @@ -8509,37 +10477,54 @@ my $LCidcnt; my $LCcurrentid; sub LCprogressbar { - my ($r)=(@_); + my ($r,$number_to_do,$preamble)=@_; $LClastpercent=0; $LCidcnt++; $LCcurrentid=$$.'_'.$LCidcnt; - my $starting=&mt('Starting'); - my $content=(<<ENDPROGBAR); + my ($starting,$content); + if ($number_to_do) { + $starting=&mt('Starting'); + $content=(<<ENDPROGBAR); +$preamble <div id="progressbar$LCcurrentid"> <span class="pblabel">$starting</span> </div> ENDPROGBAR - &r_print($r,$content.&LCprogressbar_script($LCcurrentid)); + } else { + $starting=&mt('Loading...'); + $LClastpercent='false'; + $content=(<<ENDPROGBAR); +$preamble + <div id="progressbar$LCcurrentid"> + <div class="progress-label">$starting</div> + </div> +ENDPROGBAR + } + &r_print($r,$content.&LCprogressbar_script($LCcurrentid,$number_to_do)); } sub LCprogressbarUpdate { - my ($r,$val,$text)=@_; - unless ($val) { - if ($LClastpercent) { - $val=$LClastpercent; - } else { - $val=0; - } + my ($r,$val,$text,$number_to_do)=@_; + if ($number_to_do) { + unless ($val) { + if ($LClastpercent) { + $val=$LClastpercent; + } else { + $val=0; + } + } + if ($val<0) { $val=0; } + if ($val>100) { $val=0; } + $LClastpercent=$val; + unless ($text) { $text=$val.'%'; } + } else { + $val = 'false'; } - if ($val<0) { $val=0; } - if ($val>100) { $val=0; } - $LClastpercent=$val; - unless ($text) { $text=$val.'%'; } $text=&js_ready($text); &r_print($r,<<ENDUPDATE); <script type="text/javascript"> // <![CDATA[ -LCupdateProgress($val,'$text','$LCcurrentid'); +LCupdateProgress($val,'$text','$LCcurrentid','$number_to_do'); // ]]> </script> ENDUPDATE @@ -8724,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)) { @@ -8914,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)) { @@ -9659,8 +11661,24 @@ sub get_secgrprole_info { } sub user_picker { - my ($dom,$srch,$forcenewuser,$caller,$cancreate,$usertype,$context) = @_; + my ($dom,$srch,$forcenewuser,$caller,$cancreate,$usertype,$context,$fixeddom,$noinstd) = @_; my $currdom = $dom; + my @alldoms = &Apache::lonnet::all_domains(); + if (@alldoms == 1) { + my %domsrch = &Apache::lonnet::get_dom('configuration', + ['directorysrch'],$alldoms[0]); + my $domdesc = &Apache::lonnet::domain($alldoms[0],'description'); + my $showdom = $domdesc; + if ($showdom eq '') { + $showdom = $dom; + } + if (ref($domsrch{'directorysrch'}) eq 'HASH') { + if ((!$domsrch{'directorysrch'}{'available'}) && + ($domsrch{'directorysrch'}{'lcavailable'} eq '0')) { + return (&mt('LON-CAPA directory search is not available in domain: [_1]',$showdom),0); + } + } + } my %curr_selected = ( srchin => 'dom', srchby => 'lastname', @@ -9707,7 +11725,23 @@ sub user_picker { ); &html_escape(\%html_lt); &js_escape(\%js_lt); - my $domform = &select_dom_form($currdom,'srchdomain',1,1); + my $domform; + my $allow_blank = 1; + if ($fixeddom) { + $allow_blank = 0; + $domform = &select_dom_form($currdom,'srchdomain',$allow_blank,1,undef,[$currdom]); + } else { + 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">'; my @srchins = ('crs','dom','alc','instd'); @@ -9719,6 +11753,7 @@ sub user_picker { next if ($option eq 'alc'); next if (($option eq 'crs') && ($env{'form.form'} eq 'requestcrs')); next if ($option eq 'crs' && !$env{'request.course.id'}); + next if (($option eq 'instd') && ($noinstd)); if ($curr_selected{'srchin'} eq $option) { $srchinsel .= ' <option value="'.$option.'" selected="selected">'.$html_lt{$option}.'</option>'; @@ -9901,7 +11936,7 @@ END_BLOCK &Apache::lonhtmlcommon::row_closure(1). &Apache::lonhtmlcommon::end_pick_box(). '<br />'; - return $output; + return ($output,1); } sub user_rule_check { @@ -10198,11 +12233,15 @@ sub sorted_inst_types { } sub get_institutional_codes { - my ($settings,$allcourses,$LC_code) = @_; + my ($cdom,$crs,$settings,$allcourses,$LC_code) = @_; # Get complete list of course sections to update my @currsections = (); my @currxlists = (); + my (%unclutteredsec,%unclutteredlcsec); my $coursecode = $$settings{'internal.coursecode'}; + my $crskey = $crs.':'.$coursecode; + @{$unclutteredsec{$crskey}} = (); + @{$unclutteredlcsec{$crskey}} = (); if ($$settings{'internal.sectionnums'} ne '') { @currsections = split(/,/,$$settings{'internal.sectionnums'}); @@ -10213,24 +12252,37 @@ sub get_institutional_codes { } if (@currxlists > 0) { - foreach (@currxlists) { - if (m/^([^:]+):(\w*)$/) { + foreach my $xl (@currxlists) { + if ($xl =~ /^([^:]+):(\w*)$/) { unless (grep/^$1$/,@{$allcourses}) { - push @{$allcourses},$1; + push(@{$allcourses},$1); $$LC_code{$1} = $2; } } } } - + if (@currsections > 0) { - foreach (@currsections) { - if (m/^(\w+):(\w*)$/) { - my $sec = $coursecode.$1; + foreach my $sec (@currsections) { + if ($sec =~ m/^(\w+):(\w*)$/ ) { + my $instsec = $1; my $lc_sec = $2; - unless (grep/^$sec$/,@{$allcourses}) { - push @{$allcourses},$sec; - $$LC_code{$sec} = $lc_sec; + unless (grep/^\Q$instsec\E$/,@{$unclutteredsec{$crskey}}) { + push(@{$unclutteredsec{$crskey}},$instsec); + push(@{$unclutteredlcsec{$crskey}},$lc_sec); + } + } + } + } + + if (@{$unclutteredsec{$crskey}} > 0) { + my %formattedsec = &Apache::lonnet::auto_instsec_reformat($cdom,'clutter',\%unclutteredsec); + if ((ref($formattedsec{$crskey}) eq 'ARRAY') && (ref($unclutteredlcsec{$crskey}) eq 'ARRAY')) { + for (my $i=0; $i<@{$formattedsec{$crskey}}; $i++) { + my $sec = $coursecode.$formattedsec{$crskey}[$i]; + unless (grep/^\Q$sec\E$/,@{$allcourses}) { + push(@{$allcourses},$sec); + $$LC_code{$sec} = $unclutteredlcsec{$crskey}[$i]; } } } @@ -10327,7 +12379,9 @@ reservable_now - ref to hash of student_ Keys in inner hash are: (a) symb: either blank or symb to which slot use is restricted. - (b) endreserve: end date of reservation period. + (b) endreserve: end date of reservation period. + (c) uniqueperiod: start,end dates when slot is to be uniquely + selected. sorted_future - ref to array of student_schedulable slots reservable in the future, ordered by start date of reservation period. @@ -10337,7 +12391,9 @@ 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. =back @@ -10413,6 +12469,10 @@ sub get_future_slots { my $startreserve = $slots{$slot}->{'startreserve'}; my $endreserve = $slots{$slot}->{'endreserve'}; my $symb = $slots{$slot}->{'symb'}; + my $uniqueperiod; + if (ref($slots{$slot}->{'uniqueperiod'}) eq 'ARRAY') { + $uniqueperiod = join(',',@{$slots{$slot}->{'uniqueperiod'}}); + } if (($startreserve < $now) && (!$endreserve || $endreserve > $now)) { my $lastres = $endreserve; @@ -10421,13 +12481,15 @@ sub get_future_slots { } $reservable_now{$slot} = { symb => $symb, - endreserve => $lastres + endreserve => $lastres, + uniqueperiod => $uniqueperiod, }; } elsif (($startreserve > $now) && (!$endreserve || $endreserve > $startreserve)) { $future_reservable{$slot} = { symb => $symb, - startreserve => $startreserve + startreserve => $startreserve, + uniqueperiod => $uniqueperiod, }; } } @@ -10585,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); @@ -10601,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(); @@ -10685,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(.+)$})) { @@ -10727,6 +12818,19 @@ 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} = {}; @@ -10802,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')) { @@ -10840,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) { @@ -10868,6 +12977,8 @@ 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)); @@ -10879,6 +12990,12 @@ sub ask_for_embedded_content { $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+://}); @@ -11492,7 +13609,7 @@ sub modify_html_refs { return; } } - if (open(my $fh,"<$container")) { + if (open(my $fh,'<',$container)) { $content = join('', <$fh>); close($fh); } else { @@ -11557,7 +13674,7 @@ sub modify_html_refs { } } } else { - if (open(my $fh,">$container")) { + if (open(my $fh,'>',$container)) { print $fh $content; close($fh); $output = '<p>'.&mt('Updated [quant,_1,reference] in [_2].', @@ -12074,6 +14191,18 @@ sub decompress_uploaded_file { sub process_decompression { my ($docudom,$docuname,$file,$destination,$dir_root,$hiddenelem) = @_; + unless (($dir_root eq '/userfiles') && ($destination =~ m{^(docs|supplemental)/(default|\d+)/\d+$})) { + return '<p class="LC_error">'.&mt('Not extracted.').'<br />'. + &mt('Unexpected file path.').'</p>'."\n"; + } + unless (($docudom =~ /^$match_domain$/) && ($docuname =~ /^$match_courseid$/)) { + return '<p class="LC_error">'.&mt('Not extracted.').'<br />'. + &mt('Unexpected course context.').'</p>'."\n"; + } + unless ($file eq &Apache::lonnet::clean_filename($file)) { + return '<p class="LC_error">'.&mt('Not extracted.').'<br />'. + &mt('Filename contained unexpected characters.').'</p>'."\n"; + } my ($dir,$error,$warning,$output); if ($file !~ /\.(zip|tar|bz2|gz|tar.gz|tar.bz2|tgz)$/i) { $error = &mt('Filename not a supported archive file type.'). @@ -12108,30 +14237,44 @@ sub process_decompression { } } my $numskip = scalar(@to_skip); - if (($numskip > 0) && - ($numskip == $env{'form.archive_itemcount'})) { + my $numoverwrite = scalar(@to_overwrite); + 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.'); } elsif (!$error) { my ($decompressed,$display); - if ($numskip > 0) { + if (($numskip) || ($numoverwrite)) { my $tempdir = time.'_'.$$.int(rand(10000)); mkdir("$dir/$tempdir",0755); - system("mv $dir/$file $dir/$tempdir/$file"); - ($decompressed,$display) = - &decompress_uploaded_file($file,"$dir/$tempdir"); - foreach my $item (@to_skip) { - if (($item ne '') && ($item !~ /\.\./)) { - if (-f "$dir/$tempdir/$item") { - unlink("$dir/$tempdir/$item"); - } elsif (-d "$dir/$tempdir/$item") { - system("rm -rf $dir/$tempdir/$item"); + if (&File::Copy::move("$dir/$file","$dir/$tempdir/$file")) { + ($decompressed,$display) = + &decompress_uploaded_file($file,"$dir/$tempdir"); + foreach my $item (@to_skip) { + if (($item ne '') && ($item !~ /\.\./)) { + if (-f "$dir/$tempdir/$item") { + unlink("$dir/$tempdir/$item"); + } elsif (-d "$dir/$tempdir/$item") { + &File::Path::remove_tree("$dir/$tempdir/$item",{ safe => 1 }); + } } } + foreach my $item (@to_overwrite) { + if ((-e "$dir/$tempdir/$item") && (-e "$dir/$item")) { + if (($item ne '') && ($item !~ /\.\./)) { + if (-f "$dir/$item") { + unlink("$dir/$item"); + } elsif (-d "$dir/$item") { + &File::Path::remove_tree("$dir/$item",{ safe => 1 }); + } + &File::Copy::move("$dir/$tempdir/$item","$dir/$item"); + } + } + } + if (&File::Copy::move("$dir/$tempdir/$file","$dir/$file")) { + &File::Path::remove_tree("$dir/$tempdir",{ safe => 1 }); + } } - system("mv $dir/$tempdir/* $dir"); - rmdir("$dir/$tempdir"); } else { ($decompressed,$display) = &decompress_uploaded_file($file,$dir); @@ -12149,8 +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) || - ((@to_skip > 0) && (grep(/^\Q$item\E$/,@to_skip)))) { + unless (($item =~ /^\.+$/) || ($item eq $file)) { push(@newitems,$item); if ($dirptr&$testdir) { $is_dir{$item} = 1; @@ -12635,7 +14777,7 @@ END sub process_extracted_files { my ($context,$docudom,$docuname,$destination,$dir_root,$hiddenelem) = @_; my $numitems = $env{'form.archive_count'}; - return unless ($numitems); + return if ((!$numitems) || ($numitems =~ /\D/)); my @ids=&Apache::lonnet::current_machine_ids(); my ($prefix,$pathtocheck,$dir,$ishome,$error,$warning,%toplevelitems,%is_dir, %folders,%containers,%mapinner,%prompttofetch); @@ -12648,7 +14790,7 @@ sub process_extracted_files { } else { $prefix = $Apache::lonnet::perlvar{'lonDocRoot'}; $pathtocheck = "$dir_root/$docudom/$docuname/$destination"; - $dir = "$dir_root/$docudom/$docuname"; + $dir = "$dir_root/$docudom/$docuname"; } my $currdir = "$dir_root/$destination"; (my $docstype,$mapinner{'0'}) = ($destination =~ m{^(docs|supplemental)/(\w+)/}); @@ -12737,7 +14879,9 @@ sub process_extracted_files { '.'.$containers{$outer},1,1); $newseqid{$i} = $newidx; unless ($errtext) { - $result .= '<li>'.&mt('Folder: [_1] added to course',$docstitle).'</li>'."\n"; + $result .= '<li>'.&mt('Folder: [_1] added to course', + &HTML::Entities::encode($docstitle,'<>&"')). + '</li>'."\n"; } } } else { @@ -12746,38 +14890,49 @@ sub process_extracted_files { my $url = '/uploaded/'.$docudom.'/'.$docuname.'/'. $docstype.'/'.$mapinner{$outer}.'/'.$newidx.'/'. $title; - if (!-e "$prefix$dir/$docstype/$mapinner{$outer}") { - mkdir("$prefix$dir/$docstype/$mapinner{$outer}",0755); - } - if (!-e "$prefix$dir/$docstype/$mapinner{$outer}/$newidx") { - mkdir("$prefix$dir/$docstype/$mapinner{$outer}/$newidx"); - } - if (-e "$prefix$dir/$docstype/$mapinner{$outer}/$newidx") { - system("mv $prefix$path $prefix$dir/$docstype/$mapinner{$outer}/$newidx/$title"); - $newdest{$i} = "$prefix$dir/$docstype/$mapinner{$outer}/$newidx"; - unless ($ishome) { - my $fetch = "$newdest{$i}/$title"; - $fetch =~ s/^\Q$prefix$dir\E//; - $prompttofetch{$fetch} = 1; + if (($outer !~ /\D/) && + (($mapinner{$outer} eq 'default') || ($mapinner{$outer} !~ /\D/)) && + ($newidx !~ /\D/)) { + if (!-e "$prefix$dir/$docstype/$mapinner{$outer}") { + mkdir("$prefix$dir/$docstype/$mapinner{$outer}",0755); } - } - $LONCAPA::map::resources[$newidx]= - $docstitle.':'.$url.':false:normal:res'; - push(@LONCAPA::map::order, $newidx); - my ($outtext,$errtext)= - &LONCAPA::map::storemap('/uploaded/'.$docudom.'/'. - $docuname.'/'.$folders{$outer}. - '.'.$containers{$outer},1,1); - unless ($errtext) { - if (-e "$prefix$dir/$docstype/$mapinner{$outer}/$newidx/$title") { - $result .= '<li>'.&mt('File: [_1] added to course',$docstitle).'</li>'."\n"; + if (!-e "$prefix$dir/$docstype/$mapinner{$outer}/$newidx") { + mkdir("$prefix$dir/$docstype/$mapinner{$outer}/$newidx"); + } + if (-e "$prefix$dir/$docstype/$mapinner{$outer}/$newidx") { + if (rename("$prefix$path","$prefix$dir/$docstype/$mapinner{$outer}/$newidx/$title")) { + $newdest{$i} = "$prefix$dir/$docstype/$mapinner{$outer}/$newidx"; + unless ($ishome) { + my $fetch = "$newdest{$i}/$title"; + $fetch =~ s/^\Q$prefix$dir\E//; + $prompttofetch{$fetch} = 1; + } + } } + $LONCAPA::map::resources[$newidx]= + $docstitle.':'.$url.':false:normal:res'; + push(@LONCAPA::map::order, $newidx); + my ($outtext,$errtext)= + &LONCAPA::map::storemap('/uploaded/'.$docudom.'/'. + $docuname.'/'.$folders{$outer}. + '.'.$containers{$outer},1,1); + unless ($errtext) { + if (-e "$prefix$dir/$docstype/$mapinner{$outer}/$newidx/$title") { + $result .= '<li>'.&mt('File: [_1] added to course', + &HTML::Entities::encode($docstitle,'<>&"')). + '</li>'."\n"; + } + } + } else { + $warning .= &mt('Item extracted from archive: [_1] has unexpected path.', + &HTML::Entities::encode($path,'<>&"')).'<br />'; } } } } } else { - $warning .= &mt('Item extracted from archive: [_1] has unexpected path.',$path).'<br />'; + $warning .= &mt('Item extracted from archive: [_1] has unexpected path.', + &HTML::Entities::encode($path,'<>&"')).'<br />'; } } for (my $i=1; $i<=$numitems; $i++) { @@ -12838,7 +14993,9 @@ sub process_extracted_files { } if ($fullpath ne '') { if (-e "$prefix$path") { - system("mv $prefix$path $fullpath/$title"); + unless (rename("$prefix$path","$fullpath/$title")) { + $warning .= &mt('Failed to rename dependency').'<br />'; + } } if (-e "$fullpath/$title") { my $showpath; @@ -12847,21 +15004,26 @@ sub process_extracted_files { } else { $showpath = "/$title"; } - $result .= '<li>'.&mt('[_1] included as a dependency',$showpath).'</li>'."\n"; - } - unless ($ishome) { - my $fetch = "$fullpath/$title"; - $fetch =~ s/^\Q$prefix$dir\E//; - $prompttofetch{$fetch} = 1; + $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//; + $prompttofetch{$fetch} = 1; + } } } } } elsif ($env{'form.archive_'.$referrer{$i}} eq 'discard') { $warning .= &mt('[_1] is a dependency of [_2], which was discarded.', - $path,$env{'form.archive_content_'.$referrer{$i}}).'<br />'; + &HTML::Entities::encode($path,'<>&"'), + &HTML::Entities::encode($env{'form.archive_content_'.$referrer{$i}},'<>&"')). + '<br />'; } } else { - $warning .= &mt('Item extracted from archive: [_1] has unexpected path.',$path).'<br />'; + $warning .= &mt('Item extracted from archive: [_1] has unexpected path.', + &HTML::Entities::encode($path)).'<br />'; } } if (keys(%todelete)) { @@ -13135,12 +15297,15 @@ sub upfile_store { $env{'form.upfile'}=~s/\n+/\n/gs; $env{'form.upfile'}=~s/\n+$//gs; - my $datatoken=$env{'user.name'}.'_'.$env{'user.domain'}. - '_enroll_'.$env{'request.course.id'}.'_'.time.'_'.$$; + my $datatoken = &valid_datatoken($env{'user.name'}.'_'.$env{'user.domain'}. + '_enroll_'.$env{'request.course.id'}.'_'. + time.'_'.$$); + return if ($datatoken eq ''); + { my $datafile = $r->dir_config('lonDaemons'). '/tmp/'.$datatoken.'.tmp'; - if ( open(my $fh,">$datafile") ) { + if ( open(my $fh,'>',$datafile) ) { print $fh $env{'form.upfile'}; close($fh); } @@ -13150,21 +15315,22 @@ sub upfile_store { =pod -=item * &load_tmp_file($r) +=item * &load_tmp_file($r,$datatoken) Load uploaded file from tmp, $r should be the HTTP Request object, -needs $env{'form.datatoken'}, +$datatoken is the name to assign to the temporary file. sets $env{'form.upfile'} to the contents of the file =cut sub load_tmp_file { - my $r=shift; + my ($r,$datatoken) = @_; + return if ($datatoken eq ''); my @studentdata=(); { my $studentfile = $r->dir_config('lonDaemons'). - '/tmp/'.$env{'form.datatoken'}.'.tmp'; - if ( open(my $fh,"<$studentfile") ) { + '/tmp/'.$datatoken.'.tmp'; + if ( open(my $fh,'<',$studentfile) ) { @studentdata=<$fh>; close($fh); } @@ -13172,6 +15338,14 @@ sub load_tmp_file { $env{'form.upfile'}=join('',@studentdata); } +sub valid_datatoken { + my ($datatoken) = @_; + if ($datatoken =~ /^$match_username\_$match_domain\_enroll_(|$match_domain\_$match_courseid)\_\d+_\d+$/) { + return $datatoken; + } + return; +} + =pod =item * &upfile_record_sep() @@ -13612,7 +15786,7 @@ sub DrawBarGraph { @Labels = @$labels; } else { for (my $i=0;$i<@{$Values[0]};$i++) { - push (@Labels,$i+1); + push(@Labels,$i+1); } } # @@ -14058,7 +16232,14 @@ requestsmail, updatesmail, or idconflict defdom (domain for which to retrieve configuration settings), origmail (scalar - email address of recipient from loncapa.conf, -i.e., predates configuration by DC via domainprefs.pm +i.e., predates configuration by DC via domainprefs.pm + +$requname username of requester (if mailing type is helpdeskmail) + +$requdom domain of requester (if mailing type is helpdeskmail) + +$reqemail e-mail address of requester (if mailing type is helpdeskmail) + Returns: comma separated list of addresses to which to send e-mail. @@ -14069,11 +16250,11 @@ Returns: comma separated list of address ############################################################ ############################################################ sub build_recipient_list { - my ($defmail,$mailing,$defdom,$origmail) = @_; + my ($defmail,$mailing,$defdom,$origmail,$requname,$requdom,$reqemail) = @_; my @recipients; - my $otheremails; + my ($otheremails,$lastresort,$allbcc,$addtext); my %domconfig = - &Apache::lonnet::get_dom('configuration',['contacts'],$defdom); + &Apache::lonnet::get_dom('configuration',['contacts'],$defdom); if (ref($domconfig{'contacts'}) eq 'HASH') { if (exists($domconfig{'contacts'}{$mailing})) { if (ref($domconfig{'contacts'}{$mailing}) eq 'HASH') { @@ -14085,14 +16266,183 @@ sub build_recipient_list { push(@recipients,$addr); } } - $otheremails = $domconfig{'contacts'}{$mailing}{'others'}; + } + $otheremails = $domconfig{'contacts'}{$mailing}{'others'}; + if ($mailing eq 'helpdeskmail') { + if ($domconfig{'contacts'}{$mailing}{'bcc'}) { + my @bccs = split(/,/,$domconfig{'contacts'}{$mailing}{'bcc'}); + my @ok_bccs; + foreach my $bcc (@bccs) { + $bcc =~ s/^\s+//g; + $bcc =~ s/\s+$//g; + if ($bcc =~ m/^[^\@]+\@[^\@]+$/) { + if (!(grep(/^\Q$bcc\E$/,@ok_bccs))) { + push(@ok_bccs,$bcc); + } + } + } + if (@ok_bccs > 0) { + $allbcc = join(', ',@ok_bccs); + } + } + $addtext = $domconfig{'contacts'}{$mailing}{'include'}; } } } elsif ($origmail ne '') { - push(@recipients,$origmail); + $lastresort = $origmail; + } + if ($mailing eq 'helpdeskmail') { + if ((ref($domconfig{'contacts'}{'overrides'}) eq 'HASH') && + (keys(%{$domconfig{'contacts'}{'overrides'}}))) { + my ($inststatus,$inststatus_checked); + if (($env{'user.name'} ne '') && ($env{'user.domain'} ne '') && + ($env{'user.domain'} ne 'public')) { + $inststatus_checked = 1; + $inststatus = $env{'environment.inststatus'}; + } + unless ($inststatus_checked) { + if (($requname ne '') && ($requdom ne '')) { + if (($requname =~ /^$match_username$/) && + ($requdom =~ /^$match_domain$/) && + (&Apache::lonnet::domain($requdom))) { + my $requhome = &Apache::lonnet::homeserver($requname, + $requdom); + unless ($requhome eq 'no_host') { + my %userenv = &Apache::lonnet::userenvironment($requdom,$requname,'inststatus'); + $inststatus = $userenv{'inststatus'}; + $inststatus_checked = 1; + } + } + } + } + unless ($inststatus_checked) { + if ($reqemail =~ /^[^\@]+\@[^\@]+$/) { + my %srch = (srchby => 'email', + srchdomain => $defdom, + srchterm => $reqemail, + srchtype => 'exact'); + my %srch_results = &Apache::lonnet::usersearch(\%srch); + foreach my $uname (keys(%srch_results)) { + if (ref($srch_results{$uname}{'inststatus'}) eq 'ARRAY') { + $inststatus = join(',',@{$srch_results{$uname}{'inststatus'}}); + $inststatus_checked = 1; + last; + } + } + unless ($inststatus_checked) { + my ($dirsrchres,%srch_results) = &Apache::lonnet::inst_directory_query(\%srch); + if ($dirsrchres eq 'ok') { + foreach my $uname (keys(%srch_results)) { + if (ref($srch_results{$uname}{'inststatus'}) eq 'ARRAY') { + $inststatus = join(',',@{$srch_results{$uname}{'inststatus'}}); + $inststatus_checked = 1; + last; + } + } + } + } + } + } + if ($inststatus ne '') { + foreach my $status (split(/\:/,$inststatus)) { + if (ref($domconfig{'contacts'}{'overrides'}{$status}) eq 'HASH') { + my @contacts = ('adminemail','supportemail'); + foreach my $item (@contacts) { + if ($domconfig{'contacts'}{'overrides'}{$status}{$item}) { + my $addr = $domconfig{'contacts'}{'overrides'}{$status}; + if (!grep(/^\Q$addr\E$/,@recipients)) { + push(@recipients,$addr); + } + } + } + $otheremails = $domconfig{'contacts'}{'overrides'}{$status}{'others'}; + if ($domconfig{'contacts'}{'overrides'}{$status}{'bcc'}) { + my @bccs = split(/,/,$domconfig{'contacts'}{'overrides'}{$status}{'bcc'}); + my @ok_bccs; + foreach my $bcc (@bccs) { + $bcc =~ s/^\s+//g; + $bcc =~ s/\s+$//g; + if ($bcc =~ m/^[^\@]+\@[^\@]+$/) { + if (!(grep(/^\Q$bcc\E$/,@ok_bccs))) { + push(@ok_bccs,$bcc); + } + } + } + if (@ok_bccs > 0) { + $allbcc = join(', ',@ok_bccs); + } + } + $addtext = $domconfig{'contacts'}{'overrides'}{$status}{'include'}; + last; + } + } + } + } } } elsif ($origmail ne '') { - push(@recipients,$origmail); + $lastresort = $origmail; + } + if (($mailing eq 'helpdeskmail') && ($lastresort ne '')) { + unless (grep(/^\Q$defdom\E$/,&Apache::lonnet::current_machine_domains())) { + my $lonhost = $Apache::lonnet::perlvar{'lonHostID'}; + my $machinedom = $Apache::lonnet::perlvar{'lonDefDomain'}; + my %what = ( + perlvar => 1, + ); + my $primary = &Apache::lonnet::domain($defdom,'primary'); + if ($primary) { + my $gotaddr; + my ($result,$returnhash) = + &Apache::lonnet::get_remote_globals($primary,{ perlvar => 1 }); + if (($result eq 'ok') && (ref($returnhash) eq 'HASH')) { + if ($returnhash->{'lonSupportEMail'} =~ /^[^\@]+\@[^\@]+$/) { + $lastresort = $returnhash->{'lonSupportEMail'}; + $gotaddr = 1; + } + } + unless ($gotaddr) { + my $uintdom = &Apache::lonnet::internet_dom($primary); + my $intdom = &Apache::lonnet::internet_dom($lonhost); + unless ($uintdom eq $intdom) { + my %domconfig = + &Apache::lonnet::get_dom('configuration',['contacts'],$machinedom); + if (ref($domconfig{'contacts'}) eq 'HASH') { + if (ref($domconfig{'contacts'}{'otherdomsmail'}) eq 'HASH') { + my @contacts = ('adminemail','supportemail'); + foreach my $item (@contacts) { + if ($domconfig{'contacts'}{'otherdomsmail'}{$item}) { + my $addr = $domconfig{'contacts'}{$item}; + if (!grep(/^\Q$addr\E$/,@recipients)) { + push(@recipients,$addr); + } + } + } + if ($domconfig{'contacts'}{'otherdomsmail'}{'others'}) { + $otheremails = $domconfig{'contacts'}{'otherdomsmail'}{'others'}; + } + if ($domconfig{'contacts'}{'otherdomsmail'}{'bcc'}) { + my @bccs = split(/,/,$domconfig{'contacts'}{'otherdomsmail'}{'bcc'}); + my @ok_bccs; + foreach my $bcc (@bccs) { + $bcc =~ s/^\s+//g; + $bcc =~ s/\s+$//g; + if ($bcc =~ m/^[^\@]+\@[^\@]+$/) { + if (!(grep(/^\Q$bcc\E$/,@ok_bccs))) { + push(@ok_bccs,$bcc); + } + } + } + if (@ok_bccs > 0) { + $allbcc = join(', ',@ok_bccs); + } + } + $addtext = $domconfig{'contacts'}{'otherdomsmail'}{'include'}; + } + } + } + } + } + } } if (defined($defmail)) { if ($defmail ne '') { @@ -14112,8 +16462,21 @@ sub build_recipient_list { } } } - my $recipientlist = join(',',@recipients); - return $recipientlist; + if ($mailing eq 'helpdeskmail') { + if ((!@recipients) && ($lastresort ne '')) { + push(@recipients,$lastresort); + } + } elsif ($lastresort ne '') { + if (!grep(/^\Q$lastresort\E$/,@recipients)) { + push(@recipients,$lastresort); + } + } + my $recipientlist = join(',',@recipients); + if (wantarray) { + return ($recipientlist,$allbcc,$addtext); + } else { + return $recipientlist; + } } ############################################################ @@ -14133,6 +16496,8 @@ Inputs: from - Sender's email address +replyto - Reply-To email address + to - Email address of recipient subject - Subject of email @@ -14143,8 +16508,6 @@ cc_string - Carbon copy email ad bcc - Blind carbon copy email address -type - File type of attachment - attachment_path - Path of file to be attached file_name - Name of file to be attached @@ -14161,8 +16524,9 @@ attachment_text - The body of an attac ############################################################ sub mime_email { - my ($from, $to, $subject, $body, $cc_string, $bcc, $attachment_path, - $file_name, $attachment_text) = @_; + my ($from,$replyto,$to,$subject,$body,$cc_string,$bcc,$attachment_path, + $file_name,$attachment_text) = @_; + my $msg = MIME::Lite->new( From => $from, To => $to, @@ -14170,6 +16534,9 @@ sub mime_email { Type =>'TEXT', Data => $body, ); + if ($replyto ne '') { + $msg->add("Reply-To" => $replyto); + } if ($cc_string ne '') { $msg->add("Cc" => $cc_string); } @@ -14285,6 +16652,8 @@ jsarray (reference to array of categorie subcats (reference to hash of arrays containing all subcategories within each category, -recursive) +maxd (reference to hash used to hold max depth for all top-level categories). + Returns: nothing Side effects: populates trails and allitems hash references. @@ -14292,7 +16661,7 @@ Side effects: populates trails and allit =cut sub extract_categories { - my ($categories,$cats,$trails,$allitems,$idx,$jsarray,$subcats) = @_; + my ($categories,$cats,$trails,$allitems,$idx,$jsarray,$subcats,$maxd) = @_; if (ref($categories) eq 'HASH') { &gather_categories($categories,$cats,$idx,$jsarray); if (ref($cats->[0]) eq 'ARRAY') { @@ -14320,12 +16689,15 @@ sub extract_categories { if (ref($subcats) eq 'HASH') { push(@{$subcats->{$item}},&escape($category).':'.&escape($name).':1'); } - &recurse_categories($cats,2,$category,$trails,$allitems,\@parents,$subcats); + &recurse_categories($cats,2,$category,$trails,$allitems,\@parents,$subcats,$maxd); } } else { if (ref($subcats) eq 'HASH') { $subcats->{$item} = []; } + if (ref($maxd) eq 'HASH') { + $maxd->{$name} = 1; + } } } } @@ -14363,13 +16735,13 @@ Side effects: populates trails and allit =cut sub recurse_categories { - my ($cats,$depth,$category,$trails,$allitems,$parents,$subcats) = @_; + my ($cats,$depth,$category,$trails,$allitems,$parents,$subcats,$maxd) = @_; my $shallower = $depth - 1; if (ref($cats->[$depth]{$category}) eq 'ARRAY') { for (my $k=0; $k<@{$cats->[$depth]{$category}}; $k++) { my $name = $cats->[$depth]{$category}[$k]; my $item = &escape($category).':'.&escape($parents->[-1]).':'.$shallower; - my $trailstr = join(' -> ',(@{$parents},$category)); + my $trailstr = join(' » ',(@{$parents},$category)); if ($allitems->{$item} eq '') { push(@{$trails},$trailstr); $allitems->{$item} = scalar(@{$trails})-1; @@ -14390,16 +16762,21 @@ sub recurse_categories { } } &recurse_categories($cats,$deeper,$name,$trails,$allitems,$parents, - $subcats); + $subcats,$maxd); pop(@{$parents}); } } else { my $item = &escape($category).':'.&escape($parents->[-1]).':'.$shallower; - my $trailstr = join(' -> ',(@{$parents},$category)); + my $trailstr = join(' » ',(@{$parents},$category)); if ($allitems->{$item} eq '') { push(@{$trails},$trailstr); $allitems->{$item} = scalar(@{$trails})-1; } + if (ref($maxd) eq 'HASH') { + if ($depth > $maxd->{$parents->[0]}) { + $maxd->{$parents->[0]} = $depth; + } + } } return; } @@ -14420,16 +16797,19 @@ currcat - scalar with an & separated lis type - scalar contains course type (Course or Community). +disabled - scalar (optional) contains disabled="disabled" if input elements are + to be readonly (e.g., Domain Helpdesk role viewing course settings). + Returns: $output (markup to be displayed) =cut sub assign_categories_table { - my ($cathash,$currcat,$type) = @_; + my ($cathash,$currcat,$type,$disabled) = @_; my $output; if (ref($cathash) eq 'HASH') { - my (@cats,@trails,%allitems,%idx,@jsarray,@path,$maxdepth); - &extract_categories($cathash,\@cats,\@trails,\%allitems,\%idx,\@jsarray); + my (@cats,@trails,%allitems,%idx,@jsarray,%maxd,@path,$maxdepth); + &extract_categories($cathash,\@cats,\@trails,\%allitems,\%idx,\@jsarray,\%maxd); $maxdepth = scalar(@cats); if (@cats > 0) { my $itemcount = 0; @@ -14465,11 +16845,11 @@ sub assign_categories_table { } $table .= '<tr '.$css_class.'><td><span class="LC_nobreak">'. '<input type="checkbox" name="usecategory" value="'. - $item.'"'.$checked.' />'.$parent_title.'</span>'. + $item.'"'.$checked.$disabled.' />'.$parent_title.'</span>'. '<input type="hidden" name="catname" value="'.$parent.'" /></td>'; my $depth = 1; push(@path,$parent); - $table .= &assign_category_rows($itemcount,\@cats,$depth,$parent,\@path,\@currcategories); + $table .= &assign_category_rows($itemcount,\@cats,$depth,$parent,\@path,\@currcategories,$disabled); pop(@path); $table .= '</tr><tr><td colspan="'.$maxdepth.'" class="LC_row_separator"></td></tr>'; $itemcount ++; @@ -14508,12 +16888,15 @@ path - Array containing all categories b currcategories - reference to array of current categories assigned to the course +disabled - scalar (optional) contains disabled="disabled" if input elements are + to be readonly (e.g., Domain Helpdesk role viewing course settings). + Returns: $output (markup to be displayed). =cut sub assign_category_rows { - my ($itemcount,$cats,$depth,$parent,$path,$currcategories) = @_; + my ($itemcount,$cats,$depth,$parent,$path,$currcategories,$disabled) = @_; my ($text,$name,$item,$chgstr); if (ref($cats) eq 'ARRAY') { my $maxdepth = scalar(@{$cats}); @@ -14536,12 +16919,12 @@ sub assign_category_rows { } $text .= '<tr><td><span class="LC_nobreak"><label>'. '<input type="checkbox" name="usecategory" value="'. - $item.'"'.$checked.' />'.$name.'</label></span>'. + $item.'"'.$checked.$disabled.' />'.$name.'</label></span>'. '<input type="hidden" name="catname" value="'.$name.'" />'. '</td><td>'; if (ref($path) eq 'ARRAY') { push(@{$path},$name); - $text .= &assign_category_rows($itemcount,$cats,$deeper,$name,$path,$currcategories); + $text .= &assign_category_rows($itemcount,$cats,$deeper,$name,$path,$currcategories,$disabled); pop(@{$path}); } $text .= '</td></tr>'; @@ -14564,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"; @@ -14604,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"; @@ -14640,8 +17034,9 @@ sub commit_studentrole { } $oldsecurl = $uurl; $expire_role_result = - &Apache::lonnet::assignrole($udom,$uname,$uurl,'st',$now,'','',$context); - if ($env{'request.course.sec'} ne '') { + &Apache::lonnet::assignrole($udom,$uname,$uurl,'st',$now, + '','','',$context,$othdomby,$requester); + if ($env{'request.course.sec'} ne '') { if ($expire_role_result eq 'refused') { my @roles = ('st'); my @statuses = ('previous'); @@ -14667,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 '') { @@ -14752,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') { @@ -14760,19 +17157,41 @@ 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'}) && + if (($env{'request.role.domain'} eq $args->{'clonedomain'}) && (&Apache::lonnet::allowed('ccc',$env{'request.role.domain'}))) { $can_clone = 1; } else { @@ -14858,19 +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; + my ($args,$logmsg,$courseid,$crsudom,$crsunum,$udom,$uname,$context, + $cnum,$category,$coderef,$callercontext,$user_lh) = @_; + my ($outcome,$msgref,$clonemsgref); my $linefeed = '<br />'."\n"; if ($context eq 'auto') { $linefeed = "\n"; @@ -14879,18 +17313,11 @@ 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); } } @@ -14913,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]',$showncrstype,$$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); } # @@ -14930,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]',$showncrstype,$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 @@ -14971,8 +17417,7 @@ sub construct_course { 'plc.users.denied', 'hidefromcat', 'checkforpriv', - 'categories', - 'internal.uniquecode'], + 'categories'], $$crsudom,$$crsunum); if ($args->{'textbook'}) { $cenv{'internal.textbook'} = $args->{'textbook'}; @@ -14987,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'}; } @@ -15008,6 +17456,7 @@ sub construct_course { $cenv{'internal.defaultcredits'} = $args->{'defaultcredits'}; } my @badclasses = (); # Used to accumulate sections/crosslistings that did not pass classlist access check for course owner. + my @oklcsecs = (); # Used to accumulate LON-CAPA sections for validated institutional sections. if ($args->{'crssections'}) { $cenv{'internal.sectionnums'} = ''; if ($args->{'crssections'} =~ m/,/) { @@ -15021,8 +17470,12 @@ sub construct_course { my $class = $args->{'crscode'}.$sec; my $addcheck = &Apache::lonnet::auto_new_course($$crsunum,$$crsudom,$class,$cenv{'internal.courseowner'}); $cenv{'internal.sectionnums'} .= $item.','; - unless ($addcheck eq 'ok') { - push @badclasses, $class; + if ($addcheck eq 'ok') { + unless (grep(/^\Q$gp\E$/,@oklcsecs)) { + push(@oklcsecs,$gp); + } + } else { + push(@badclasses,$class); } } $cenv{'internal.sectionnums'} =~ s/,$//; @@ -15049,8 +17502,12 @@ sub construct_course { my ($xl,$gp) = split/:/,$item; my $addcheck = &Apache::lonnet::auto_new_course($$crsunum,$$crsudom,$xl,$cenv{'internal.courseowner'}); $cenv{'internal.crosslistings'} .= $item.','; - unless ($addcheck eq 'ok') { - push @badclasses, $xl; + if ($addcheck eq 'ok') { + unless (grep(/^\Q$gp\E$/,@oklcsecs)) { + push(@oklcsecs,$gp); + } + } else { + push(@badclasses,$xl); } } $cenv{'internal.crosslistings'} =~ s/,$//; @@ -15085,32 +17542,63 @@ sub construct_course { } if (@badclasses > 0) { my %lt=&Apache::lonlocal::texthash( - 'tclb' => 'The courses listed below were included as sections or crosslistings affiliated with your new LON-CAPA course. However, if automated course roster updates are enabled for this class, these particular sections/crosslistings will not contribute towards enrollment, because the user identified as the course owner for this LON-CAPA course', - 'dnhr' => 'does not have rights to access enrollment in these classes', - 'adby' => 'as determined by the policies of your institution on access to official classlists' + 'tclb' => 'The courses listed below were included as sections or crosslistings affiliated with your new LON-CAPA course.', + 'howi' => 'However, if automated course roster updates are enabled for this class, these particular sections/crosslistings are not guaranteed to contribute towards enrollment.', + 'itis' => 'It is possible that rights to access enrollment for these classes will be available through assignment of co-owners.', ); - my $badclass_msg = $cenv{'internal.courseowner'}.') - '.$lt{'dnhr'}. - ' ('.$lt{'adby'}.')'; + my $badclass_msg = $lt{'tclb'}.$linefeed.$lt{'howi'}.$linefeed. + &mt('That is because the user identified as the course owner ([_1]) does not have rights to access enrollment in these classes, as determined by the policies of your institution on access to official classlists',$cenv{'internal.courseowner'}).$linefeed.$lt{'itis'}; if ($context eq 'auto') { $outcome .= $badclass_msg.$linefeed; + } else { $outcome .= '<div class="LC_warning">'.$badclass_msg.$linefeed.'<ul>'."\n"; - foreach my $item (@badclasses) { - if ($context eq 'auto') { - $outcome .= " - $item\n"; - } else { - $outcome .= "<li>$item</li>\n"; - } - } + } + foreach my $item (@badclasses) { if ($context eq 'auto') { - $outcome .= $linefeed; + $outcome .= " - $item\n"; } else { - $outcome .= "</ul><br /><br /></div>\n"; + $outcome .= "<li>$item</li>\n"; } + } + if ($context eq 'auto') { + $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 +# 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 +# sections. + if (($can_clone && $cloneid) && + ($cenv{'internal.coursecode'} ne '') && + ($cenv{'grading'} eq 'standard') && + ($cenv{'hidetotals'} ne '') && + ($cenv{'hidetotals'} ne 'all')) { + my @hidesecs; + my $deletehidetotals; + if (@oklcsecs) { + foreach my $sec (split(/,/,$cenv{'hidetotals'})) { + if (grep(/^\Q$sec$/,@oklcsecs)) { + push(@hidesecs,$sec); + } + } + if (@hidesecs) { + $cenv{'hidetotals'} = join(',',@hidesecs); + } else { + $deletehidetotals = 1; + } + } else { + $deletehidetotals = 1; + } + if ($deletehidetotals) { + delete($cenv{'hidetotals'}); + &Apache::lonnet::del('environment',['hidetotals'],$$crsudom,$$crsunum); + } + } $cenv{'internal.autostart'}=$args->{'enrollstart'}; $cenv{'internal.autoend'}=$args->{'enrollend'}; $cenv{'default_enrollment_start_date'}=$args->{'startaccess'}; @@ -15138,6 +17626,9 @@ sub construct_course { if ($args->{'setcontent'}) { $cenv{'question.email'}=$args->{'ccuname'}.':'.$args->{'ccdomain'}; } + if ($args->{'setcomment'}) { + $cenv{'comment.email'}=$args->{'ccuname'}.':'.$args->{'ccdomain'}; + } } if ($args->{'reshome'}) { $cenv{'reshome'}=$args->{'reshome'}.'/'; @@ -15209,19 +17700,23 @@ sub construct_course { # Open all assignments # if ($args->{'openall'}) { + my $opendate = time; + if ($args->{'openallfrom'} =~ /^\d+$/) { + $opendate = $args->{'openallfrom'}; + } my $storeunder=$$crsudom.'_'.$$crsunum.'.0.opendate'; - my %storecontent = ($storeunder => time, + my %storecontent = ($storeunder => $opendate, $storeunder.'.type' => 'date_start'); - - $outcome .= &mt('Opening all assignments').': '.&Apache::lonnet::cput - ('resourcedata',\%storecontent,$$crsudom,$$crsunum).$linefeed; + $outcome .= &mt('All assignments open starting [_1]', + &Apache::lonlocal::locallocaltime($opendate)).': '. + &Apache::lonnet::cput + ('resourcedata',\%storecontent,$$crsudom,$$crsunum).$linefeed; } # # Set first page # unless (($args->{'nonstandard'}) || ($args->{'firstres'} eq 'blank') || ($cloneid)) { - use LONCAPA::map; $outcome .= &mt('Setting first resource').': '; my $map = '/uploaded/'.$$crsudom.'/'.$$crsunum.'/default.sequence'; @@ -15268,7 +17763,7 @@ sub construct_course { ('resourcedata',\%storecontent,$$crsudom,$$crsunum); } - return (1,$outcome); + return (1,$outcome,\@clonemsg); } sub make_unique_code { @@ -15352,13 +17847,14 @@ sub group_term { } sub course_types { - my @types = ('official','unofficial','community','textbook','placement'); + my @types = ('official','unofficial','community','textbook','placement','lti'); my %typename = ( official => 'Official course', unofficial => 'Unofficial course', community => 'Community', textbook => 'Textbook course', placement => 'Placement test', + lti => 'LTI provider', ); return (\@types,\%typename); } @@ -15438,6 +17934,24 @@ sub compare_arrays { return @difference; } +sub lon_status_items { + my %defaults = ( + E => 100, + W => 4, + N => 1, + U => 5, + threshold => 200, + sysmail => 2500, + ); + my %names = ( + E => 'Errors', + W => 'Warnings', + N => 'Notices', + U => 'Unsent', + ); + return (\%defaults,\%names); +} + # -------------------------------------------------------- Initialize user login sub init_user_environment { my ($r, $username, $domain, $authhost, $form, $args) = @_; @@ -15445,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) { @@ -15468,12 +17981,29 @@ 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)) { if ($filename=~/^$username\_\d+\_$domain\_$authhost\.id$/) { - unlink($lonids.'/'.$filename); + if (tie(my %oldenv,'GDBM_File',"$lonids/$filename", + &GDBM_READER(),0640)) { + my $linkedfile; + if (exists($oldenv{'user.linkedenv'})) { + $linkedfile = $oldenv{'user.linkedenv'}; + } + untie(%oldenv); + if (unlink("$lonids/$filename")) { + if ($linkedfile =~ /^[a-f0-9]+_linked$/) { + if (-l "$lonids/$linkedfile.id") { + unlink("$lonids/$linkedfile.id"); + } + } + } + } else { + unlink($lonids.'/'.$filename); + } } } closedir(DIR); @@ -15496,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 @@ -15508,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'})) { @@ -15524,6 +18053,7 @@ sub init_user_environment { # --------------------------------------------------------- Write first profile { + my $ip = &Apache::lonnet::get_requestor_ip($r); my %initial_env = ("user.name" => $username, "user.domain" => $domain, @@ -15542,7 +18072,7 @@ sub init_user_environment { "request.course.sec" => '', "request.role" => 'cm', "request.role.adv" => $env{'user.adv'}, - "request.host" => $ENV{'REMOTE_ADDR'},); + "request.host" => $ip,); if ($form->{'localpath'}) { $initial_env{"browser.localpath"} = $form->{'localpath'}; @@ -15561,39 +18091,92 @@ sub init_user_environment { $env{'user.noloadbalance'} = $lonhost; } - my %is_adv = ( is_adv => $env{'user.adv'} ); - my %domdef; - unless ($domain eq 'public') { - %domdef = &Apache::lonnet::get_domain_defaults($domain); + if ($form->{'noloadbalance'}) { + my @hosts = &Apache::lonnet::current_machine_ids(); + my $hosthere = $form->{'noloadbalance'}; + if (grep(/^\Q$hosthere\E$/,@hosts)) { + $initial_env{"user.noloadbalance"} = $hosthere; + $env{'user.noloadbalance'} = $hosthere; + } } - foreach my $tool ('aboutme','blog','webdav','portfolio') { - $userenv{'availabletools.'.$tool} = - &Apache::lonnet::usertools_access($username,$domain,$tool,'reload', - undef,\%userenv,\%domdef,\%is_adv); - } + unless ($domain eq 'public') { + my %is_adv = ( is_adv => $env{'user.adv'} ); + my %domdef = &Apache::lonnet::get_domain_defaults($domain); - foreach my $crstype ('official','unofficial','community','textbook','placement') { - $userenv{'canrequest.'.$crstype} = - &Apache::lonnet::usertools_access($username,$domain,$crstype, - 'reload','requestcourses', - \%userenv,\%domdef,\%is_adv); - } + 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','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', - \%userenv,\%domdef,\%is_adv); - my %reqauthor = &Apache::lonnet::get('requestauthor',['author_status','author'], - $domain,$username); - my $reqstatus = $reqauthor{'author_status'}; - if ($reqstatus eq 'approval' || $reqstatus eq 'approved') { - if (ref($reqauthor{'author'}) eq 'HASH') { - $userenv{'requestauthorqueued'} = $reqstatus.':'. - $reqauthor{'author'}{'timestamp'}; + $userenv{'canrequest.author'} = + &Apache::lonnet::usertools_access($username,$domain,'requestauthor', + 'reload','requestauthor', + \%userenv,\%domdef,\%is_adv); + my %reqauthor = &Apache::lonnet::get('requestauthor',['author_status','author'], + $domain,$username); + my $reqstatus = $reqauthor{'author_status'}; + 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", @@ -15607,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'}); } @@ -16210,7 +18798,7 @@ sub search_courses { if (ref($courses{$cid}) eq 'HASH') { if (ref($courses{$cid}{roles}) eq 'ARRAY') { if (!grep(/^\Q$courserole\E$/,@{$courses{$cid}{roles}})) { - push (@{$courses{$cid}{roles}},$courserole); + push(@{$courses{$cid}{roles}},$courserole); } } else { $courses{$cid}{roles} = [$courserole]; @@ -16406,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 (); @@ -16433,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})); @@ -16458,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); } @@ -16506,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); @@ -16515,33 +19157,152 @@ 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 ++; } else { - if ($#LONCAPA::map::resources > 0) { - foreach my $res (@LONCAPA::map::resources) { - my ($title,$src,$ext,$type,$status)=split(/\:/,$res); + my @order = @LONCAPA::map::order; + if (@order > 0) { + my @resources = @LONCAPA::map::resources; + my @resparms = @LONCAPA::map::resparms; + foreach my $idx (@order) { + my ($title,$src,$ext,$type,$status)=split(/\:/,$resources[$idx]); if (($src ne '') && ($status eq 'res')) { + my $id = $mapnum.':'.$idx; + push(@{$suppids->{$src}},$id); + if (($hidden) || (&get_supp_parameter($resparms[$idx],'parameter_hiddenresource') =~ /^yes/i)) { + $hiddensupp->{$id} = 1; + } if ($src =~ m{^\Q/uploaded/$cdom/$cnum/\E(supplemental_\d+\.sequence)$}) { - ($numfiles,$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 ($numfiles,$errors); + 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}); + } + } + } +} + +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 { - my ($symb) = @_; - return unless ($symb); + my ($symb,$navmapref) = @_; + return unless ($symb && ref($navmapref)); my ($mapurl,$id,$resurl) = &Apache::lonnet::decode_symb($symb); if ($resurl=~/\.(sequence|page)$/) { $mapurl=$resurl; @@ -16549,9 +19310,11 @@ sub symb_to_docspath { $mapurl=$env{'course.'.$env{'request.course.id'}.'.url'}; } my $mapresobj; - my $navmap = Apache::lonnavmaps::navmap->new(); - if (ref($navmap)) { - $mapresobj = $navmap->getResourceByUrl($mapurl); + unless (ref($$navmapref)) { + $$navmapref = Apache::lonnavmaps::navmap->new(); + } + if (ref($$navmapref)) { + $mapresobj = $$navmapref->getResourceByUrl($mapurl); } $mapurl=~s{^.*/([^/]+)\.(\w+)$}{$1}; my $type=$2; @@ -16561,7 +19324,7 @@ sub symb_to_docspath { if ($pcslist ne '') { foreach my $pc (split(/,/,$pcslist)) { next if ($pc <= 1); - my $res = $navmap->getByMapPc($pc); + my $res = $$navmapref->getByMapPc($pc); if (ref($res)) { my $thisurl = $res->src(); $thisurl=~s{^.*/([^/]+)\.\w+$}{$1}; @@ -16607,11 +19370,72 @@ 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) = @_; + my ($context,$lonhost,$defdom) = @_; my ($output,$error); my ($captcha,$pubkey,$privkey,$version) = - &get_captcha_config($context,$lonhost); + &get_captcha_config($context,$lonhost,$defdom); if ($captcha eq 'original') { $output = &create_captcha(); unless ($output) { @@ -16627,9 +19451,9 @@ sub captcha_display { } sub captcha_response { - my ($context,$lonhost) = @_; + my ($context,$lonhost,$defdom) = @_; my ($captcha_chk,$captcha_error); - my ($captcha,$pubkey,$privkey,$version) = &get_captcha_config($context,$lonhost); + my ($captcha,$pubkey,$privkey,$version) = &get_captcha_config($context,$lonhost,$defdom); if ($captcha eq 'original') { ($captcha_chk,$captcha_error) = &check_captcha(); } elsif ($captcha eq 'recaptcha') { @@ -16641,7 +19465,7 @@ sub captcha_response { } sub get_captcha_config { - my ($context,$lonhost) = @_; + my ($context,$lonhost,$dom_in_effect) = @_; my ($captcha,$pubkey,$privkey,$version,$hashtocheck); my $hostname = &Apache::lonnet::hostname($lonhost); my $serverhomeID = &Apache::lonnet::get_server_homeID($hostname); @@ -16689,7 +19513,28 @@ sub get_captcha_config { } elsif ($domconfhash{$serverhomedom.'.login.captcha'} eq 'original') { $captcha = 'original'; } - } + } elsif ($context eq 'passwords') { + if ($dom_in_effect) { + my %passwdconf = &Apache::lonnet::get_passwdconf($dom_in_effect); + if ($passwdconf{'captcha'} eq 'recaptcha') { + if (ref($passwdconf{'recaptchakeys'}) eq 'HASH') { + $pubkey = $passwdconf{'recaptchakeys'}{'public'}; + $privkey = $passwdconf{'recaptchakeys'}{'private'}; + } + if ($privkey && $pubkey) { + $captcha = 'recaptcha'; + $version = $passwdconf{'recaptchaversion'}; + if ($version ne '2') { + $version = 1; + } + } else { + $captcha = 'original'; + } + } elsif ($passwdconf{'captcha'} ne 'notused') { + $captcha = 'original'; + } + } + } return ($captcha,$pubkey,$privkey,$version); } @@ -16706,13 +19551,17 @@ sub create_captcha { if (-e $Apache::lonnet::perlvar{'lonCaptchaDir'}.'/'.$md5sum.'.png') { $output = '<input type="hidden" name="crypt" value="'.$md5sum.'" />'."\n". + '<span class="LC_nobreak">'. &mt('Type in the letters/numbers shown below').' '. - '<input type="text" size="5" name="code" value="" autocomplete="off" />'. - '<br />'. + '<input type="text" size="5" name="code" value="" autocomplete="new-password" />'. + '</span><br />'. '<img src="'.$captcha_params{'www_output_dir'}.'/'.$md5sum.'.png" alt="captcha" />'; last; } } + if ($output eq '') { + &Apache::lonnet::logthis("Failed to create Captcha code after $tries attempts."); + } return $output; } @@ -16751,7 +19600,8 @@ sub check_captcha { sub create_recaptcha { my ($pubkey,$version) = @_; if ($version >= 2) { - return '<div class="g-recaptcha" data-sitekey="'.$pubkey.'"></div>'; + return '<div class="g-recaptcha" data-sitekey="'.$pubkey.'"></div>'. + '<div style="padding:0;clear:both;margin:0;border:0"></div>'; } else { my $use_ssl; if ($ENV{'SERVER_PORT'} == 443) { @@ -16769,15 +19619,21 @@ sub create_recaptcha { sub check_recaptcha { my ($privkey,$version) = @_; my $captcha_chk; + my $ip = &Apache::lonnet::get_requestor_ip(); if ($version >= 2) { - my $ua = LWP::UserAgent->new; - $ua->timeout(10); my %info = ( secret => $privkey, response => $env{'form.g-recaptcha-response'}, - remoteip => $ENV{'REMOTE_ADDR'}, + 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') { @@ -16791,7 +19647,7 @@ sub check_recaptcha { my $captcha_result = $captcha->check_answer( $privkey, - $ENV{'REMOTE_ADDR'}, + $ip, $env{'form.recaptcha_challenge_field'}, $env{'form.recaptcha_response_field'}, ); @@ -16840,15 +19696,34 @@ 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) { + 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]!~/^error\:/)) { + 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); @@ -16908,6 +19783,339 @@ 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); + return if (($remprotocol eq '') || ($remhost eq '')); + + $remprotocol = lc($remprotocol); + $remhost = lc($remhost); + my $remport = 80; + if ($remprotocol eq 'https') { + $remport = 443; + } + my ($result,$cached) = &Apache::lonnet::is_cached_new('noiframe',$remhost.':'.$remport); + if ($cached) { + unless ($nocache) { + if ($result) { + return 1; + } else { + return 0; + } + } + } + my $uselink; + my $request = new HTTP::Request('HEAD',$url); + 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')); + $secpolicy =~ s/^\s+|\s+$//g; + $xframeop =~ s/^\s+|\s+$//g; + if (($secpolicy ne '') || ($xframeop ne '')) { + my $remotehost = $remprotocol.'://'.$remhost; + my ($origin,$protocol,$port); + if ($ENV{'SERVER_PORT'} =~/^\d+$/) { + $port = $ENV{'SERVER_PORT'}; + } else { + $port = 80; + } + if ($absolute eq '') { + $protocol = 'http:'; + if ($port == 443) { + $protocol = 'https:'; + } + $origin = $protocol.'//'.lc($hostname); + } else { + $origin = lc($absolute); + ($protocol,$hostname) = ($absolute =~ m{^(https?:)//([^/]+)$}); + } + if (($secpolicy) && ($secpolicy =~ /\Qframe-ancestors\E([^;]*)(;|$)/)) { + my $framepolicy = $1; + $framepolicy =~ s/^\s+|\s+$//g; + my @policies = split(/\s+/,$framepolicy); + if (@policies) { + if (grep(/^\Q'none'\E$/,@policies)) { + $uselink = 1; + } else { + $uselink = 1; + if ((grep(/^\Q*\E$/,@policies)) || (grep(/^\Q$protocol\E$/,@policies)) || + (($origin ne '') && (grep(/^\Q$origin\E$/,@policies))) || + (($ip ne '') && (grep(/^\Q$ip\E$/,@policies)))) { + undef($uselink); + } + if ($uselink) { + if (grep(/^\Q'self'\E$/,@policies)) { + if (($origin ne '') && ($remotehost eq $origin)) { + undef($uselink); + } + } + } + if ($uselink) { + my @possok; + if ($ip ne '') { + push(@possok,$ip); + } + my $hoststr = ''; + foreach my $part (reverse(split(/\./,$hostname))) { + if ($hoststr eq '') { + $hoststr = $part; + } else { + $hoststr = "$part.$hoststr"; + } + if ($hoststr eq $hostname) { + push(@possok,$hostname); + } else { + push(@possok,"*.$hoststr"); + } + } + if (@possok) { + foreach my $poss (@possok) { + last if (!$uselink); + foreach my $policy (@policies) { + if ($policy =~ m{^(\Q$protocol\E//|)\Q$poss\E(\Q:$port\E|)$}) { + undef($uselink); + last; + } + } + } + } + } + } + } + } elsif ($xframeop ne '') { + $uselink = 1; + my @policies = split(/\s*,\s*/,$xframeop); + if (@policies) { + unless (grep(/^deny$/,@policies)) { + if ($origin ne '') { + if (grep(/^sameorigin$/,@policies)) { + if ($remotehost eq $origin) { + undef($uselink); + } + } + if ($uselink) { + foreach my $policy (@policies) { + if ($policy =~ /^allow-from\s*(.+)$/) { + my $allowfrom = $1; + if (($allowfrom ne '') && ($allowfrom eq $origin)) { + undef($uselink); + last; + } + } + } + } + } + } + } + } + } + } + if ($nocache) { + if ($cached) { + my $devalidate; + if ($uselink && !$result) { + $devalidate = 1; + } elsif (!$uselink && $result) { + $devalidate = 1; + } + if ($devalidate) { + &Apache::lonnet::devalidate_cache_new('noiframe',$remhost.':'.$remport); + } + } + } else { + if ($uselink) { + $result = 1; + } else { + $result = 0; + } + &Apache::lonnet::do_cache_new('noiframe',$remhost.':'.$remport,$result,3600); + } + 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__;