--- loncom/interface/loncommon.pm 2013/05/10 16:09:58 1.1127 +++ loncom/interface/loncommon.pm 2020/06/01 20:35:02 1.1342 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # a pile of common routines # -# $Id: loncommon.pm,v 1.1127 2013/05/10 16:09:58 raeburn Exp $ +# $Id: loncommon.pm,v 1.1342 2020/06/01 20:35:02 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -69,12 +69,25 @@ use Apache::lontexconvert(); use Apache::lonclonecourse(); use Apache::lonuserutils(); use Apache::lonuserstate(); +use Apache::courseclassifier(); use LONCAPA qw(:DEFAULT :match); +use LONCAPA::LWPReq; +use HTTP::Request; use DateTime::TimeZone; -use DateTime::Locale::Catalog; +use DateTime::Locale; +use Encode(); use Text::Aspell; use Authen::Captcha; use Captcha::reCAPTCHA; +use JSON::DWIW; +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); @@ -190,7 +203,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); @@ -212,7 +225,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); @@ -226,7 +239,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); @@ -240,7 +253,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); @@ -254,12 +267,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); } @@ -269,7 +282,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); @@ -422,7 +435,7 @@ sub studentbrowser_javascript { END # output the initial values for the selection lists - $result .= "\n"; my @order = sort(keys(%{$hashref})); if (ref($menuorder) eq 'ARRAY') { @order = @{$menuorder}; @@ -1272,9 +1306,13 @@ sub help_open_topic { } # Add the text + my $target = ' target="_top"'; + if (($env{'request.lti.login'}) && ($env{'request.lti.target'} eq 'iframe')) { + $target = ''; + } if ($text ne "") { $template.='' - .'' + .'' .$text.''; } @@ -1284,7 +1322,7 @@ sub help_open_topic { if ($imgid ne '') { $imgid = ' id="'.$imgid.'"'; } - $template.=' ' + $template.=' ' .''.&mt('Help: [_1]',$topic).''; unless ($not_author) { - $out .= ' ' - .&help_open_topic('Authoring_Output_Tags',&mt('Output Tags'),$stayOnPage,undef,600) + $out .= '' + .&help_open_topic('Authoring_Output_Tags',&mt('Output Tags'),$stayOnPage,undef,600) + .' ' + .&help_open_topic('Authoring_Multilingual_Problems',&mt('How to create problems in different languages'),$stayOnPage,undef,600) .''; } $out .= ''; # End cheatsheet @@ -1380,32 +1420,38 @@ sub top_nav_help { $text = &mt($text); my $stay_on_page = 1; - my $link = ($stay_on_page) ? "javascript:helpMenu('display')" - : "javascript:helpMenu('open')"; - my $banner_link = &update_help_link(undef,undef,undef,undef,$stay_on_page); - + my ($link,$banner_link); + unless ($env{'request.noversionuri'} =~ m{^/adm/helpmenu}) { + $link = ($stay_on_page) ? "javascript:helpMenu('display')" + : "javascript:helpMenu('open')"; + $banner_link = &update_help_link(undef,undef,undef,undef,$stay_on_page); + } my $title = &mt('Get help'); - - return <<"END"; + if ($link) { + return <<"END"; $banner_link - $text +$text END + } else { + return ' '.$text.' '; + } } sub help_menu_js { - my ($text) = @_; + my ($httphost) = @_; my $stayOnPage = 1; my $width = 620; my $height = 600; my $helptopic=&general_help(); - my $details_link = '/adm/help/'.$helptopic.'.hlp'; + my $details_link = $httphost.'/adm/help/'.$helptopic.'.hlp'; my $nothing=&Apache::lonhtmlcommon::javascript_nothing(); my $start_page = &Apache::loncommon::start_page('Help Menu', undef, {'frameset' => 1, 'js_ready' => 1, + 'use_absolute' => $httphost, 'add_entries' => { - 'border' => '0', + 'border' => '0', 'rows' => "110,*",},}); my $end_page = &Apache::loncommon::end_page({'frameset' => 1, @@ -1435,9 +1481,10 @@ function helpMenu(target) { return; } function writeHelp(caller) { - caller.document.writeln('$start_page\\n\\n\\n$end_page') - caller.document.close() - caller.focus() + caller.document.writeln('$start_page\\n\\n'); + caller.document.writeln('\\n$end_page'); + caller.document.close(); + caller.focus(); } // END LON-CAPA Internal --> // ]]> @@ -1468,19 +1515,24 @@ sub help_open_bug { { $link = $url; } + + my $target = ' target="_top"'; + if (($env{'request.lti.login'}) && ($env{'request.lti.target'} eq 'iframe')) { + $target = ''; + } # Add the text if ($text ne "") { $template .= "". - "
$text"; + "$text"; } # Add the graphic my $title = &mt('Report a Bug'); my $bugicon=&lonhttpdurl("/adm/lonMisc/smallBug.gif"); $template .= <<"ENDTEMPLATE"; - (Bug: $topic) + (Bug: $topic) ENDTEMPLATE if ($text ne '') { $template.='
' }; return $template; @@ -1745,12 +1797,528 @@ 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); + $browse_or_search = <<"END"; + + 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 1) { + window['select1'+element+'_changed'](); + } + } + } + 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,numcrsdirs) { + if (document.getElementById('chooser_'+element+'_crsres')) { + document.getElementById('chooser_'+element+'_crsres').style.display = 'none'; + } + if (document.getElementById('chooser_'+element+'_upload')) { + var curr = document.getElementById('chooser_'+element+'_upload').style.display; + if (curr == 'none') { + if (numcrsdirs) { + form.elements['crsauthorpath_'+element].selectedIndex = 0; + form.elements['newsubdir_'+element][0].checked = true; + toggleNewsubdir(form,element); + } + } + document.getElementById('chooser_'+element+'_upload').style.display = 'block'; + } + return; + } + + function toggleResImport(form,element) { + var choices = new Array('crsres','upload'); + for (var i=0; i +// + function fold_box(curDepth, lastresource){ + + // we need a list because there can be several blocks you need to fold in one tag + var block = document.getElementsByName('foldblock_'+curDepth); + // but there is only one folding button per tag + var foldbutton = document.getElementById('folding_btn_'+curDepth); + + if(block.item(0).style.display == 'none'){ + + foldbutton.value = '@{[&mt("Hide")]}'; + for (i = 0; i < block.length; i++){ + block.item(i).style.display = ''; + } + }else{ + + foldbutton.value = '@{[&mt("Show")]}'; + for (i = 0; i < block.length; i++){ + // block.item(i).style.visibility = 'collapse'; + block.item(i).style.display = 'none'; + } + }; + saveState(lastresource); + } + + function saveState (lastresource) { + + var tag_list = getTagList(); + if(tag_list != null){ + var timestamp = new Date().getTime(); + var key = lastresource; + + // the value pattern is: 'time;key1,value1;key2,value2; ... ' + // starting with timestamp + var value = timestamp+';'; + + // building the list of key-value pairs + for(var i = 0; i < tag_list.length; i++){ + value += tag_list[i]+','; + value += document.getElementsByName(tag_list[i])[0].style.display+';'; + } + + // only iterate whole storage if nothing to override + if(localStorage.getItem(key) == null){ + + // prevent storage from growing large + if(localStorage.length > 50){ + var regex_getTimestamp = /^(?:\d)+;/; + var oldest_timestamp = regex_getTimestamp.exec(localStorage.key(0)); + var oldest_key; + + for(var i = 1; i < localStorage.length; i++){ + if (regex_getTimestamp.exec(localStorage.key(i)) < oldest_timestamp) { + oldest_key = localStorage.key(i); + oldest_timestamp = regex_getTimestamp.exec(oldest_key); + } + } + localStorage.removeItem(oldest_key); + } + } + localStorage.setItem(key,value); + } + } + + // restore folding status of blocks (on page load) + function restoreState (lastresource) { + if(localStorage.getItem(lastresource) != null){ + var key = lastresource; + var value = localStorage.getItem(key); + var regex_delTimestamp = /^\d+;/; + + value.replace(regex_delTimestamp, ''); + + var valueArr = value.split(';'); + var pairs; + var elements; + for (var i = 0; i < valueArr.length; i++){ + pairs = valueArr[i].split(','); + elements = document.getElementsByName(pairs[0]); + + for (var j = 0; j < elements.length; j++){ + elements[j].style.display = pairs[1]; + if (pairs[1] == "none"){ + var regex_id = /([_\\d]+)\$/; + regex_id.exec(pairs[0]); + document.getElementById("folding_btn"+RegExp.\$1).value = "Show"; + } + } + } + } + } + + function getTagList () { + + var stringToSearch = document.lonhomework.innerHTML; + + var ret = new Array(); + var regex_findBlock = /(foldblock_.*?)"/g; + var tag_list = stringToSearch.match(regex_findBlock); + + if(tag_list != null){ + for(var i = 0; i < tag_list.length; i++){ + ret.push(tag_list[i].replace(/"/, '')); + } + } + return ret; + } + + function saveScrollPosition (resource) { + var tag_list = getTagList(); + + // we dont always want to jump to the first block + // 170 is roughly above the "Problem Editing" header. we just want to save if the user scrolled down further than this + if(\$(window).scrollTop() > 170){ + if(tag_list != null){ + var result; + for(var i = 0; i < tag_list.length; i++){ + if(isElementInViewport(tag_list[i])){ + result += tag_list[i]+';'; + } + } + sessionStorage.setItem('anchor_'+resource, result); + } + } else { + // we dont need to save zero, just delete the item to leave everything tidy + sessionStorage.removeItem('anchor_'+resource); + } + } + + function restoreScrollPosition(resource){ + + var elem = sessionStorage.getItem('anchor_'+resource); + if(elem != null){ + var tag_list = elem.split(';'); + var elem_list; + + for(var i = 0; i < tag_list.length; i++){ + elem_list = document.getElementsByName(tag_list[i]); + + if(elem_list.length > 0){ + elem = elem_list[0]; + break; + } + } + elem.scrollIntoView(); + } + } + + function isElementInViewport(el) { + + // change to last element instead of first + var elem = document.getElementsByName(el); + var rect = elem[0].getBoundingClientRect(); + + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */ + rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */ + ); + } + + function autosize(depth){ + var cmInst = window['cm'+depth]; + var fitsizeButton = document.getElementById('fitsize'+depth); + + // is fixed size, switching to dynamic + if (sessionStorage.getItem("autosized_"+depth) == null) { + cmInst.setSize("","auto"); + fitsizeButton.value = "@{[&mt('Fixed size')]}"; + sessionStorage.setItem("autosized_"+depth, "yes"); + + // is dynamic size, switching to fixed + } else { + cmInst.setSize("","300px"); + fitsizeButton.value = "@{[&mt('Dynamic size')]}"; + sessionStorage.removeItem("autosized_"+depth); + } + } + +$browse_or_search + +// ]]> + +COLORFULEDIT +} + +sub xmleditor_js { + return < + +XMLEDIT +} + +sub insert_folding_button { + my $curDepth = $Apache::lonxml::curdepth; + my $lastresource = $env{'request.ambiguous'}; + + return ""; +} + +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 ($form,$firstselectname,$secondselectname,$onchangefirst,$only,$suffix,$disabled) = @_; + return (0) unless ($env{'request.course.id'}); + my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; + my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; + my $crshome = $env{'course.'.$env{'request.course.id'}.'.home'}; + return (0) unless (($cnum ne '') && ($cdom ne '')); + my $londocroot = $Apache::lonnet::perlvar{'lonDocRoot'}; + my @ids=&Apache::lonnet::current_machine_ids(); + my ($output,$is_home,$relpath,%subdirs,%files,%selimport_menus); + + if (grep(/^\Q$crshome\E$/,@ids)) { + $is_home = 1; + } + $relpath = "/priv/$cdom/$cnum"; + &Apache::lonnet::recursedirs($is_home,'priv',$londocroot,$relpath,'',\%subdirs,\%files); + my %lt = &Apache::lonlocal::texthash ( + fnam => 'Filename', + dire => 'Directory', + ); + my $numdirs = scalar(keys(%files)); + my (%possexts,$singledir,@singledirfiles); + if ($only) { + map { $possexts{$_} = 1; } split(/\s*,\s*/,$only); + } + my (%nonemptydirs,$possdirs); + if ($numdirs > 1) { + my @order; + foreach my $key (sort { lc($a) cmp lc($b) } (keys(%files))) { + if (ref($files{$key}) eq 'HASH') { + my $shown = $key; + if ($key eq '') { + $shown = '/'; + } + my @ordered = (); + foreach my $file (sort { lc($a) cmp lc($b) } (keys(%{$files{$key}}))) { + next if ($file =~ /\.rights$/); + if ($only) { + my ($ext) = ($file =~ /\.([^.]+)$/); + unless ($possexts{lc($ext)}) { + next; + } + } + $selimport_menus{$key}->{'select2'}->{$file} = $file; + push(@ordered,$file); + } + if (@ordered) { + push(@order,$key); + $nonemptydirs{$key} = 1; + $selimport_menus{$key}->{'text'} = $shown; + $selimport_menus{$key}->{'default'} = ''; + $selimport_menus{$key}->{'select2'}->{''} = ''; + $selimport_menus{$key}->{'order'} = \@ordered; + } + } + } + $possdirs = scalar(keys(%nonemptydirs)); + if ($possdirs > 1) { + my @order = sort { lc($a) cmp lc($b) } (keys(%nonemptydirs)); + $output = $lt{'dire'}. + &linked_select_forms($form,'
'. + $lt{'fnam'},'', + $firstselectname,$secondselectname, + \%selimport_menus,\@order, + $onchangefirst,'',$suffix).'
'; + } elsif ($possdirs == 1) { + $singledir = (keys(%nonemptydirs))[0]; + if (ref($selimport_menus{$singledir}->{'order'}) eq 'ARRAY') { + @singledirfiles = @{$selimport_menus{$singledir}->{'order'}}; + } + delete($selimport_menus{$singledir}); + } + } elsif ($numdirs == 1) { + $singledir = (keys(%files))[0]; + foreach my $file (sort { lc($a) cmp lc($b) } (keys(%{$files{$singledir}}))) { + if ($only) { + my ($ext) = ($file =~ /\.([^.]+)$/); + unless ($possexts{lc($ext)}) { + next; + } + } else { + next if ($file =~ /\.rights$/); + } + push(@singledirfiles,$file); + } + if (@singledirfiles) { + $possdirs = 1; + } + } + if (($possdirs == 1) && (@singledirfiles)) { + my $showdir = $singledir; + if ($singledir eq '') { + $showdir = '/'; + } + $output = $lt{'dire'}. + '
'. + $lt{'fnam'}.'
'."\n"; + } + return ($possdirs,$output); +} + =pod =head1 Excel and CSV file utility routines -=over 4 - =cut ############################################################### @@ -1758,6 +2326,8 @@ RESIZE =pod +=over 4 + =item * &csv_translate($text) Translate $text to allow it to be output as a 'comma separated values' @@ -1932,10 +2502,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))]; @@ -2004,12 +2588,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 \n"; + my $disabled; + if ($readonly) { + $disabled = ' disabled="disabled"'; + } + my $selectform = " form to allow a user to select the domain to preform an operation in. @@ -2208,14 +2799,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}); @@ -2226,7 +2822,7 @@ sub select_dom_form { if (ref($excdoms) eq 'ARRAY') { map { $exclude{$_} = 1; } @{$excdoms}; } - my $selectdomain = "\n"; foreach my $dom (@domains) { next if ($exclude{$dom}); $selectdomain.="