--- loncom/interface/loncommon.pm 2013/07/15 17:42:11 1.1140 +++ loncom/interface/loncommon.pm 2016/07/08 17:21:01 1.1249 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # a pile of common routines # -# $Id: loncommon.pm,v 1.1140 2013/07/15 17:42:11 raeburn Exp $ +# $Id: loncommon.pm,v 1.1249 2016/07/08 17:21:01 damieng Exp $ # # Copyright Michigan State University Board of Trustees # @@ -69,12 +69,20 @@ use Apache::lontexconvert(); use Apache::lonclonecourse(); use Apache::lonuserutils(); use Apache::lonuserstate(); +use Apache::courseclassifier(); use LONCAPA qw(:DEFAULT :match); 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 LWP::UserAgent; +use Crypt::DES; +use DynaLoader; # for Crypt::DES version +use MIME::Lite; +use MIME::Types; # ---------------------------------------------- Designs use vars qw(%defaultdesign); @@ -532,7 +540,7 @@ ENDAUTHORBRW sub coursebrowser_javascript { my ($domainfilter,$sec_element,$formname,$role_element,$crstype, - $credits_element) = @_; + $credits_element,$instcode) = @_; my $wintitle = 'Course_Browser'; if ($crstype eq 'Community') { $wintitle = 'Community_Browser'; @@ -583,6 +591,12 @@ sub coursebrowser_javascript { var ownername = document.forms[formid].ccuname.value; var ownerdom = document.forms[formid].ccdomain.options[document.forms[formid].ccdomain.selectedIndex].value; url += '&cloner='+ownername+':'+ownerdom; + if (type == 'Course') { + url += '&crscode='+document.forms[formid].crscode.value; + } + } + if (formname == 'requestcrs') { + url += '&crsdom=$domainfilter&crscode=$instcode'; } if (multflag !=null && multflag != '') { url += '&multiple='+multflag; @@ -867,6 +881,8 @@ sub selectcourse_link { my $linktext = &mt('Select Course'); if ($selecttype eq 'Community') { $linktext = &mt('Select Community'); + } elsif ($selecttype eq 'Placement') { + $linktext = &mt('Select Placement Test'); } elsif ($selecttype eq 'Course/Community') { $linktext = &mt('Select Course/Community'); $type = ''; @@ -958,15 +974,16 @@ sub select_datelocale { } $output .= '> '; } + my @languages = &Apache::lonlocal::preferred_languages(); my (@possibles,%locale_names); - my @locales = DateTime::Locale::Catalog::Locales; - foreach my $locale (@locales) { - if (ref($locale) eq 'HASH') { - my $id = $locale->{'id'}; - if ($id ne '') { - my $en_terr = $locale->{'en_territory'}; - my $native_terr = $locale->{'native_territory'}; - my @languages = &Apache::lonlocal::preferred_languages(); + my @locales = DateTime::Locale->ids(); + foreach my $id (@locales) { + if ($id ne '') { + my ($en_terr,$native_terr); + my $loc = DateTime::Locale->load($id); + if (ref($loc)) { + $en_terr = $loc->name(); + $native_terr = $loc->native_name(); if (grep(/^en$/,@languages) || !@languages) { if ($en_terr ne '') { $locale_names{$id} = '('.$en_terr.')'; @@ -980,8 +997,9 @@ sub select_datelocale { $locale_names{$id} = '('.$en_terr.')'; } } - push (@possibles,$id); - } + $locale_names{$id} = Encode::encode('UTF-8',$locale_names{$id}); + push(@possibles,$id); + } } } foreach my $item (sort(@possibles)) { @@ -991,7 +1009,7 @@ sub select_datelocale { } $output.=">$item"; if ($locale_names{$item} ne '') { - $output.=" $locale_names{$item}\n"; + $output.=' '.$locale_names{$item}; } $output.="\n"; } @@ -1076,6 +1094,9 @@ linked_select_forms takes the following =item * $onchangesecond, additional javascript call to execute for an onchange event for the second \n"; + $result .= ""; +} + +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) = @_; + 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}}))) { + 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; + } + } + 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 +2300,8 @@ RESIZE =pod +=over 4 + =item * &csv_translate($text) Translate $text to allow it to be output as a 'comma separated values' @@ -2017,12 +2561,16 @@ See lonrights.pm for an example invocati #------------------------------------------- sub select_form { - my ($def,$name,$hashref,$onchange) = @_; + my ($def,$name,$hashref,$onchange,$readonly) = @_; return unless (ref($hashref) eq 'HASH'); if ($onchange) { $onchange = ' onchange="'.$onchange.'"'; } - my $selectform = "\n"; my @keys; if (exists($hashref->{'select_form_order'})) { @keys=@{$hashref->{'select_form_order'}}; @@ -2351,6 +2899,10 @@ Outputs: =item * $clientmobile +=item * $clientinfo + +=item * $clientosversion + =back =back @@ -2370,8 +2922,9 @@ sub decode_user_agent { my $clientmathml=''; my $clientunicode='0'; my $clientmobile=0; + my $clientosversion=''; for (my $i=0;$i<=$#browsertype;$i++) { - my ($bname,$match,$notmatch,$vreg,$minv,$univ)=split(/\:/,$browsertype[$i]); + my ($bname,$match,$notmatch,$vreg,$minv,$univ)=split(/\%/,$browsertype[$i]); if (($httpbrowser=~/$match/i) && ($httpbrowser!~/$notmatch/i)) { $clientbrowser=$bname; $httpbrowser=~/$vreg/i; @@ -2381,6 +2934,7 @@ sub decode_user_agent { } } my $clientos='unknown'; + my $clientinfo; if (($httpbrowser=~/linux/i) || ($httpbrowser=~/unix/i) || ($httpbrowser=~/ux/i) || @@ -2390,13 +2944,24 @@ sub decode_user_agent { if ($httpbrowser=~/next/i) { $clientos='next'; } if (($httpbrowser=~/mac/i) || ($httpbrowser=~/powerpc/i)) { $clientos='mac'; } - if ($httpbrowser=~/win/i) { $clientos='win'; } + if ($httpbrowser=~/win/i) { + $clientos='win'; + if ($httpbrowser =~/Windows\s+NT\s+(\d+\.\d+)/i) { + $clientosversion = $1; + } + } if ($httpbrowser=~/embed/i) { $clientos='pda'; } if ($httpbrowser=~/(Android|iPod|iPad|iPhone|webOS|Blackberry|Windows Phone|Opera m(?:ob|in)|Fennec)/i) { $clientmobile=lc($1); } + if ($httpbrowser=~ m{Firefox/(\d+\.\d+)}) { + $clientinfo = 'firefox-'.$1; + } elsif ($httpbrowser=~ m{chromeframe/(\d+\.\d+)\.}) { + $clientinfo = 'chromeframe-'.$1; + } return ($httpbrowser,$clientbrowser,$clientversion,$clientmathml, - $clientunicode,$clientos,$clientmobile); + $clientunicode,$clientos,$clientmobile,$clientinfo, + $clientosversion); } ############################################################### @@ -3066,6 +3631,8 @@ sub get_related_words { =pod +=back + =head1 Spell checking =over 4 @@ -3099,12 +3666,6 @@ Note: This sub assumes that aspell is in =cut -=pod - -=back - -=cut - sub check_spelling { my ($wordlist, $language) = @_; my @misspellings; @@ -3320,7 +3881,7 @@ sub screenname { # ------------------------------------------------------------- Confirm Wrapper =pod -=item confirmwrapper +=item * &confirmwrapper($message) Wrap messages about completion of operation in box @@ -3593,7 +4154,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 @@ -3736,7 +4301,7 @@ sub user_lang { =over 4 =item * &get_previous_attempt($symb, $username, $domain, $course, - $getattempt, $regexp, $gradesub) + $getattempt, $regexp, $gradesub, $usec, $identifier) Return string with previous attempt on problem. Arguments: @@ -3758,6 +4323,11 @@ Return string with previous attempt on p =item * $gradesub: routine that processes the string if it matches $regexp +=item * $usec: section of the desired student + +=item * $identifier: counter for student (multiple students one problem) or + problem (one student; whole sequence). + =back The output string is a table containing all desired attempts, if any. @@ -3765,7 +4335,7 @@ The output string is a table containing =cut sub get_previous_attempt { - my ($symb,$username,$domain,$course,$getattempt,$regexp,$gradesub)=@_; + my ($symb,$username,$domain,$course,$getattempt,$regexp,$gradesub,$usec,$identifier)=@_; my $prevattempts=''; no strict 'refs'; if ($symb) { @@ -3775,13 +4345,18 @@ sub get_previous_attempt { my %lasthash=(); my $version; for ($version=1;$version<=$returnhash{'version'};$version++) { - foreach my $key (sort(split(/\:/,$returnhash{$version.':keys'}))) { - $lasthash{$key}=$returnhash{$version.':'.$key}; + foreach my $key (reverse(sort(split(/\:/,$returnhash{$version.':keys'})))) { + if ($key =~ /\.rawrndseed$/) { + my ($id) = ($key =~ /^(.+)\.rawrndseed$/); + $lasthash{$id.'.rndseed'} = $returnhash{$version.':'.$key}; + } else { + $lasthash{$key}=$returnhash{$version.':'.$key}; + } } } $prevattempts=&start_data_table().&start_data_table_header_row(); $prevattempts.=''.&mt('History').''; - my (%typeparts,%lasthidden); + my (%typeparts,%lasthidden,%regraded,%hidestatus); my $showsurv=&Apache::lonnet::allowed('vas',$env{'request.course.id'}); foreach my $key (sort(keys(%lasthash))) { my ($ign,@parts) = split(/\./,$key); @@ -3798,6 +4373,18 @@ sub get_previous_attempt { $lasthidden{$ign.'.'.$id} = 1; } } + if ($identifier ne '') { + my $id = join(',',@parts); + if (&Apache::lonnet::EXT("resource.$id.problemstatus",$symb, + $domain,$username,$usec,undef,$course) =~ /^no/) { + $hidestatus{$ign.'.'.$id} = 1; + } + } + } elsif ($data eq 'regrader') { + if (($identifier ne '') && (@parts)) { + my $id = join(',',@parts); + $regraded{$ign.'.'.$id} = 1; + } } } else { if ($#parts == 0) { @@ -3809,17 +4396,60 @@ sub get_previous_attempt { } $prevattempts.=&end_data_table_header_row(); if ($getattempt eq '') { + my (%solved,%resets,%probstatus); + if (($identifier ne '') && (keys(%regraded) > 0)) { + for ($version=1;$version<=$returnhash{'version'};$version++) { + foreach my $id (keys(%regraded)) { + if (($returnhash{$version.':'.$id.'.regrader'}) && + ($returnhash{$version.':'.$id.'.tries'} eq '') && + ($returnhash{$version.':'.$id.'.award'} eq '')) { + push(@{$resets{$id}},$version); + } + } + } + } for ($version=1;$version<=$returnhash{'version'};$version++) { - my @hidden; + my (@hidden,@unsolved); if (%typeparts) { foreach my $id (keys(%typeparts)) { - if (($returnhash{$version.':'.$id.'.type'} eq 'anonsurvey') || ($returnhash{$version.':'.$id.'.type'} eq 'anonsurveycred')) { + if (($returnhash{$version.':'.$id.'.type'} eq 'anonsurvey') || + ($returnhash{$version.':'.$id.'.type'} eq 'anonsurveycred')) { push(@hidden,$id); + } elsif ($identifier ne '') { + unless (($returnhash{$version.':'.$id.'.type'} eq 'survey') || + ($returnhash{$version.':'.$id.'.type'} eq 'surveycred') || + ($hidestatus{$id})) { + next if ((ref($resets{$id}) eq 'ARRAY') && grep(/^\Q$version\E$/,@{$resets{$id}})); + if ($returnhash{$version.':'.$id.'.solved'} eq 'correct_by_student') { + push(@{$solved{$id}},$version); + } elsif (($returnhash{$version.':'.$id.'.solved'} ne '') && + (ref($solved{$id}) eq 'ARRAY')) { + my $skip; + if (ref($resets{$id}) eq 'ARRAY') { + foreach my $reset (@{$resets{$id}}) { + if ($reset > $solved{$id}[-1]) { + $skip=1; + last; + } + } + } + unless ($skip) { + my ($ign,$partslist) = split(/\./,$id,2); + push(@unsolved,$partslist); + } + } + } } } } $prevattempts.=&start_data_table_row(). - ''.&mt('Transaction [_1]',$version).''; + ''.&mt('Transaction [_1]',$version); + if (@unsolved) { + $prevattempts .= ''; + } + $prevattempts .= ''; if (@hidden) { foreach my $key (sort(keys(%lasthash))) { next if ($key =~ /\.foilorder$/); @@ -3841,9 +4471,15 @@ sub get_previous_attempt { } } else { if ($key =~ /\./) { - my $value = &format_previous_attempt_value($key, - $returnhash{$version.':'.$key}); - $prevattempts.=''.$value.' '; + my $value = $returnhash{$version.':'.$key}; + if ($key =~ /\.rndseed$/) { + my ($id) = ($key =~ /^(.+)\.[^.]+$/); + if (exists($returnhash{$version.':'.$id.'.rawrndseed'})) { + $value = $returnhash{$version.':'.$id.'.rawrndseed'}; + } + } + $prevattempts.=''.&format_previous_attempt_value($key,$value). + ' '; } else { $prevattempts.=' '; } @@ -3852,9 +4488,15 @@ sub get_previous_attempt { } else { foreach my $key (sort(keys(%lasthash))) { next if ($key =~ /\.foilorder$/); - my $value = &format_previous_attempt_value($key, - $returnhash{$version.':'.$key}); - $prevattempts.=''.$value.' '; + my $value = $returnhash{$version.':'.$key}; + if ($key =~ /\.rndseed$/) { + my ($id) = ($key =~ /^(.+)\.[^.]+$/); + if (exists($returnhash{$version.':'.$id.'.rawrndseed'})) { + $value = $returnhash{$version.':'.$id.'.rawrndseed'}; + } + } + $prevattempts.=''.&format_previous_attempt_value($key,$value). + ' '; } } $prevattempts.=&end_data_table_row(); @@ -3879,7 +4521,7 @@ sub get_previous_attempt { if ($key =~/$regexp$/ && (defined &$gradesub)) { $value = &$gradesub($value); } - $prevattempts.=''.$value.' '; + $prevattempts.=''. $value.' '; } else { $prevattempts.=' '; } @@ -3895,7 +4537,7 @@ sub get_previous_attempt { if ($key =~/$regexp$/ && (defined &$gradesub)) { $value = &$gradesub($value); } - $prevattempts.=''.$value.' '; + $prevattempts.=''.$value.' '; } } $prevattempts.= &end_data_table_row().&end_data_table(); @@ -3916,11 +4558,13 @@ sub get_previous_attempt { sub format_previous_attempt_value { my ($key,$value) = @_; if (($key =~ /timestamp/) || ($key=~/duedate/)) { - $value = &Apache::lonlocal::locallocaltime($value); + $value = &Apache::lonlocal::locallocaltime($value); } elsif (ref($value) eq 'ARRAY') { - $value = '('.join(', ', @{ $value }).')'; + $value = &HTML::Entities::encode('('.join(', ', @{ $value }).')','"<>&'); } elsif ($key =~ /answerstring$/) { my %answers = &Apache::lonnet::str2hash($value); + my @answer = %answers; + %answers = map {&HTML::Entities::encode($_, '"<>&')} @answer; my @anskeys = sort(keys(%answers)); if (@anskeys == 1) { my $answer = $answers{$anskeys[0]}; @@ -3943,7 +4587,7 @@ sub format_previous_attempt_value { } } } else { - $value = &unescape($value); + $value = &HTML::Entities::encode(&unescape($value), '"<>&'); } return $value; } @@ -4304,23 +4948,20 @@ sub findallcourses { ############################################### sub blockcheck { - my ($setters,$activity,$uname,$udom,$url) = @_; + my ($setters,$activity,$uname,$udom,$url,$is_course) = @_; - if (!defined($udom)) { + 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); + return ($startblock,$endblock,$triggerblock); + } + } else { $udom = $env{'user.domain'}; - } - if (!defined($uname)) { $uname = $env{'user.name'}; } - # If uname and udom are for a course, check for blocks in the course. - - if (&Apache::lonnet::is_course($udom,$uname)) { - my ($startblock,$endblock,$triggerblock) = - &get_blocks($setters,$activity,$udom,$uname,$url); - return ($startblock,$endblock,$triggerblock); - } - my $startblock = 0; my $endblock = 0; my $triggerblock = ''; @@ -4330,7 +4971,8 @@ sub blockcheck { # boards, chat or groups, check for blocking in current course only. if (($activity eq 'boards' || $activity eq 'chat' || - $activity eq 'groups') && ($env{'request.course.id'})) { + $activity eq 'groups' || $activity eq 'printout') && + ($env{'request.course.id'})) { foreach my $key (keys(%live_courses)) { if ($key ne $env{'request.course.id'}) { delete($live_courses{$key}); @@ -4594,12 +5236,12 @@ sub parse_block_record { } sub blocking_status { - my ($activity,$uname,$udom,$url) = @_; + my ($activity,$uname,$udom,$url,$is_course) = @_; my %setters; # check for active blocking my ($startblock,$endblock,$triggerblock) = - &blockcheck(\%setters,$activity,$uname,$udom,$url); + &blockcheck(\%setters,$activity,$uname,$udom,$url,$is_course); my $blocked = 0; if ($startblock && $endblock) { $blocked = 1; @@ -4611,9 +5253,9 @@ 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') { - $querystring .= "&udom=$udom" if $udom; - $querystring .= "&uname=$uname" if $uname; + if (($activity eq 'port') || ($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,'&"'); } @@ -4632,13 +5274,17 @@ END_MYBLOCK my $popupUrl = "/adm/blockingstatus/$querystring"; my $text = &mt('Communication Blocked'); + my $class = 'LC_comblock'; if ($activity eq 'docs') { $text = &mt('Content Access Blocked'); + $class = ''; } elsif ($activity eq 'printout') { $text = &mt('Printing Blocked'); + } elsif ($activity eq 'passwd') { + $text = &mt('Password Changing Blocked'); } $output .= <<"END_BLOCK"; -
+
$text @@ -4654,22 +5300,44 @@ END_BLOCK ############################################### sub check_ip_acc { - my ($acc)=@_; + my ($acc,$clientip)=@_; &Apache::lonxml::debug("acc is $acc"); if (!defined($acc) || $acc =~ /^\s*$/ || $acc =~/^\s*no\s*$/i) { return 1; } - my $allowed=0; - my $ip=$env{'request.host'} || $ENV{'REMOTE_ADDR'}; + my $allowed; + my $ip=$env{'request.host'} || $ENV{'REMOTE_ADDR'} || $clientip; my $name; - foreach my $pattern (split(',',$acc)) { - $pattern =~ s/^\s*//; - $pattern =~ s/\s*$//; + my %access = ( + allowfrom => 1, + denyfrom => 0, + ); + my @allows; + my @denies; + foreach my $item (split(',',$acc)) { + $item =~ s/^\s*//; + $item =~ s/\s*$//; + my $pattern; + if ($item =~ /^\!(.+)$/) { + push(@denies,$1); + } else { + push(@allows,$item); + } + } + my $numdenies = scalar(@denies); + my $numallows = scalar(@allows); + my $count = 0; + foreach my $pattern (@denies,@allows) { + $count ++; + my $acctype = 'allowfrom'; + if ($count <= $numdenies) { + $acctype = 'denyfrom'; + } if ($pattern =~ /\*$/) { #35.8.* $pattern=~s/\*//; - if ($ip =~ /^\Q$pattern\E/) { $allowed=1; } + if ($ip =~ /^\Q$pattern\E/) { $allowed=$access{$acctype}; } } elsif ($pattern =~ /(\d+\.\d+\.\d+)\.\[(\d+)-(\d+)\]$/) { #35.8.3.[34-56] my $low=$2; @@ -4677,7 +5345,7 @@ sub check_ip_acc { $pattern=$1; if ($ip =~ /^\Q$pattern\E/) { my $last=(split(/\./,$ip))[3]; - if ($last <=$high && $last >=$low) { $allowed=1; } + if ($last <=$high && $last >=$low) { $allowed=$access{$acctype}; } } } elsif ($pattern =~ /^\*/) { #*.msu.edu @@ -4687,10 +5355,10 @@ sub check_ip_acc { my $netaddr=inet_aton($ip); ($name)=gethostbyaddr($netaddr,AF_INET); } - if ($name =~ /\Q$pattern\E$/i) { $allowed=1; } + if ($name =~ /\Q$pattern\E$/i) { $allowed=$access{$acctype}; } } elsif ($pattern =~ /\d+\.\d+\.\d+\.\d+/) { #127.0.0.1 - if ($ip =~ /^\Q$pattern\E/) { $allowed=1; } + if ($ip =~ /^\Q$pattern\E/) { $allowed=$access{$acctype}; } } else { #some.name.com if (!defined($name)) { @@ -4698,9 +5366,16 @@ sub check_ip_acc { my $netaddr=inet_aton($ip); ($name)=gethostbyaddr($netaddr,AF_INET); } - if ($name =~ /\Q$pattern\E$/i) { $allowed=1; } + if ($name =~ /\Q$pattern\E$/i) { $allowed=$access{$acctype}; } + } + if ($allowed =~ /^(0|1)$/) { last; } + } + if ($allowed eq '') { + if ($numdenies && !$numallows) { + $allowed = 1; + } else { + $allowed = 0; } - if ($allowed) { last; } } return $allowed; } @@ -4756,23 +5431,29 @@ sub get_domainconf { if (keys(%{$domconfig{'login'}})) { foreach my $key (keys(%{$domconfig{'login'}})) { if (ref($domconfig{'login'}{$key}) eq 'HASH') { - if ($key eq 'loginvia') { - if (ref($domconfig{'login'}{'loginvia'}) eq 'HASH') { - foreach my $hostname (keys(%{$domconfig{'login'}{'loginvia'}})) { - if (ref($domconfig{'login'}{'loginvia'}{$hostname}) eq 'HASH') { - if ($domconfig{'login'}{'loginvia'}{$hostname}{'server'}) { - my $server = $domconfig{'login'}{'loginvia'}{$hostname}{'server'}; - $designhash{$udom.'.login.loginvia'} = $server; - if ($domconfig{'login'}{'loginvia'}{$hostname}{'serverpath'} eq 'custom') { - - $designhash{$udom.'.login.loginvia_'.$hostname} = $server.':'.$domconfig{'login'}{'loginvia'}{$hostname}{'custompath'}; - } else { - $designhash{$udom.'.login.loginvia_'.$hostname} = $server.':'.$domconfig{'login'}{'loginvia'}{$hostname}{'serverpath'}; + if (($key eq 'loginvia') || ($key eq 'headtag')) { + if (ref($domconfig{'login'}{$key}) eq 'HASH') { + foreach my $hostname (keys(%{$domconfig{'login'}{$key}})) { + if (ref($domconfig{'login'}{$key}{$hostname}) eq 'HASH') { + if ($key eq 'loginvia') { + if ($domconfig{'login'}{'loginvia'}{$hostname}{'server'}) { + my $server = $domconfig{'login'}{'loginvia'}{$hostname}{'server'}; + $designhash{$udom.'.login.loginvia'} = $server; + if ($domconfig{'login'}{'loginvia'}{$hostname}{'serverpath'} eq 'custom') { + + $designhash{$udom.'.login.loginvia_'.$hostname} = $server.':'.$domconfig{'login'}{'loginvia'}{$hostname}{'custompath'}; + } else { + $designhash{$udom.'.login.loginvia_'.$hostname} = $server.':'.$domconfig{'login'}{'loginvia'}{$hostname}{'serverpath'}; + } } - if ($domconfig{'login'}{'loginvia'}{$hostname}{'exempt'}) { - $designhash{$udom.'.login.loginvia_exempt_'.$hostname} = $domconfig{'login'}{'loginvia'}{$hostname}{'exempt'}; + } elsif ($key eq 'headtag') { + if ($domconfig{'login'}{'headtag'}{$hostname}{'url'}) { + $designhash{$udom.'.login.headtag_'.$hostname} = $domconfig{'login'}{'headtag'}{$hostname}{'url'}; } } + if ($domconfig{'login'}{$key}{$hostname}{'exempt'}) { + $designhash{$udom.'.login.'.$key.'_exempt_'.$hostname} = $domconfig{'login'}{$key}{$hostname}{'exempt'}; + } } } } @@ -5029,10 +5710,20 @@ sub CSTR_pageheader { $lastitem = $thisdisfn; } + my ($crsauthor,$title); + 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; + $title = &mt('Course Authoring Space'); + } else { + $title = &mt('Authoring Space'); + } + my $output = '
' .&Apache::loncommon::help_open_menu('','',3,'Authoring') #FIXME: Broken? Where is it? - .''.&mt('Authoring Space:').' ' + .''.$title.' ' .'
' #FIXME lonpubdir: target="_parent" .&Apache::lonhtmlcommon::crumbs($uname.'/'.$parentpath,'_top','/priv/'.$udom,undef,undef); @@ -5043,13 +5734,18 @@ sub CSTR_pageheader { .$lastitem .''; } - $output .= - '
' - #FIXME lonpubdir: &Apache::lonhtmlcommon::crumbs($uname.$thisdisfn.'/','_top','/priv','','+1',1)."
" - .&Apache::lonhtmlcommon::select_recent('construct','recent','this.form.action=this.form.recent.value;this.form.submit()') - .'
' - .&Apache::lonmenu::constspaceform() - .'
'; + + if ($crsauthor) { + $output .= ''.&Apache::lonmenu::constspaceform(); + } else { + $output .= + '
' + #FIXME lonpubdir: &Apache::lonhtmlcommon::crumbs($uname.$thisdisfn.'/','_top','/priv','','+1',1)."
" + .&Apache::lonhtmlcommon::select_recent('construct','recent','this.form.action=this.form.recent.value;this.form.submit()') + .'' + .&Apache::lonmenu::constspaceform(); + } + $output .= '
'; return $output; } @@ -5093,9 +5789,6 @@ Inputs: =item * $args, optional argument valid values are no_auto_mt_title -> prevents &mt()ing the title arg - inherit_jsmath -> when creating popup window in a page, - should it have jsmath forced on by the - current page =item * $advtoolsref, optional argument, ref to an array containing inlineremote items to be added in "Functions" menu below @@ -5120,6 +5813,7 @@ sub bodytag { $public = 1; } if (!$args->{'no_auto_mt_title'}) { $title = &mt($title); } + my $httphost = $args->{'use_absolute'}; $function = &get_users_function() if (!$function); my $img = &designparm($function.'.img',$domain); @@ -5135,7 +5829,10 @@ sub bodytag { @design{keys(%$addentries)} = @$addentries{keys(%$addentries)}; # role and realm - my ($role,$realm) = split(/\./,$env{'request.role'},2); + my ($role,$realm) = split(m{\./},$env{'request.role'},2); + if ($realm) { + $realm = '/'.$realm; + } if ($role eq 'ca') { my ($rdom,$rname) = ($realm =~ m{^/($match_domain)/($match_username)$}); $realm = &plainname($rname,$rdom); @@ -5159,7 +5856,7 @@ sub bodytag { # construct main body tag my $bodytag = "". - &Apache::lontexconvert::init_math_support($args->{'inherit_jsmath'}); + &Apache::lontexconvert::init_math_support(); &get_unprocessed_cgi($ENV{'QUERY_STRING'}, ['inhibitmenu']); @@ -5183,7 +5880,17 @@ sub bodytag { $dc_info =~ s/\s+$//; } - $role = '('.$role.')' if $role; + my $crstype; + if ($env{'request.course.id'}) { + $crstype = $env{'course.'.$env{'request.course.id'}.'.type'}; + } elsif ($args->{'crstype'}) { + $crstype = $args->{'crstype'}; + } + if (($crstype eq 'Placement') && (!$env{'request.role.adv'})) { + undef($role); + } else { + $role = '('.$role.')' if ($role && !$env{'browser.mobile'}); + } if ($env{'request.state'} eq 'construct') { $forcereg=1; } @@ -5192,9 +5899,9 @@ sub bodytag { # } $bodytag .= Apache::lonhtmlcommon::scripttag( - Apache::lonmenu::utilityfunctions(), 'start'); + Apache::lonmenu::utilityfunctions($httphost), 'start'); - my ($left,$right) = Apache::lonmenu::primary_menu(); + my ($left,$right) = Apache::lonmenu::primary_menu($crstype); if ($env{'request.noversionuri'} =~ m{^/res/adm/pages/}) { if ($dc_info) { @@ -5216,9 +5923,13 @@ sub bodytag { } $bodytag .= qq|
$realm $dc_info
|; + #if directed to not display the secondary menu, don't. + if ($args->{'no_secondary_menu'}) { + return $bodytag; + } #don't show menus for public users if (!$public){ - $bodytag .= Apache::lonmenu::secondary_menu(); + $bodytag .= Apache::lonmenu::secondary_menu($httphost); $bodytag .= Apache::lonmenu::serverform(); $bodytag .= Apache::lonhtmlcommon::scripttag('', 'end'); if ($env{'request.state'} eq 'construct') { @@ -5278,7 +5989,7 @@ sub make_attr_string { } my $attr_string; - foreach my $attr (keys(%$attr_ref)) { + foreach my $attr (sort(keys(%$attr_ref))) { $attr_string .= " $attr=\"".$attr_ref->{$attr}.'" '; } return $attr_string; @@ -5308,7 +6019,6 @@ sub endbodytag { unless ((ref($args) eq 'HASH') && ($args->{'notbody'})) { $endbodytag=''; } - $endbodytag=&Apache::lontexconvert::jsMath_process()."\n".$endbodytag; if ( exists( $env{'internal.head.redirect'} ) ) { if (!(ref($args) eq 'HASH' && $args->{'noredirectlink'})) { $endbodytag= @@ -5483,6 +6193,17 @@ div.LC_confirm_box .LC_success img { vertical-align: middle; } +.LC_maxwidth { + max-width: 100%; + height: auto; +} + +.LC_textsize_mobile { + \@media only screen and (max-device-width: 480px) { + -webkit-text-size-adjust:100%; -moz-text-size-adjust:100%; -ms-text-size-adjust:100%; + } +} + .LC_icon { border: none; vertical-align: middle; @@ -5588,6 +6309,12 @@ ul.LC_breadcrumb_tools_outerlist li { float: right; } +.LC_placement_prog { + padding-right: 20px; + font-weight: bold; + font-size: 90%; +} + table#LC_title_bar td { background: $tabbg; } @@ -5604,6 +6331,10 @@ table#LC_menubuttons img { vertical-align: middle; } +.LC_breadcrumbs_hoverable { + background: $sidebg; +} + td.LC_table_cell_checkbox { text-align: center; } @@ -6461,7 +7192,7 @@ div.LC_edit_problem_footer, div.LC_edit_problem_footer div, div.LC_edit_problem_editxml_header, div.LC_edit_problem_editxml_header div { - margin-top: 5px; + z-index: 100; } div.LC_edit_problem_header_title { @@ -6477,14 +7208,17 @@ table.LC_edit_problem_header_title { background: $tabbg; } -div.LC_edit_problem_discards { - float: left; - padding-bottom: 5px; +div.LC_edit_actionbar { + background-color: $sidebg; + margin: 0; + padding: 0; + line-height: 200%; } -div.LC_edit_problem_saves { - float: right; - padding-bottom: 5px; +div.LC_edit_actionbar div{ + padding: 0; + margin: 0; + display: inline-block; } .LC_edit_opt { @@ -6492,6 +7226,18 @@ div.LC_edit_problem_saves { white-space: nowrap; } +.LC_edit_problem_latexhelper{ + text-align: right; +} + +#LC_edit_problem_colorful div{ + margin-left: 40px; +} + +#LC_edit_problem_codemirror div{ + margin-left: 0px; +} + img.stift { border-width: 0; vertical-align: middle; @@ -6579,6 +7325,10 @@ fieldset { /* overflow: hidden; */ } +article.geogebraweb div { + margin: 0; +} + fieldset > legend { font-weight: bold; padding: 0 5px 0 5px; @@ -6606,7 +7356,6 @@ fieldset > legend { ol.LC_primary_menu { margin: 0; padding: 0; - background-color: $pgbg_or_bgcolor; } ol#LC_PathBreadcrumbs { @@ -6618,23 +7367,48 @@ ol.LC_primary_menu li { vertical-align: middle; text-align: left; list-style: none; + position: relative; float: left; + z-index: 100; /* will be displayed above codemirror and underneath the help-layer */ + line-height: 1.5em; } -ol.LC_primary_menu li a { +ol.LC_primary_menu li a, +ol.LC_primary_menu li p { display: block; margin: 0; padding: 0 5px 0 10px; text-decoration: none; } -ol.LC_primary_menu li ul { +ol.LC_primary_menu li p span.LC_primary_menu_innertitle { + display: inline-block; + width: 95%; + text-align: left; +} + +ol.LC_primary_menu li p span.LC_primary_menu_innerarrow { + display: inline-block; + width: 5%; + float: right; + text-align: right; + font-size: 70%; +} + +ol.LC_primary_menu ul { display: none; - width: 10em; + width: 15em; background-color: $data_table_light; + position: absolute; + top: 100%; } -ol.LC_primary_menu li:hover ul, ol.LC_primary_menu li.hover ul { +ol.LC_primary_menu ul ul { + left: 100%; + top: 0; +} + +ol.LC_primary_menu li:hover > ul, ol.LC_primary_menu li.hover > ul { display: block; position: absolute; margin: 0; @@ -6643,15 +7417,21 @@ ol.LC_primary_menu li:hover ul, ol.LC_pr } ol.LC_primary_menu li:hover li, ol.LC_primary_menu li.hover li { +/* First Submenu -> size should be smaller than the menu title of the whole menu */ font-size: 90%; vertical-align: top; float: none; border-left: 1px solid black; border-right: 1px solid black; +/* A dark bottom border to visualize different menu options; +overwritten in the create_submenu routine for the last border-bottom of the menu */ + border-bottom: 1px solid $data_table_dark; } -ol.LC_primary_menu li:hover li a, ol.LC_primary_menu li.hover li a { - background-color:$data_table_light; +ol.LC_primary_menu li li p:hover { + color:$button_hover; + text-decoration:none; + background-color:$data_table_dark; } ol.LC_primary_menu li li a:hover { @@ -6659,6 +7439,11 @@ ol.LC_primary_menu li li a:hover { background-color:$data_table_dark; } +/* Font-size equal to the size of the predecessors*/ +ol.LC_primary_menu li:hover li li { + font-size: 100%; +} + ol.LC_primary_menu li img { vertical-align: bottom; height: 1.1em; @@ -7201,6 +7986,16 @@ ul.LC_funclist li { } /* + styles used for response display +*/ +div.LC_radiofoil, div.LC_rankfoil { + margin: .5em 0em .5em 0em; +} +table.LC_itemgroup { + margin-top: 1em; +} + +/* styles used by TTH when "Default set of options to pass to tth/m when converting TeX" in course settings has been set @@ -7221,6 +8016,87 @@ span.roman {font-family: serif; font-sty span.overacc2 {position: relative; left: .8em; top: -1.2ex;} span.overacc1 {position: relative; left: .6em; top: -1.2ex;} +/* + sections with roles, for content only +*/ +section[class^="role-"] { + padding-left: 10px; + padding-right: 5px; + margin-top: 8px; + margin-bottom: 8px; + border: 1px solid #2A4; + border-radius: 5px; + box-shadow: 0px 1px 1px #BBB; +} +section[class^="role-"]>h1 { + position: relative; + margin: 0px; + padding-top: 10px; + padding-left: 40px; +} +section[class^="role-"]>h1:before { + position: absolute; + left: -5px; + top: 5px; +} +section.role-activity>h1:before { + content:url('/adm/daxe/images/section_icons/activity.png'); +} +section.role-advice>h1:before { + content:url('/adm/daxe/images/section_icons/advice.png'); +} +section.role-bibliography>h1:before { + content:url('/adm/daxe/images/section_icons/bibliography.png'); +} +section.role-citation>h1:before { + content:url('/adm/daxe/images/section_icons/citation.png'); +} +section.role-conclusion>h1:before { + content:url('/adm/daxe/images/section_icons/conclusion.png'); +} +section.role-definition>h1:before { + content:url('/adm/daxe/images/section_icons/definition.png'); +} +section.role-demonstration>h1:before { + content:url('/adm/daxe/images/section_icons/demonstration.png'); +} +section.role-example>h1:before { + content:url('/adm/daxe/images/section_icons/example.png'); +} +section.role-explanation>h1:before { + content:url('/adm/daxe/images/section_icons/explanation.png'); +} +section.role-introduction>h1:before { + content:url('/adm/daxe/images/section_icons/introduction.png'); +} +section.role-method>h1:before { + content:url('/adm/daxe/images/section_icons/method.png'); +} +section.role-more_information>h1:before { + content:url('/adm/daxe/images/section_icons/more_information.png'); +} +section.role-objectives>h1:before { + content:url('/adm/daxe/images/section_icons/objectives.png'); +} +section.role-prerequisites>h1:before { + content:url('/adm/daxe/images/section_icons/prerequisites.png'); +} +section.role-remark>h1:before { + content:url('/adm/daxe/images/section_icons/remark.png'); +} +section.role-reminder>h1:before { + content:url('/adm/daxe/images/section_icons/reminder.png'); +} +section.role-summary>h1:before { + content:url('/adm/daxe/images/section_icons/summary.png'); +} +section.role-syntax>h1:before { + content:url('/adm/daxe/images/section_icons/syntax.png'); +} +section.role-warning>h1:before { + content:url('/adm/daxe/images/section_icons/warning.png'); +} + END } @@ -7257,6 +8133,7 @@ sub headtag { my $function = $args->{'function'} || &get_users_function(); my $domain = $args->{'domain'} || &determinedomain(); my $bgcolor = $args->{'bgcolor'} || &designparm($function.'.pgbg',$domain); + my $httphost = $args->{'use_absolute'}; my $url = join(':',$env{'user.name'},$env{'user.domain'}, $Apache::lonnet::perlvar{'lonVersion'}, #time(), @@ -7267,9 +8144,12 @@ sub headtag { my $result = ''. - &font_settings(); + &font_settings($args); - my $inhibitprint = &print_suppression(); + my $inhibitprint; + if ($args->{'print_suppress'}) { + $inhibitprint = &print_suppression(); + } if (!$args->{'frameset'}) { $result .= &Apache::lonhtmlcommon::htmlareaheaders(); @@ -7280,7 +8160,7 @@ sub headtag { if (!$args->{'no_nav_bar'} && !$args->{'only_body'} && !$args->{'frameset'}) { - $result .= &help_menu_js(); + $result .= &help_menu_js($httphost); $result.=&modal_window(); $result.=&togglebox_script(); $result.=&wishlist_window(); @@ -7309,16 +8189,102 @@ sub headtag { ADDMETA + } else { + unless (($args->{'frameset'}) || ($args->{'js_ready'}) || ($args->{'only_body'}) || ($args->{'no_nav_bar'})) { + my $requrl = $env{'request.uri'}; + if ($requrl eq '') { + $requrl = $ENV{'REQUEST_URI'}; + $requrl =~ s/\?.+$//; + } + unless (($requrl =~ m{^/adm/(?:switchserver|login|authenticate|logout|groupsort|cleanup|helper|slotrequest|grades)(\?|$)}) || + (($requrl =~ m{^/res/}) && (($env{'form.submitted'} eq 'scantron') || + ($env{'form.grade_symb'}) || ($Apache::lonhomework::scantronmode)))) { + 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); + 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(); + } + 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". + &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'}) { + $newurl .= '&symb='.$env{'request.symb'}; + } else { + $newurl .= '&origurl='.$requrl; + } + } + &js_escape(\$msg); + $result.=< + +OFFLOAD + } + } + } + } + } + } } if (!defined($title)) { $title = 'The LearningOnline Network with CAPA'; } if (!$args->{'no_auto_mt_title'}) { $title = &mt($title); } $result .= ' LON-CAPA '.$title.'' - .'' + .'{'frameset'}) { + $result .= ' /'; + } + $result .= '>' .$inhibitprint .$head_extra; - if ($env{'browser.mobile'}) { + my $clientmobile; + if (($env{'user.name'} eq '') && ($env{'user.domain'} eq '')) { + (undef,undef,undef,undef,undef,undef,$clientmobile) = &decode_user_agent(); + } else { + $clientmobile = $env{'browser.mobile'}; + } + if ($clientmobile) { $result .= ' '; @@ -7332,15 +8298,21 @@ ADDMETA Returns neccessary to set the proper encoding -Inputs: none +Inputs: optional reference to HASH -- $args passed to &headtag() =cut sub font_settings { + my ($args) = @_; my $headerstring=''; - if (!$env{'browser.mathml'} && $env{'browser.unicode'}) { - $headerstring.= - ''; + if ((!$env{'browser.mathml'} && $env{'browser.unicode'}) || + ((ref($args) eq 'HASH') && ($args->{'browser.unicode'}))) { + $headerstring.= + '{'frameset'}) { + $headerstring.= ' /'; + } + $headerstring .= '>'."\n"; } return $headerstring; } @@ -7385,7 +8357,7 @@ 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); + my $blocked = &blocking_status('printout',$cnum,$cdom,undef,1); if ($blocked) { my $checkrole = "cm./$cdom/$cnum"; if ($env{'request.course.sec'} ne '') { @@ -7432,6 +8404,7 @@ Inputs: none =cut sub xml_begin { + my ($is_frameset) = @_; my $output=''; if ($env{'browser.mathml'}) { @@ -7443,9 +8416,12 @@ sub xml_begin { .'' .''; + } elsif ($is_frameset) { + $output=''."\n". + ''."\n"; } else { - $output='' - .''; + $output=''."\n". + ''."\n"; } return $output; } @@ -7490,9 +8466,6 @@ $args - additional optional args support head -> skip the generation body -> skip all generation no_auto_mt_title -> prevent &mt()ing the title arg - inherit_jsmath -> when creating popup window in a page, - should it have jsmath forced on by the - current page bread_crumbs -> Array containing breadcrumbs bread_crumbs_component -> if exists show it as headline else show only the breadcrumbs group -> includes the current group, if page is for a @@ -7512,7 +8485,7 @@ sub start_page { my ($result,@advtools); if (! exists($args->{'skip_phases'}{'head'}) ) { - $result .= &xml_begin() . &headtag($title, $head_extra, $args); + $result .= &xml_begin($args->{'frameset'}) . &headtag($title, $head_extra, $args); } if (! exists($args->{'skip_phases'}{'body'}) ) { @@ -7564,7 +8537,10 @@ sub start_page { #if bread_crumbs_component exists show it as headline else show only the breadcrumbs if(exists($args->{'bread_crumbs_component'})){ $result .= &Apache::lonhtmlcommon::breadcrumbs($args->{'bread_crumbs_component'}); - }else{ + } elsif ($args->{'crstype'} eq 'Placement') { + $result .= &Apache::lonhtmlcommon::breadcrumbs('','','','','','','','','', + $args->{'crstype'}); + } else { $result .= &Apache::lonhtmlcommon::breadcrumbs(); } } @@ -7613,9 +8589,13 @@ function set_wishlistlink(title, path) { title = document.title; title = title.replace(/^LON-CAPA /,''); } + title = encodeURIComponent(title); + title = title.replace("'","\\\'"); if (!path) { path = location.pathname; } + path = encodeURIComponent(path); + path = path.replace("'","\\\'"); Win = window.open('/adm/wishlist?mode=newLink&setTitle='+title+'&setPath='+path, 'wishlistNewLink','width=560,height=350,scrollbars=0'); } @@ -7658,12 +8638,13 @@ var modalWindow = { }; var openMyModal = function(source,width,height,scrolling,transparency,style) { + source = source.replace("'","'"); modalWindow.windowId = "myModal"; modalWindow.width = width; modalWindow.height = height; - modalWindow.content = ""; modalWindow.open(); - }; + }; // END LON-CAPA Internal --> // ]]> @@ -8040,10 +9021,16 @@ function expand_div(caller) { } sub simple_error_page { - my ($r,$title,$msg) = @_; + my ($r,$title,$msg,$args) = @_; + if (ref($args) eq 'HASH') { + if (!$args->{'no_auto_mt_msg'}) { $msg = &mt($msg); } + } else { + $msg = &mt($msg); + } + my $page = &Apache::loncommon::start_page($title). - '

'.&mt($msg).'

'. + '

'.$msg.'

'. &Apache::loncommon::end_page(); if (ref($r)) { $r->print($page); @@ -8259,7 +9246,7 @@ role status: active, previous or future. sub check_user_status { my ($udom,$uname,$cdom,$crs,$role,$sec) = @_; my %userinfo = &Apache::lonnet::dump('roles',$udom,$uname); - my @uroles = keys %userinfo; + my @uroles = keys(%userinfo); my $srchstr; my $active_chk = 'none'; my $now = time; @@ -8670,11 +9657,11 @@ Incoming parameters: 2. user's domain 3. quota name - portfolio, author, or course (if no quota name provided, defaults to portfolio). -4. crstype - official, unofficial or community, if quota name is - course +4. crstype - official, unofficial, textbook, placement or community, + if quota name is course Returns: -1. Disk quota (in Mb) assigned to student. +1. Disk quota (in MB) assigned to student. 2. (Optional) Type of setting: custom or default (individually assigned or default for user's institutional status). @@ -8744,7 +9731,9 @@ sub get_user_quota { if ($quota eq '' || wantarray) { if ($quotaname eq 'course') { my %domdefs = &Apache::lonnet::get_domain_defaults($udom); - if (($crstype eq 'official') || ($crstype eq 'unofficial') || ($crstype eq 'community')) { + if (($crstype eq 'official') || ($crstype eq 'unofficial') || + ($crstype eq 'community') || ($crstype eq 'textbook') || + ($crstype eq 'placement')) { $defquota = $domdefs{$crstype.'quota'}; } if ($defquota eq '') { @@ -8778,6 +9767,7 @@ Retrieves default quota assigned for sto given an (optional) user's institutional status. Incoming parameters: + 1. domain 2. (Optional) institutional status(es). This is a : separated list of status types (e.g., faculty, staff, student etc.) @@ -8788,21 +9778,20 @@ Incoming parameters: (if no quota name provided, defaults to portfolio). Returns: -1. Default disk quota (in Mb) for user portfolios in the domain. + +1. Default disk quota (in MB) for user portfolios in the domain. 2. (Optional) institutional type which determined the value of the default quota. If a value has been stored in the domain's configuration db, it will return that, otherwise it returns 20 (for backwards compatibility with domains which have not set up a configuration -db file; the original statically defined portfolio quota was 20 Mb). +db file; the original statically defined portfolio quota was 20 MB). If the user's status includes multiple types (e.g., staff and student), the largest default quota which applies to the user determines the default quota returned. -=back - =cut ############################################### @@ -8880,28 +9869,31 @@ sub default_quota { Returns warning message if upload of file to authoring space, or copying of existing file within authoring space will cause quota for the authoring -space to be exceeded, +space to be exceeded. Same, if upload of a file directly to a course/community via Course Editor will cause quota for uploaded content for the course to be exceeded. -Inputs: 6 +Inputs: 7 1. username or coursenum 2. domain 3. context ('author' or 'course') 4. filename of file for which action is being requested 5. filesize (kB) of file 6. action being taken: copy or upload. +7. quotatype (in course context -- official, unofficial, textbook, placement or community). Returns: 1 scalar: HTML to display containing warning if quota would be exceeded, - otherwise return null. + otherwise return null. + +=back =cut sub excess_filesize_warning { - my ($uname,$udom,$context,$filename,$filesize,$action) = @_; + my ($uname,$udom,$context,$filename,$filesize,$action,$quotatype) = @_; my $current_disk_usage = 0; - my $disk_quota = &get_user_quota($uname,$udom,$context); #expressed in MB + my $disk_quota = &get_user_quota($uname,$udom,$context,$quotatype); #expressed in MB if ($context eq 'author') { my $authorspace = $Apache::lonnet::perlvar{'lonDocRoot'}."/priv/$udom/$uname"; $current_disk_usage = &Apache::lonnet::diskusage($udom,$uname,$authorspace); @@ -8912,10 +9904,10 @@ sub excess_filesize_warning { } $disk_quota = int($disk_quota * 1000); if (($current_disk_usage + $filesize) > $disk_quota) { - return '

'. + return '

'. &mt("Unable to $action [_1]. (size = [_2] kilobytes). Disk quota will be exceeded.", - ''.$filename.'',$filesize).''. - '
'.&mt('Disk quota is [_1] kilobytes. Your current disk usage is [_2] kilobytes.', + ''.$filename.'',$filesize).'

'. + '

'.&mt('Disk quota is [_1] kilobytes. Your current disk usage is [_2] kilobytes.', $disk_quota,$current_disk_usage). '

'; } @@ -8987,7 +9979,7 @@ sub user_picker { } $srchterm = $srch->{'srchterm'}; } - my %lt=&Apache::lonlocal::texthash( + my %html_lt=&Apache::lonlocal::texthash( 'usr' => 'Search criteria', 'doma' => 'Domain/institution to search', 'uname' => 'username', @@ -9000,6 +9992,8 @@ sub user_picker { 'exact' => 'is', 'contains' => 'contains', 'begins' => 'begins with', + ); + my %js_lt=&Apache::lonlocal::texthash( 'youm' => "You must include some text to search for.", 'thte' => "The text you are searching for must contain at least two characters when using a 'begins' type search.", 'thet' => "The text you are searching for must contain at least three characters when using a 'contains' type search.", @@ -9009,6 +10003,8 @@ sub user_picker { 'whse' => "When searching by last,first you must include at least one character in the first name.", 'thfo' => "The following need to be corrected before the search can be run:", ); + &html_escape(\%html_lt); + &js_escape(\%js_lt); my $domform = &select_dom_form($currdom,'srchdomain',1,1); my $srchinsel = ' \n"; @@ -9035,10 +10031,10 @@ sub user_picker { foreach my $option ('lastname','lastfirst','uname') { if ($curr_selected{'srchby'} eq $option) { $srchbysel .= ' - '; + '; } else { $srchbysel .= ' - '; + '; } } $srchbysel .= "\n \n"; @@ -9047,10 +10043,10 @@ sub user_picker { foreach my $option ('begins','contains','exact') { if ($curr_selected{'srchtype'} eq $option) { $srchtypesel .= ' - '; + '; } else { $srchtypesel .= ' - '; + '; } } $srchtypesel .= "\n \n"; @@ -9135,46 +10131,46 @@ function validateEntry(callingForm) { if (srchterm == "") { checkok = 0; - msg += "$lt{'youm'}\\n"; + msg += "$js_lt{'youm'}\\n"; } if (srchtype== 'begins') { if (srchterm.length < 2) { checkok = 0; - msg += "$lt{'thte'}\\n"; + msg += "$js_lt{'thte'}\\n"; } } if (srchtype== 'contains') { if (srchterm.length < 3) { checkok = 0; - msg += "$lt{'thet'}\\n"; + msg += "$js_lt{'thet'}\\n"; } } if (srchin == 'instd') { if (srchdomain == '') { checkok = 0; - msg += "$lt{'yomc'}\\n"; + msg += "$js_lt{'yomc'}\\n"; } } if (srchin == 'dom') { if (srchdomain == '') { checkok = 0; - msg += "$lt{'ymcd'}\\n"; + msg += "$js_lt{'ymcd'}\\n"; } } if (srchby == 'lastfirst') { if (srchterm.indexOf(",") == -1) { checkok = 0; - msg += "$lt{'whus'}\\n"; + msg += "$js_lt{'whus'}\\n"; } if (srchterm.indexOf(",") == srchterm.length -1) { checkok = 0; - msg += "$lt{'whse'}\\n"; + msg += "$js_lt{'whse'}\\n"; } } if (checkok == 0) { - alert("$lt{'thfo'}\\n"+msg); + alert("$js_lt{'thfo'}\\n"+msg); return; } if (checkok == 1) { @@ -9192,10 +10188,10 @@ $new_user_create END_BLOCK $output .= &Apache::lonhtmlcommon::start_pick_box(). - &Apache::lonhtmlcommon::row_title($lt{'doma'}). + &Apache::lonhtmlcommon::row_title($html_lt{'doma'}). $domform. &Apache::lonhtmlcommon::row_closure(). - &Apache::lonhtmlcommon::row_title($lt{'usr'}). + &Apache::lonhtmlcommon::row_title($html_lt{'usr'}). $srchbysel. $srchtypesel. ''. @@ -9208,56 +10204,160 @@ END_BLOCK sub user_rule_check { my ($usershash,$checks,$alerts,$rulematch,$inst_results,$curr_rules,$got_rules) = @_; - my $response; + my ($response,%inst_response); if (ref($usershash) eq 'HASH') { - foreach my $user (keys(%{$usershash})) { - my ($uname,$udom) = split(/:/,$user); - next if ($udom eq '' || $uname eq ''); - my ($id,$newuser); - if (ref($usershash->{$user}) eq 'HASH') { - $newuser = $usershash->{$user}->{'newuser'}; - $id = $usershash->{$user}->{'id'}; - } - my $inst_response; + if (keys(%{$usershash}) > 1) { + my (%by_username,%by_id,%userdoms); + my $checkid; if (ref($checks) eq 'HASH') { - if (defined($checks->{'username'})) { - ($inst_response,%{$inst_results->{$user}}) = - &Apache::lonnet::get_instuser($udom,$uname); - } elsif (defined($checks->{'id'})) { - ($inst_response,%{$inst_results->{$user}}) = - &Apache::lonnet::get_instuser($udom,undef,$id); + if ((!defined($checks->{'username'})) && (defined($checks->{'id'}))) { + $checkid = 1; + } + } + foreach my $user (keys(%{$usershash})) { + my ($uname,$udom) = split(/:/,$user); + if ($checkid) { + if (ref($usershash->{$user}) eq 'HASH') { + if ($usershash->{$user}->{'id'} ne '') { + $by_id{$udom}{$usershash->{$user}->{'id'}} = $uname; + $userdoms{$udom} = 1; + if (ref($inst_results) eq 'HASH') { + $inst_results->{$uname.':'.$udom} = {}; + } + } + } + } else { + $by_username{$udom}{$uname} = 1; + $userdoms{$udom} = 1; + if (ref($inst_results) eq 'HASH') { + $inst_results->{$uname.':'.$udom} = {}; + } + } + } + foreach my $udom (keys(%userdoms)) { + if (!$got_rules->{$udom}) { + my %domconfig = &Apache::lonnet::get_dom('configuration', + ['usercreation'],$udom); + if (ref($domconfig{'usercreation'}) eq 'HASH') { + foreach my $item ('username','id') { + if (ref($domconfig{'usercreation'}{$item.'_rule'}) eq 'ARRAY') { + $$curr_rules{$udom}{$item} = + $domconfig{'usercreation'}{$item.'_rule'}; + } + } + } + $got_rules->{$udom} = 1; + } + } + if ($checkid) { + foreach my $udom (keys(%by_id)) { + my ($outcome,$results) = &Apache::lonnet::get_multiple_instusers($udom,$by_id{$udom},'id'); + if ($outcome eq 'ok') { + foreach my $id (keys(%{$by_id{$udom}})) { + my $uname = $by_id{$udom}{$id}; + $inst_response{$uname.':'.$udom} = $outcome; + } + if (ref($results) eq 'HASH') { + foreach my $uname (keys(%{$results})) { + if (exists($inst_response{$uname.':'.$udom})) { + $inst_response{$uname.':'.$udom} = $outcome; + $inst_results->{$uname.':'.$udom} = $results->{$uname}; + } + } + } + } } } else { - ($inst_response,%{$inst_results->{$user}}) = - &Apache::lonnet::get_instuser($udom,$uname); - return; + foreach my $udom (keys(%by_username)) { + my ($outcome,$results) = &Apache::lonnet::get_multiple_instusers($udom,$by_username{$udom}); + if ($outcome eq 'ok') { + foreach my $uname (keys(%{$by_username{$udom}})) { + $inst_response{$uname.':'.$udom} = $outcome; + } + if (ref($results) eq 'HASH') { + foreach my $uname (keys(%{$results})) { + $inst_results->{$uname.':'.$udom} = $results->{$uname}; + } + } + } + } } - if (!$got_rules->{$udom}) { - my %domconfig = &Apache::lonnet::get_dom('configuration', - ['usercreation'],$udom); - if (ref($domconfig{'usercreation'}) eq 'HASH') { - foreach my $item ('username','id') { - if (ref($domconfig{'usercreation'}{$item.'_rule'}) eq 'ARRAY') { - $$curr_rules{$udom}{$item} = - $domconfig{'usercreation'}{$item.'_rule'}; + } elsif (keys(%{$usershash}) == 1) { + my $user = (keys(%{$usershash}))[0]; + my ($uname,$udom) = split(/:/,$user); + if (($udom ne '') && ($uname ne '')) { + if (ref($usershash->{$user}) eq 'HASH') { + if (ref($checks) eq 'HASH') { + if (defined($checks->{'username'})) { + ($inst_response{$user},%{$inst_results->{$user}}) = + &Apache::lonnet::get_instuser($udom,$uname); + } elsif (defined($checks->{'id'})) { + if ($usershash->{$user}->{'id'} ne '') { + ($inst_response{$user},%{$inst_results->{$user}}) = + &Apache::lonnet::get_instuser($udom,undef, + $usershash->{$user}->{'id'}); + } else { + ($inst_response{$user},%{$inst_results->{$user}}) = + &Apache::lonnet::get_instuser($udom,$uname); + } + } + } else { + ($inst_response{$user},%{$inst_results->{$user}}) = + &Apache::lonnet::get_instuser($udom,$uname); + return; + } + if (!$got_rules->{$udom}) { + my %domconfig = &Apache::lonnet::get_dom('configuration', + ['usercreation'],$udom); + if (ref($domconfig{'usercreation'}) eq 'HASH') { + foreach my $item ('username','id') { + if (ref($domconfig{'usercreation'}{$item.'_rule'}) eq 'ARRAY') { + $$curr_rules{$udom}{$item} = + $domconfig{'usercreation'}{$item.'_rule'}; + } + } } + $got_rules->{$udom} = 1; } } - $got_rules->{$udom} = 1; + } else { + return; + } + } else { + return; + } + foreach my $user (keys(%{$usershash})) { + my ($uname,$udom) = split(/:/,$user); + next if (($udom eq '') || ($uname eq '')); + my $id; + if (ref($inst_results) eq 'HASH') { + if (ref($inst_results->{$user}) eq 'HASH') { + $id = $inst_results->{$user}->{'id'}; + } + } + if ($id eq '') { + if (ref($usershash->{$user})) { + $id = $usershash->{$user}->{'id'}; + } } foreach my $item (keys(%{$checks})) { if (ref($$curr_rules{$udom}) eq 'HASH') { if (ref($$curr_rules{$udom}{$item}) eq 'ARRAY') { if (@{$$curr_rules{$udom}{$item}} > 0) { - my %rule_check = &Apache::lonnet::inst_rulecheck($udom,$uname,$id,$item,$$curr_rules{$udom}{$item}); + my %rule_check = &Apache::lonnet::inst_rulecheck($udom,$uname,$id,$item, + $$curr_rules{$udom}{$item}); foreach my $rule (@{$$curr_rules{$udom}{$item}}) { if ($rule_check{$rule}) { $$rulematch{$user}{$item} = $rule; - if ($inst_response eq 'ok') { + if ($inst_response{$user} eq 'ok') { if (ref($inst_results) eq 'HASH') { if (ref($inst_results->{$user}) eq 'HASH') { if (keys(%{$inst_results->{$user}}) == 0) { $$alerts{$item}{$udom}{$uname} = 1; + } elsif ($item eq 'id') { + if ($inst_results->{$user}->{'id'} eq '') { + $$alerts{$item}{$udom}{$uname} = 1; + } } } } @@ -9368,7 +10468,14 @@ sub personal_data_fieldtitles { sub sorted_inst_types { my ($dom) = @_; - my ($usertypes,$order) = &Apache::lonnet::retrieve_inst_usertypes($dom); + my ($usertypes,$order); + my %domdefaults = &Apache::lonnet::get_domain_defaults($dom); + if (ref($domdefaults{'inststatus'}) eq 'HASH') { + $usertypes = $domdefaults{'inststatus'}{'inststatustypes'}; + $order = $domdefaults{'inststatus'}{'inststatusorder'}; + } else { + ($usertypes,$order) = &Apache::lonnet::retrieve_inst_usertypes($dom); + } my $othertitle = &mt('All users'); if ($env{'request.course.id'}) { $othertitle = &mt('Any users'); @@ -9536,13 +10643,35 @@ future_reservable - ref to hash of stude sub get_future_slots { my ($cnum,$cdom,$now,$symb) = @_; + my $map; + if ($symb) { + ($map) = &Apache::lonnet::decode_symb($symb); + } my (%reservable_now,%future_reservable,@sorted_reservable,@sorted_future); my %slots = &Apache::lonnet::get_course_slots($cnum,$cdom); foreach my $slot (keys(%slots)) { next unless($slots{$slot}->{'type'} eq 'schedulable_student'); if ($symb) { - next if (($slots{$slot}->{'symb'} ne '') && - ($slots{$slot}->{'symb'} ne $symb)); + if ($slots{$slot}->{'symb'} ne '') { + my $canuse; + my %oksymbs; + my @slotsymbs = split(/\s*,\s*/,$slots{$slot}->{'symb'}); + map { $oksymbs{$_} = 1; } @slotsymbs; + if ($oksymbs{$symb}) { + $canuse = 1; + } else { + foreach my $item (@slotsymbs) { + if ($item =~ /\.(page|sequence)$/) { + (undef,undef,my $sloturl) = &Apache::lonnet::decode_symb($item); + if (($map ne '') && ($map eq $sloturl)) { + $canuse = 1; + last; + } + } + } + } + next unless ($canuse); + } } if (($slots{$slot}->{'starttime'} > $now) && ($slots{$slot}->{'endtime'} > $now)) { @@ -9754,7 +10883,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); @@ -9766,11 +10911,13 @@ sub ask_for_embedded_content { my $numexisting = 0; my $numunused = 0; my ($output,$upload_output,$toplevel,$url,$udom,$uname,$getpropath,$cdom,$cnum, - $fileloc,$filename,$delete_output,$modify_output,$title,$symb,$path); + $fileloc,$filename,$delete_output,$modify_output,$title,$symb,$path,$navmap); my $heading = &mt('Upload embedded files'); my $buttontext = &mt('Upload'); - my ($navmap,$cdom,$cnum); + # 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(); @@ -9832,7 +10979,15 @@ sub ask_for_embedded_content { ($path) = ($toplevel =~ m{^(\Q/uploaded/$cdom/$cnum/\E(?:docs|supplemental)/(?:default|\d+)/\d+)/}); } - $fileloc = &Apache::lonnet::filelocation('',$toplevel); + if ($toplevel=~/^\/*(uploaded|editupload)/) { + $fileloc = $toplevel; + $fileloc=~ s/^\s*(\S+)\s*$/$1/; + my ($udom,$uname,$fname) = + ($fileloc=~ m{^/+(?:uploaded|editupload)/+($match_domain)/+($match_name)/+(.*)$}); + $fileloc = propath($udom,$uname).'/userfiles/'.$fname; + } else { + $fileloc = &Apache::lonnet::filelocation('',$toplevel); + } $fileloc =~ s{^/}{}; ($filename) = ($fileloc =~ m{.+/([^/]+)$}); $heading = &mt('Status of dependencies in [_1]',"$title ($filename)"); @@ -9847,6 +11002,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(.+)$})) { @@ -9854,17 +11019,18 @@ sub ask_for_embedded_content { } else { $embed_file = $file; } - my $absolutepath; + my ($absolutepath,$cleaned_file); if ($embed_file =~ m{^\w+://}) { - $newfiles{$embed_file} = 1; - $mapping{$embed_file} = $embed_file; + $cleaned_file = $embed_file; + $newfiles{$cleaned_file} = 1; + $mapping{$cleaned_file} = $embed_file; } else { + $cleaned_file = &clean_path($embed_file); if ($embed_file =~ m{^/}) { $absolutepath = $embed_file; - $embed_file =~ s{^(/+)}{}; } - if ($embed_file =~ m{/}) { - my ($path,$fname) = ($embed_file =~ m{^(.+)/([^/]*)$}); + if ($cleaned_file =~ m{/}) { + my ($path,$fname) = ($cleaned_file =~ m{^(.+)/([^/]*)$}); $path = &check_for_traversal($path,$url,$toplevel); my $item = $fname; if ($path ne '') { @@ -9881,13 +11047,26 @@ sub ask_for_embedded_content { } else { $dependencies{$embed_file} = 1; if ($absolutepath) { - $mapping{$embed_file} = $absolutepath; + $mapping{$cleaned_file} = $absolutepath; } else { - $mapping{$embed_file} = $embed_file; + $mapping{$cleaned_file} = $embed_file; } } } } + + # 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} = {}; @@ -9963,6 +11142,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')) { @@ -10001,6 +11183,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) { @@ -10029,6 +11213,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)); @@ -10040,6 +11226,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+://}); @@ -10257,6 +11449,46 @@ sub ask_for_embedded_content { return ($output,$counter,$numpathchg); } +=pod + +=item * clean_path($name) + +Performs clean-up of directories, subdirectories and filename in an +embedded object, referenced in an HTML file which is being uploaded +to a course or portfolio, where +"Upload embedded images/multimedia files if HTML file" checkbox was +checked. + +Clean-up is similar to replacements in lonnet::clean_filename() +except each / between sub-directory and next level is preserved. + +=cut + +sub clean_path { + my ($embed_file) = @_; + $embed_file =~s{^/+}{}; + my @contents; + if ($embed_file =~ m{/}) { + @contents = split(/\//,$embed_file); + } else { + @contents = ($embed_file); + } + my $lastidx = scalar(@contents)-1; + for (my $i=0; $i<=$lastidx; $i++) { + $contents[$i]=~s{\\}{/}g; + $contents[$i]=~s/\s+/\_/g; + $contents[$i]=~s{[^/\w\.\-]}{}g; + if ($i == $lastidx) { + $contents[$i]=~s/\.(\d+)(?=\.)/_$1/g; + } + } + if ($lastidx > 0) { + return join('/',@contents); + } else { + return $contents[0]; + } +} + sub embedded_file_element { my ($context,$num,$embed_file,$mapping,$allfiles,$codebase,$type) = @_; return unless ((ref($mapping) eq 'HASH') && (ref($allfiles) eq 'HASH') && @@ -10381,7 +11613,8 @@ sub upload_embedded { # Check if extension is valid if (($fname =~ /\.(\w+)$/) && (&Apache::loncommon::fileembstyle($1) eq 'hdn')) { - $output .= &mt('Invalid file extension ([_1]) - reserved for LONCAPA use - rename the file with a different extension and re-upload. ',$1).'
'; + $output .= &mt('Invalid file extension ([_1]) - reserved for internal use.',$1) + .' '.&mt('Rename the file with a different extension and re-upload.').'
'; next; } elsif (($fname =~ /\.(\w+)$/) && (!defined(&Apache::loncommon::fileembstyle($1)))) { @@ -10645,6 +11878,7 @@ sub modify_html_refs { my $numchg = ($content =~ s{($attrib_regexp\s*=\s*['"]?)\Q$ref\E(['"]?)}{$1$newname$2}gi); $count += $numchg; $allfiles{$newname} = $allfiles{$ref}; + delete($allfiles{$ref}); } if ($env{'form.embedded_codebase_'.$i} ne '') { $codebase = &unescape($env{'form.embedded_codebase_'.$i}); @@ -10820,11 +12054,11 @@ sub check_for_upload { if ($currsize < $filesize) { my $extra = $filesize - $currsize; if (($current_disk_usage + $extra) > $disk_quota) { - my $msg = ''. + my $msg = '

'. &mt('Unable to upload [_1]. (size = [_2] kilobytes). Disk quota will be exceeded if existing (smaller) file with same name (size = [_3] kilobytes) is replaced.', - ''.$fname.'',$filesize,$currsize).''. - '
'.&mt('Disk quota is [_1] kilobytes. Your current disk usage is [_2] kilobytes.', - $disk_quota,$current_disk_usage); + ''.$fname.'',$filesize,$currsize).'

'. + '

'.&mt('Disk quota is [_1] kilobytes. Your current disk usage is [_2] kilobytes.', + $disk_quota,$current_disk_usage).'

'; return ('will_exceed_quota',$msg); } } @@ -10833,21 +12067,21 @@ sub check_for_upload { } } if (($current_disk_usage + $filesize) > $disk_quota){ - my $msg = ''. - &mt('Unable to upload [_1]. (size = [_2] kilobytes). Disk quota will be exceeded.',''.$fname.'',$filesize).''. - '
'.&mt('Disk quota is [_1] kilobytes. Your current disk usage is [_2] kilobytes.',$disk_quota,$current_disk_usage); + my $msg = '

'. + &mt('Unable to upload [_1]. (size = [_2] kilobytes). Disk quota will be exceeded.',''.$fname.'',$filesize).'

'. + '

'.&mt('Disk quota is [_1] kilobytes. Your current disk usage is [_2] kilobytes.',$disk_quota,$current_disk_usage).'

'; return ('will_exceed_quota',$msg); } elsif ($found_file) { if ($locked_file) { - my $msg = ''; + my $msg = '

'; $msg .= &mt('Unable to upload [_1]. A locked file by that name was found in [_2].',''.$fname.'',''.$port_path.$env{'form.currentpath'}.''); - $msg .= '
'; + $msg .= '

'; $msg .= &mt('You will be able to rename or delete existing [_1] after a grade has been assigned.',''.$fname.''); return ('file_locked',$msg); } else { - my $msg = ''; + my $msg = '

'; $msg .= &mt(' A file by that name: [_1] was found in [_2].',''.$fname.'',$port_path.$env{'form.currentpath'}); - $msg .= ''; + $msg .= '

'; return ('existingfile',$msg); } } @@ -10938,16 +12172,66 @@ sub decompress_form { } } if ($mimetype =~ m{^application/(x\-)?(compressed|zip)}) { - my @camtasia = ("$topdir/","$topdir/index.html", + my @camtasia6 = ("$topdir/","$topdir/index.html", "$topdir/media/", "$topdir/media/$topdir.mp4", "$topdir/media/FirstFrame.png", "$topdir/media/player.swf", "$topdir/media/swfobject.js", "$topdir/media/expressInstall.swf"); - my @diffs = &compare_arrays(\@paths,\@camtasia); + my @camtasia8_1 = ("$topdir/","$topdir/$topdir.html", + "$topdir/$topdir.mp4", + "$topdir/$topdir\_config.xml", + "$topdir/$topdir\_controller.swf", + "$topdir/$topdir\_embed.css", + "$topdir/$topdir\_First_Frame.png", + "$topdir/$topdir\_player.html", + "$topdir/$topdir\_Thumbnails.png", + "$topdir/playerProductInstall.swf", + "$topdir/scripts/", + "$topdir/scripts/config_xml.js", + "$topdir/scripts/handlebars.js", + "$topdir/scripts/jquery-1.7.1.min.js", + "$topdir/scripts/jquery-ui-1.8.15.custom.min.js", + "$topdir/scripts/modernizr.js", + "$topdir/scripts/player-min.js", + "$topdir/scripts/swfobject.js", + "$topdir/skins/", + "$topdir/skins/configuration_express.xml", + "$topdir/skins/express_show/", + "$topdir/skins/express_show/player-min.css", + "$topdir/skins/express_show/spritesheet.png"); + my @camtasia8_4 = ("$topdir/","$topdir/$topdir.html", + "$topdir/$topdir.mp4", + "$topdir/$topdir\_config.xml", + "$topdir/$topdir\_controller.swf", + "$topdir/$topdir\_embed.css", + "$topdir/$topdir\_First_Frame.png", + "$topdir/$topdir\_player.html", + "$topdir/$topdir\_Thumbnails.png", + "$topdir/playerProductInstall.swf", + "$topdir/scripts/", + "$topdir/scripts/config_xml.js", + "$topdir/scripts/techsmith-smart-player.min.js", + "$topdir/skins/", + "$topdir/skins/configuration_express.xml", + "$topdir/skins/express_show/", + "$topdir/skins/express_show/spritesheet.min.css", + "$topdir/skins/express_show/spritesheet.png", + "$topdir/skins/express_show/techsmith-smart-player.min.css"); + my @diffs = &compare_arrays(\@paths,\@camtasia6); if (@diffs == 0) { - $is_camtasia = 1; + $is_camtasia = 6; + } else { + @diffs = &compare_arrays(\@paths,\@camtasia8_1); + if (@diffs == 0) { + $is_camtasia = 8; + } else { + @diffs = &compare_arrays(\@paths,\@camtasia8_4); + if (@diffs == 0) { + $is_camtasia = 8; + } + } } } my $output; @@ -10959,8 +12243,7 @@ sub decompress_form { function camtasiaToggle() { for (var i=0; i'. ''.$lt{'proa'}.' 
'. @@ -11145,7 +12428,7 @@ sub decompress_uploaded_file { sub process_decompression { my ($docudom,$docuname,$file,$destination,$dir_root,$hiddenelem) = @_; my ($dir,$error,$warning,$output); - if ($file !~ /\.(zip|tar|bz2|gz|tar.gz|tar.bz2|tgz)$/) { + if ($file !~ /\.(zip|tar|bz2|gz|tar.gz|tar.bz2|tgz)$/i) { $error = &mt('Filename not a supported archive file type.'). '
'.&mt('Filename should end with one of: [_1].', '.zip, .tar, .bz2, .gz, .tar.gz, .tar.bz2, .tgz'); @@ -11255,6 +12538,7 @@ sub process_decompression { \%titles,\%children); } if ($env{'form.autoextract_camtasia'}) { + my $version = $env{'form.autoextract_camtasia'}; my %displayed; my $total = 1; $env{'form.archive_directory'} = []; @@ -11273,12 +12557,15 @@ sub process_decompression { $env{'form.archive_'.$i} = 'display'; $env{'form.archive_title_'.$i} = $env{'form.camtasia_foldername'}; $displayed{'folder'} = $i; - } elsif ($item eq "$contents[0]/index.html") { + } elsif ((($item eq "$contents[0]/index.html") && ($version == 6)) || + (($item eq "$contents[0]/$contents[0]".'.html') && ($version == 8))) { $env{'form.archive_'.$i} = 'display'; $env{'form.archive_title_'.$i} = $env{'form.camtasia_moviename'}; $displayed{'web'} = $i; } else { - if ($item eq "$contents[0]/media") { + if ((($item eq "$contents[0]/media") && ($version == 6)) || + ((($item eq "$contents[0]/scripts") || ($item eq "$contents[0]/skins") || + ($item eq "$contents[0]/skins/express_show")) && ($version == 8))) { push(@{$env{'form.archive_directory'}},$i); } $env{'form.archive_'.$i} = 'dependency'; @@ -12005,7 +13292,7 @@ sub cleanup_empty_dirs { =pod -=item &get_folder_hierarchy() +=item * &get_folder_hierarchy() Provides hierarchy of names of folders/sub-folders containing the current item, @@ -12115,6 +13402,9 @@ sub get_turnedin_filepath { my $title = $res->compTitle(); $title =~ s/\W+/_/g; if ($title ne '') { + if (($pc > 1) && (length($title) > 12)) { + $title = substr($title,0,12); + } push(@pathitems,$title); } } @@ -12123,6 +13413,9 @@ sub get_turnedin_filepath { my $maptitle = $mapres->compTitle(); $maptitle =~ s/\W+/_/g; if ($maptitle ne '') { + if (length($maptitle) > 12) { + $maptitle = substr($maptitle,0,12); + } push(@pathitems,$maptitle); } unless ($env{'request.state'} eq 'construct') { @@ -12163,6 +13456,9 @@ sub get_turnedin_filepath { $restitle = time; } } + if (length($restitle) > 12) { + $restitle = substr($restitle,0,12); + } push(@pathitems,$restitle); $path .= join('/',@pathitems); } @@ -13100,16 +14396,20 @@ sub restore_settings { =item * &build_recipient_list() -Build recipient lists for five types of e-mail: +Build recipient lists for following types of e-mail: (a) Error Reports, (b) Package Updates, (c) lonstatus warnings/errors -(d) Help requests, (e) Course requests needing approval, generated by -lonerrorhandler.pm, CHECKRPMS, loncron, lonsupportreq.pm and -loncoursequeueadmin.pm respectively. +(d) Help requests, (e) Course requests needing approval, (f) loncapa +module change checking, student/employee ID conflict checks, as +generated by lonerrorhandler.pm, CHECKRPMS, loncron, +lonsupportreq.pm, loncoursequeueadmin.pm, searchcat.pl respectively. Inputs: defmail (scalar - email address of default recipient), -mailing type (scalar - errormail, packagesmail, or helpdeskmail), +mailing type (scalar: errormail, packagesmail, helpdeskmail, +requestsmail, updatesmail, or idconflictsmail). + defdom (domain for which to retrieve configuration settings), + origmail (scalar - email address of recipient from loncapa.conf, i.e., predates configuration by DC via domainprefs.pm @@ -13174,6 +14474,87 @@ sub build_recipient_list { =pod +=over 4 + +=item * &mime_email() + +Sends an email with a possible attachment + +Inputs: + +=over 4 + +from - Sender's email address + +to - Email address of recipient + +subject - Subject of email + +body - Body of email + +cc_string - Carbon copy email address + +bcc - Blind carbon copy email address + +type - File type of attachment + +attachment_path - Path of file to be attached + +file_name - Name of file to be attached + +attachment_text - The body of an attachment of type "TEXT" + +=back + +=back + +=cut + +############################################################ +############################################################ + +sub mime_email { + my ($from, $to, $subject, $body, $cc_string, $bcc, $attachment_path, + $file_name, $attachment_text) = @_; + my $msg = MIME::Lite->new( + From => $from, + To => $to, + Subject => $subject, + Type =>'TEXT', + Data => $body, + ); + if ($cc_string ne '') { + $msg->add("Cc" => $cc_string); + } + if ($bcc ne '') { + $msg->add("Bcc" => $bcc); + } + $msg->attr("content-type" => "text/plain"); + $msg->attr("content-type.charset" => "UTF-8"); + # Attach file if given + if ($attachment_path) { + unless ($file_name) { + if ($attachment_path =~ m-/([^/]+)$-) { $file_name = $1; } + } + my ($type, $encoding) = MIME::Types::by_suffix($attachment_path); + $msg->attach(Type => $type, + Path => $attachment_path, + Filename => $file_name + ); + # Otherwise attach text if given + } elsif ($attachment_text) { + $msg->attach(Type => 'TEXT', + Data => $attachment_text); + } + # Send it + $msg->send('sendmail'); +} + +############################################################ +############################################################ + +=pod + =head1 Course Catalog Routines =over 4 @@ -13276,6 +14657,8 @@ sub extract_categories { $trailstr = &mt('Official courses (with institutional codes)'); } elsif ($name eq 'communities') { $trailstr = &mt('Communities'); + } elsif ($name eq 'placement') { + $trailstr = &mt('Placement Tests'); } else { $trailstr = $name; } @@ -13305,7 +14688,7 @@ sub extract_categories { =pod -=item *&recurse_categories() +=item * &recurse_categories() Recursively used to generate breadcrumb trails for course categories. @@ -13376,7 +14759,7 @@ sub recurse_categories { =pod -=item *&assign_categories_table() +=item * &assign_categories_table() Create a datatable for display of hierarchical categories in a domain, with checkboxes to allow a course to be categorized. @@ -13414,8 +14797,10 @@ sub assign_categories_table { next if ($parent eq 'instcode'); if ($type eq 'Community') { next unless ($parent eq 'communities'); + } elsif ($type eq 'Placement') { + next unless ($parent eq 'placement'); } else { - next if ($parent eq 'communities'); + next if (($parent eq 'communities') || ($parent eq 'placement')); } my $css_class = $itemcount%2?' class="LC_odd_row"':''; my $item = &escape($parent).'::0'; @@ -13428,6 +14813,8 @@ sub assign_categories_table { my $parent_title = $parent; if ($parent eq 'communities') { $parent_title = &mt('Communities'); + } elsif ($parent eq 'placement') { + $parent_title = &mt('Placement Tests'); } $table .= ''. '[$depth]{$parent}[$j]; $item = &escape($name).':'.&escape($parent).':'.$depth; @@ -13519,6 +14906,12 @@ sub assign_category_rows { return $text; } +=pod + +=back + +=cut + ############################################################ ############################################################ @@ -13736,34 +15129,92 @@ sub check_clone { (&Apache::lonnet::allowed('ccc',$env{'request.role.domain'}))) { $can_clone = 1; } else { - my %clonehash = &Apache::lonnet::get('environment',['cloners'], + my %clonehash = &Apache::lonnet::get('environment',['cloners','internal.coursecode'], $args->{'clonedomain'},$args->{'clonecourse'}); - my @cloners = split(/,/,$clonehash{'cloners'}); - if (grep(/^\*$/,@cloners)) { - $can_clone = 1; - } elsif (grep(/^\*\:\Q$args->{'ccdomain'}\E$/,@cloners)) { - $can_clone = 1; + if ($clonehash{'cloners'} eq '') { + my %domdefs = &Apache::lonnet::get_domain_defaults($args->{'course_domain'}); + if ($domdefs{'canclone'}) { + unless ($domdefs{'canclone'} eq 'none') { + if ($domdefs{'canclone'} eq 'domain') { + if ($args->{'ccdomain'} eq $args->{'clonedomain'}) { + $can_clone = 1; + } + } elsif (($clonehash{'internal.coursecode'}) && ($args->{'crscode'}) && + ($args->{'clonedomain'} eq $args->{'course_domain'})) { + if (&Apache::lonnet::default_instcode_cloning($args->{'clonedomain'},$domdefs{'canclone'}, + $clonehash{'internal.coursecode'},$args->{'crscode'})) { + $can_clone = 1; + } + } + } + } } else { + my @cloners = split(/,/,$clonehash{'cloners'}); + if (grep(/^\*$/,@cloners)) { + $can_clone = 1; + } elsif (grep(/^\*\:\Q$args->{'ccdomain'}\E$/,@cloners)) { + $can_clone = 1; + } elsif (grep(/^\Q$args->{'ccuname'}\E:\Q$args->{'ccdomain'}\E$/,@cloners)) { + $can_clone = 1; + } + unless ($can_clone) { + if (($clonehash{'internal.coursecode'}) && ($args->{'crscode'}) && + ($args->{'clonedomain'} eq $args->{'course_domain'})) { + my (%gotdomdefaults,%gotcodedefaults); + foreach my $cloner (@cloners) { + if (($cloner ne '*') && ($cloner !~ /^\*\:$match_domain$/) && + ($cloner !~ /^$match_username\:$match_domain$/) && ($cloner ne '')) { + my (%codedefaults,@code_order); + if (ref($gotcodedefaults{$args->{'clonedomain'}}) eq 'HASH') { + if (ref($gotcodedefaults{$args->{'clonedomain'}}{'defaults'}) eq 'HASH') { + %codedefaults = %{$gotcodedefaults{$args->{'clonedomain'}}{'defaults'}}; + } + if (ref($gotcodedefaults{$args->{'clonedomain'}}{'order'}) eq 'ARRAY') { + @code_order = @{$gotcodedefaults{$args->{'clonedomain'}}{'order'}}; + } + } else { + &Apache::lonnet::auto_instcode_defaults($args->{'clonedomain'}, + \%codedefaults, + \@code_order); + $gotcodedefaults{$args->{'clonedomain'}}{'defaults'} = \%codedefaults; + $gotcodedefaults{$args->{'clonedomain'}}{'order'} = \@code_order; + } + if (@code_order > 0) { + if (&Apache::lonnet::check_instcode_cloning(\%codedefaults,\@code_order, + $cloner,$clonehash{'internal.coursecode'}, + $args->{'crscode'})) { + $can_clone = 1; + last; + } + } + } + } + } + } + } + unless ($can_clone) { my $ccrole = 'cc'; if ($args->{'crstype'} eq 'Community') { $ccrole = 'co'; } my %roleshash = &Apache::lonnet::get_my_roles($args->{'ccuname'}, - $args->{'ccdomain'}, - 'userroles',['active'],[$ccrole], - [$args->{'clonedomain'}]); - if (($roleshash{$args->{'clonecourse'}.':'.$args->{'clonedomain'}.':'.$ccrole}) || (grep(/^\Q$args->{'ccuname'}\E:\Q$args->{'ccdomain'}\E$/,@cloners))) { + $args->{'ccdomain'}, + 'userroles',['active'],[$ccrole], + [$args->{'clonedomain'}]); + if ($roleshash{$args->{'clonecourse'}.':'.$args->{'clonedomain'}.':'.$ccrole}) { $can_clone = 1; - } elsif (&Apache::lonnet::is_course_owner($args->{'clonedomain'},$args->{'clonecourse'},$args->{'ccuname'},$args->{'ccdomain'})) { + } elsif (&Apache::lonnet::is_course_owner($args->{'clonedomain'},$args->{'clonecourse'}, + $args->{'ccuname'},$args->{'ccdomain'})) { $can_clone = 1; + } + } + 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'}); } else { - 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'}); - } 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'}); - } - } + $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'}); + } } } } @@ -13771,7 +15222,7 @@ sub check_clone { } sub construct_course { - my ($args,$logmsg,$courseid,$crsudom,$crsunum,$udom,$uname,$context,$cnum,$category) = @_; + my ($args,$logmsg,$courseid,$crsudom,$crsunum,$udom,$uname,$context,$cnum,$category,$coderef) = @_; my $outcome; my $linefeed = '
'."\n"; if ($context eq 'auto') { @@ -13799,7 +15250,12 @@ sub construct_course { # # Open course # - my $crstype = lc($args->{'crstype'}); + my $showncrstype; + if ($args->{'crstype'} eq 'Placement') { + $showncrstype = 'placement test'; + } else { + $showncrstype = lc($args->{'crstype'}); + } my %cenv=(); $$courseid=&Apache::lonnet::createcourse($args->{'course_domain'}, $args->{'cdescr'}, @@ -13816,7 +15272,7 @@ sub construct_course { # Utils::Course. This needs to at least be output as a comment # if anyone ever decides to not show this, and Utils::Course::new # will need to be suitably modified. - $outcome .= &mt('New LON-CAPA [_1] ID: [_2]',$crstype,$$courseid).$linefeed; + $outcome .= &mt('New LON-CAPA [_1] ID: [_2]',$showncrstype,$$courseid).$linefeed; if ($$courseid =~ /^error:/) { return (0,$outcome); } @@ -13836,7 +15292,7 @@ sub construct_course { # Do the cloning # if ($can_clone && $cloneid) { - $clonemsg = &mt('Cloning [_1] from [_2]',$crstype,$clonehome); + $clonemsg = &mt('Cloning [_1] from [_2]',$showncrstype,$clonehome); if ($context ne 'auto') { $clonemsg = ''.$clonemsg.''; } @@ -13868,8 +15324,12 @@ sub construct_course { 'plc.users.denied', 'hidefromcat', 'checkforpriv', - 'categories'], + 'categories', + 'internal.uniquecode'], $$crsudom,$$crsunum); + if ($args->{'textbook'}) { + $cenv{'internal.textbook'} = $args->{'textbook'}; + } } # @@ -14053,6 +15513,25 @@ sub construct_course { } } +# +# generate and store uniquecode (available to course requester), if course should have one. +# + if ($args->{'uniquecode'}) { + my ($code,$error) = &make_unique_code($$crsudom,$$crsunum); + if ($code) { + $cenv{'internal.uniquecode'} = $code; + my %crsinfo = + &Apache::lonnet::courseiddump($$crsudom,'.',1,'.','.',$$crsunum,undef,undef,'.'); + if (ref($crsinfo{$$crsudom.'_'.$$crsunum}) eq 'HASH') { + $crsinfo{$$crsudom.'_'.$$crsunum}{'uniquecode'} = $code; + my $putres = &Apache::lonnet::courseidput($$crsudom,\%crsinfo,$crsuhome,'notime'); + } + if (ref($coderef)) { + $$coderef = $code; + } + } + } + if ($args->{'disresdis'}) { $cenv{'pch.roles.denied'}='st'; } @@ -14118,14 +15597,91 @@ sub construct_course { $outcome .= ($fatal?$errtext:'write ok').$linefeed; } +# +# Set params for Placement Tests +# + if ($args->{'crstype'} eq 'Placement') { + my %storecontent; + my $prefix=$$crsudom.'_'.$$crsunum.'.0.'; + my %defaults = ( + buttonshide => { value => 'yes', + type => 'string_yesno',}, + type => { value => 'randomizetry', + type => 'string_questiontype',}, + maxtries => { value => 1, + type => 'int_pos',}, + problemstatus => { value => 'no', + type => 'string_problemstatus',}, + ); + foreach my $key (keys(%defaults)) { + $storecontent{$prefix.$key} = $defaults{$key}{'value'}; + $storecontent{$prefix.$key.'.type'} = $defaults{$key}{'type'}; + } + &Apache::lonnet::cput + ('resourcedata',\%storecontent,$$crsudom,$$crsunum); + } + return (1,$outcome); } +sub make_unique_code { + my ($cdom,$cnum) = @_; + # get lock on uniquecodes db + my $lockhash = { + $cnum."\0".'uniquecodes' => $env{'user.name'}. + ':'.$env{'user.domain'}, + }; + my $tries = 0; + my $gotlock = &Apache::lonnet::newput_dom('uniquecodes',$lockhash,$cdom); + my ($code,$error); + + while (($gotlock ne 'ok') && ($tries<3)) { + $tries ++; + sleep 1; + $gotlock = &Apache::lonnet::newput_dom('uniquecodes',$lockhash,$cdom); + } + if ($gotlock eq 'ok') { + my %currcodes = &Apache::lonnet::dump_dom('uniquecodes',$cdom); + my $gotcode; + my $attempts = 0; + while ((!$gotcode) && ($attempts < 100)) { + $code = &generate_code(); + if (!exists($currcodes{$code})) { + $gotcode = 1; + unless (&Apache::lonnet::newput_dom('uniquecodes',{ $code => $cnum },$cdom) eq 'ok') { + $error = 'nostore'; + } + } + $attempts ++; + } + my @del_lock = ($cnum."\0".'uniquecodes'); + my $dellockoutcome = &Apache::lonnet::del_dom('uniquecodes',\@del_lock,$cdom); + } else { + $error = 'nolock'; + } + return ($code,$error); +} + +sub generate_code { + my $code; + my @letts = qw(B C D G H J K M N P Q R S T V W X Z); + for (my $i=0; $i<6; $i++) { + my $lettnum = int (rand 2); + my $item = ''; + if ($lettnum) { + $item = $letts[int( rand(18) )]; + } else { + $item = 1+int( rand(8) ); + } + $code .= $item; + } + return $code; +} + ############################################################ ############################################################ -#SD -# only Community and Course, or anything else? +# Community, Course and Placement Test sub course_type { my ($cid) = @_; if (!defined($cid)) { @@ -14143,16 +15699,19 @@ sub group_term { my %names = ( 'Course' => 'group', 'Community' => 'group', + 'Placement' => 'group', ); return $names{$crstype}; } sub course_types { - my @types = ('official','unofficial','community'); + my @types = ('official','unofficial','community','textbook','placement'); my %typename = ( official => 'Official course', unofficial => 'Unofficial course', community => 'Community', + textbook => 'Textbook course', + placement => 'Placement test', ); return (\@types,\%typename); } @@ -14213,7 +15772,7 @@ sub escape_url { my ($url) = @_; my @urlslices = split(/\//, $url,-1); my $lastitem = &escape(pop(@urlslices)); - return join('/',@urlslices).'/'.$lastitem; + return &HTML::Entities::encode(join('/',@urlslices),"'").'/'.$lastitem; } sub compare_arrays { @@ -14271,6 +15830,17 @@ sub init_user_environment { } } closedir(DIR); +# If there is a undeleted lockfile for the user's paste buffer remove it. + my $namespace = 'nohist_courseeditor'; + my $lockingkey = 'paste'."\0".'locked_num'; + my %lockhash = &Apache::lonnet::get($namespace,[$lockingkey], + $domain,$username); + if (exists($lockhash{$lockingkey})) { + my $delresult = &Apache::lonnet::del($namespace,[$lockingkey],$domain,$username); + unless ($delresult eq 'ok') { + &Apache::lonnet::logthis("Failed to delete paste buffer locking key in $namespace for ".$username.":".$domain." Result was: $delresult"); + } + } } # Give them a new cookie my $id = ($args->{'robot'} ? 'robot'.$args->{'robot'} @@ -14284,8 +15854,8 @@ sub init_user_environment { } # ------------------------------------ Check browser type and MathML capability - my ($httpbrowser,$clientbrowser,$clientversion,$clientmathml, - $clientunicode,$clientos,$clientmobile) = &decode_user_agent($r); + my ($httpbrowser,$clientbrowser,$clientversion,$clientmathml,$clientunicode, + $clientos,$clientmobile,$clientinfo,$clientosversion) = &decode_user_agent($r); # ------------------------------------------------------------- Get environment @@ -14317,6 +15887,8 @@ sub init_user_environment { "browser.unicode" => $clientunicode, "browser.os" => $clientos, "browser.mobile" => $clientmobile, + "browser.info" => $clientinfo, + "browser.osversion" => $clientosversion, "server.domain" => $Apache::lonnet::perlvar{'lonDefDomain'}, "request.course.fn" => '', "request.course.uri" => '', @@ -14336,6 +15908,12 @@ sub init_user_environment { $env{'browser.interface'}=$form->{'interface'}; } + if ($form->{'iptoken'}) { + my $lonhost = $r->dir_config('lonHostID'); + $initial_env{"user.noloadbalance"} = $lonhost; + $env{'user.noloadbalance'} = $lonhost; + } + my %is_adv = ( is_adv => $env{'user.adv'} ); my %domdef; unless ($domain eq 'public') { @@ -14348,7 +15926,7 @@ sub init_user_environment { undef,\%userenv,\%domdef,\%is_adv); } - foreach my $crstype ('official','unofficial','community') { + foreach my $crstype ('official','unofficial','community','textbook','placement') { $userenv{'canrequest.'.$crstype} = &Apache::lonnet::usertools_access($username,$domain,$crstype, 'reload','requestcourses', @@ -14453,36 +16031,757 @@ sub clean_symb { return ($symb,$enc); } -sub build_release_hashes { - my ($checkparms,$checkresponsetypes,$checkcrstypes,$anonsurvey,$randomizetry) = @_; - return unless((ref($checkparms) eq 'HASH') && (ref($checkresponsetypes) eq 'HASH') && - (ref($checkcrstypes) eq 'HASH') && (ref($anonsurvey) eq 'HASH') && - (ref($randomizetry) eq 'HASH')); - foreach my $key (keys(%Apache::lonnet::needsrelease)) { - my ($item,$name,$value) = split(/:/,$key); - if ($item eq 'parameter') { - if (ref($checkparms->{$name}) eq 'ARRAY') { - unless(grep(/^\Q$name\E$/,@{$checkparms->{$name}})) { - push(@{$checkparms->{$name}},$value); - } +############################################################ +############################################################ + +=pod + +=head1 Routines for building display used to search for courses + + +=over 4 + +=item * &build_filters() + +Create markup for a table used to set filters to use when selecting +courses in a domain. Used by lonpickcourse.pm, lonmodifycourse.pm +and quotacheck.pl + + +Inputs: + +filterlist - anonymous array of fields to include as potential filters + +crstype - course type + +roleelement - fifth arg in selectcourse_link() populates fifth arg in javascript: opencrsbrowser() function, used + to pop-open a course selector (will contain "extra element"). + +multelement - if multiple course selections will be allowed, this will be a hidden form element: name: multiple; value: 1 + +filter - anonymous hash of criteria and their values + +action - form action + +numfiltersref - ref to scalar (count of number of elements in institutional codes -- e.g., 4 for year, semester, department, and number) + +caller - caller context (e.g., set to 'modifycourse' when routine is called from lonmodifycourse.pm) + +cloneruname - username of owner of new course who wants to clone + +clonerudom - domain of owner of new course who wants to clone + +typeelem - text to use for left column in row containing course type (i.e., Course, Community or Course/Community) + +codetitlesref - reference to array of titles of components in institutional codes (official courses) + +codedom - domain + +formname - value of form element named "form". + +fixeddom - domain, if fixed. + +prevphase - value to assign to form element named "phase" when going back to the previous screen + +cnameelement - name of form element in form on opener page which will receive title of selected course + +cnumelement - name of form element in form on opener page which will receive courseID of selected course + +cdomelement - name of form element in form on opener page which will receive domain of selected course + +setroles - includes access constraint identifier when setting a roles-based condition for acces to a portfolio file + +clonetext - hidden form elements containing list of courses cloneable by intended course owner when DC creates a course + +clonewarning - warning message about missing information for intended course owner when DC creates a course + + +Returns: $output - HTML for display of search criteria, and hidden form elements. + + +Side Effects: None + +=cut + +# ---------------------------------------------- search for courses based on last activity etc. + +sub build_filters { + my ($filterlist,$crstype,$roleelement,$multelement,$filter,$action, + $numtitlesref,$caller,$cloneruname,$clonerudom,$typeelement, + $codetitlesref,$codedom,$formname,$fixeddom,$prevphase, + $cnameelement,$cnumelement,$cdomelement,$setroles, + $clonetext,$clonewarning) = @_; + my ($list,$jscript); + my $onchange = 'javascript:updateFilters(this)'; + my ($domainselectform,$sincefilterform,$createdfilterform, + $ownerdomselectform,$persondomselectform,$instcodeform, + $typeselectform,$instcodetitle); + if ($formname eq '') { + $formname = $caller; + } + foreach my $item (@{$filterlist}) { + unless (($item eq 'descriptfilter') || ($item eq 'instcodefilter') || + ($item eq 'sincefilter') || ($item eq 'createdfilter')) { + if ($item eq 'domainfilter') { + $filter->{$item} = &LONCAPA::clean_domain($filter->{$item}); + } elsif ($item eq 'coursefilter') { + $filter->{$item} = &LONCAPA::clean_courseid($filter->{$item}); + } elsif ($item eq 'ownerfilter') { + $filter->{$item} = &LONCAPA::clean_username($filter->{$item}); + } elsif ($item eq 'ownerdomfilter') { + $filter->{'ownerdomfilter'} = + &LONCAPA::clean_domain($filter->{$item}); + $ownerdomselectform = &select_dom_form($filter->{'ownerdomfilter'}, + 'ownerdomfilter',1); + } elsif ($item eq 'personfilter') { + $filter->{$item} = &LONCAPA::clean_username($filter->{$item}); + } elsif ($item eq 'persondomfilter') { + $persondomselectform = &select_dom_form($filter->{'persondomfilter'}, + 'persondomfilter',1); } else { - push(@{$checkparms->{$name}},$value); + $filter->{$item} =~ s/\W//g; } - } elsif ($item eq 'resourcetag') { - if ($name eq 'responsetype') { - $checkresponsetypes->{$value} = $Apache::lonnet::needsrelease{$key} + if (!$filter->{$item}) { + $filter->{$item} = ''; } - } elsif ($item eq 'course') { - if ($name eq 'crstype') { - $checkcrstypes->{$value} = $Apache::lonnet::needsrelease{$key}; + } + if ($item eq 'domainfilter') { + my $allow_blank = 1; + if ($formname eq 'portform') { + $allow_blank=0; + } elsif ($formname eq 'studentform') { + $allow_blank=0; + } + if ($fixeddom) { + $domainselectform = ''. + &Apache::lonnet::domain($codedom,'description'); + } else { + $domainselectform = &select_dom_form($filter->{$item}, + 'domainfilter', + $allow_blank,'',$onchange); + } + } else { + $list->{$item} = &HTML::Entities::encode($filter->{$item},'<>&"'); + } + } + + # last course activity filter and selection + $sincefilterform = &timebased_select_form('sincefilter',$filter); + + # course created filter and selection + if (exists($filter->{'createdfilter'})) { + $createdfilterform = &timebased_select_form('createdfilter',$filter); + } + + my $prefix = $crstype; + if ($crstype eq 'Placement') { + $prefix = 'Placement Test' + } + my %lt = &Apache::lonlocal::texthash( + 'cac' => "$prefix Activity", + 'ccr' => "$prefix Created", + 'cde' => "$prefix Title", + 'cdo' => "$prefix Domain", + 'ins' => 'Institutional Code', + 'inc' => 'Institutional Categorization', + 'cow' => "$prefix Owner/Co-owner", + 'cop' => "$prefix Personnel Includes", + 'cog' => 'Type', + ); + + if (($formname eq 'ccrs') || ($formname eq 'requestcrs')) { + my $typeval = 'Course'; + if ($crstype eq 'Community') { + $typeval = 'Community'; + } elsif ($crstype eq 'Placement') { + $typeval = 'Placement'; + } + $typeselectform = ''; + } else { + $typeselectform = '"; + } + + my ($cloneableonlyform,$cloneabletitle); + if (exists($filter->{'cloneableonly'})) { + my $cloneableon = ''; + my $cloneableoff = ' checked="checked"'; + if ($filter->{'cloneableonly'}) { + $cloneableon = $cloneableoff; + $cloneableoff = ''; + } + $cloneableonlyform = ''.(' 'x3).''; + if ($formname eq 'ccrs') { + $cloneabletitle = &mt('Cloneable for [_1]',$cloneruname.':'.$clonerudom); + } else { + $cloneabletitle = &mt('Cloneable by you'); + } + } + my $officialjs; + if ($crstype eq 'Course') { + if (exists($filter->{'instcodefilter'})) { +# if (($fixeddom) || ($formname eq 'requestcrs') || +# ($formname eq 'modifycourse') || ($formname eq 'filterpicker')) { + if ($codedom) { + $officialjs = 1; + ($instcodeform,$jscript,$$numtitlesref) = + &Apache::courseclassifier::instcode_selectors($codedom,'filterpicker', + $officialjs,$codetitlesref); + if ($jscript) { + $jscript = ''."\n"; + } + } + if ($instcodeform eq '') { + $instcodeform = + ''; + $instcodetitle = $lt{'ins'}; + } else { + $instcodetitle = $lt{'inc'}; + } + if ($fixeddom) { + $instcodetitle .= '
('.$codedom.')'; + } + } + } + my $output = qq| +
+ +|; + if ($formname eq 'modifycourse') { + $output .= ''."\n". + ''."\n"; + } elsif ($formname eq 'quotacheck') { + $output .= qq| + + +|; + } else { + my $name_input; + if ($cnameelement ne '') { + $name_input = ''; + } + $output .= qq| + + +$name_input +$roleelement +$multelement +$typeelement +|; + if ($formname eq 'portform') { + $output .= ''."\n"; + } + } + if ($fixeddom) { + $output .= ''."\n"; + } + $output .= "
\n".&Apache::lonhtmlcommon::start_pick_box(); + if ($sincefilterform) { + $output .= &Apache::lonhtmlcommon::row_title($lt{'cac'}) + .$sincefilterform + .&Apache::lonhtmlcommon::row_closure(); + } + if ($createdfilterform) { + $output .= &Apache::lonhtmlcommon::row_title($lt{'ccr'}) + .$createdfilterform + .&Apache::lonhtmlcommon::row_closure(); + } + if ($domainselectform) { + $output .= &Apache::lonhtmlcommon::row_title($lt{'cdo'}) + .$domainselectform + .&Apache::lonhtmlcommon::row_closure(); + } + if ($typeselectform) { + if (($formname eq 'ccrs') || ($formname eq 'requestcrs')) { + $output .= $typeselectform; + } else { + $output .= &Apache::lonhtmlcommon::row_title($lt{'cog'}) + .$typeselectform + .&Apache::lonhtmlcommon::row_closure(); + } + } + if ($instcodeform) { + $output .= &Apache::lonhtmlcommon::row_title($instcodetitle) + .$instcodeform + .&Apache::lonhtmlcommon::row_closure(); + } + if (exists($filter->{'ownerfilter'})) { + $output .= &Apache::lonhtmlcommon::row_title($lt{'cow'}). + '
'.&mt('Username').'
'. + '
'.&mt('Domain').'
'. + $ownerdomselectform.'
'. + &Apache::lonhtmlcommon::row_closure(); + } + if (exists($filter->{'personfilter'})) { + $output .= &Apache::lonhtmlcommon::row_title($lt{'cop'}). + '
'.&mt('Username').'
'. + '
'.&mt('Domain').'
'. + $persondomselectform.'
'. + &Apache::lonhtmlcommon::row_closure(); + } + if (exists($filter->{'coursefilter'})) { + $output .= &Apache::lonhtmlcommon::row_title(&mt('LON-CAPA course ID')) + .'' + .&Apache::lonhtmlcommon::row_closure(); + } + if ($cloneableonlyform) { + $output .= &Apache::lonhtmlcommon::row_title($cloneabletitle). + $cloneableonlyform.&Apache::lonhtmlcommon::row_closure(); + } + if (exists($filter->{'descriptfilter'})) { + $output .= &Apache::lonhtmlcommon::row_title($lt{'cde'}) + .'' + .&Apache::lonhtmlcommon::row_closure(1); + } + $output .= &Apache::lonhtmlcommon::end_pick_box().'

'.$clonetext."\n". + ''."\n". + '

'."\n".'
'."\n".'
'."\n"; + return $jscript.$clonewarning.$output; +} + +=pod + +=item * &timebased_select_form() + +Create markup for a dropdown list used to select a time-based +filter e.g., Course Activity, Course Created, when searching for courses +or communities + +Inputs: + +item - name of form element (sincefilter or createdfilter) + +filter - anonymous hash of criteria and their values + +Returns: HTML for a select box contained a blank, then six time selections, + with value set in incoming form variables currently selected. + +Side Effects: None + +=cut + +sub timebased_select_form { + my ($item,$filter) = @_; + if (ref($filter) eq 'HASH') { + $filter->{$item} =~ s/[^\d-]//g; + if (!$filter->{$item}) { $filter->{$item}=-1; } + return &select_form( + $filter->{$item}, + $item, + { '-1' => '', + '86400' => &mt('today'), + '604800' => &mt('last week'), + '2592000' => &mt('last month'), + '7776000' => &mt('last three months'), + '15552000' => &mt('last six months'), + '31104000' => &mt('last year'), + 'select_form_order' => + ['-1','86400','604800','2592000','7776000', + '15552000','31104000']}); + } +} + +=pod + +=item * &js_changer() + +Create script tag containing Javascript used to submit course search form +when course type or domain is changed, and also to hide 'Searching ...' on +page load completion for page showing search result. + +Inputs: None + +Returns: markup containing updateFilters() and hideSearching() javascript functions. + +Side Effects: None + +=cut + +sub js_changer { + return < +// {major},$anonsurvey->{minor}) = split(/\./,$Apache::lonnet::needsrelease{'parameter:type:anonsurvey'}); - ($randomizetry->{major},$randomizetry->{minor}) = split(/\./,$Apache::lonnet::needsrelease{'parameter:type:randomizetry'}); return; } +// ]]> + + +ENDJS +} + +=pod + +=item * &search_courses() + +Process selected filters form course search form and pass to lonnet::courseiddump +to retrieve a hash for which keys are courseIDs which match the selected filters. + +Inputs: + +dom - domain being searched + +type - course type ('Course' or 'Community' or '.' if any). + +filter - anonymous hash of criteria and their values + +numtitles - for institutional codes - number of categories + +cloneruname - optional username of new course owner + +clonerudom - optional domain of new course owner + +domcloner - optional "domcloner" flag; has value=1 if user has ccc priv in domain being filtered by, + (used when DC is using course creation form) + +codetitles - reference to array of titles of components in institutional codes (official courses). + +cc_clone - escaped comma separated list of courses for which course cloner has active CC role + (and so can clone automatically) + +reqcrsdom - domain of new course, where search_courses is used to identify potential courses to clone + +reqinstcode - institutional code of new course, where search_courses is used to identify potential + courses to clone + +Returns: %courses - hash of courses satisfying search criteria, keys = course IDs, values are corresponding colon-separated escaped description, institutional code, owner and type. + + +Side Effects: None + +=cut + + +sub search_courses { + my ($dom,$type,$filter,$numtitles,$cloneruname,$clonerudom,$domcloner,$codetitles, + $cc_clone,$reqcrsdom,$reqinstcode) = @_; + my (%courses,%showcourses,$cloner); + if (($filter->{'ownerfilter'} ne '') || + ($filter->{'ownerdomfilter'} ne '')) { + $filter->{'combownerfilter'} = $filter->{'ownerfilter'}.':'. + $filter->{'ownerdomfilter'}; + } + foreach my $item ('descriptfilter','coursefilter','combownerfilter') { + if (!$filter->{$item}) { + $filter->{$item}='.'; + } + } + my $now = time; + my $timefilter = + ($filter->{'sincefilter'}==-1?1:$now-$filter->{'sincefilter'}); + my ($createdbefore,$createdafter); + if (($filter->{'createdfilter'} ne '') && ($filter->{'createdfilter'} !=-1)) { + $createdbefore = $now; + $createdafter = $now-$filter->{'createdfilter'}; + } + my ($instcodefilter,$regexpok); + if ($numtitles) { + if ($env{'form.official'} eq 'on') { + $instcodefilter = + &Apache::courseclassifier::instcode_search_str($dom,$numtitles,$codetitles); + $regexpok = 1; + } elsif ($env{'form.official'} eq 'off') { + $instcodefilter = &Apache::courseclassifier::instcode_search_str($dom,$numtitles,$codetitles); + unless ($instcodefilter eq '') { + $regexpok = -1; + } + } + } else { + $instcodefilter = $filter->{'instcodefilter'}; + } + if ($instcodefilter eq '') { $instcodefilter = '.'; } + if ($type eq '') { $type = '.'; } + + if (($clonerudom ne '') && ($cloneruname ne '')) { + $cloner = $cloneruname.':'.$clonerudom; + } + %courses = &Apache::lonnet::courseiddump($dom, + $filter->{'descriptfilter'}, + $timefilter, + $instcodefilter, + $filter->{'combownerfilter'}, + $filter->{'coursefilter'}, + undef,undef,$type,$regexpok,undef,undef, + undef,undef,$cloner,$cc_clone, + $filter->{'cloneableonly'}, + $createdbefore,$createdafter,undef, + $domcloner,undef,$reqcrsdom,$reqinstcode); + if (($filter->{'personfilter'} ne '') && ($filter->{'persondomfilter'} ne '')) { + my $ccrole; + if ($type eq 'Community') { + $ccrole = 'co'; + } else { + $ccrole = 'cc'; + } + my %rolehash = &Apache::lonnet::get_my_roles($filter->{'personfilter'}, + $filter->{'persondomfilter'}, + 'userroles',undef, + [$ccrole,'in','ad','ep','ta','cr'], + $dom); + foreach my $role (keys(%rolehash)) { + my ($cnum,$cdom,$courserole) = split(':',$role); + my $cid = $cdom.'_'.$cnum; + if (exists($courses{$cid})) { + 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); + } + } else { + $courses{$cid}{roles} = [$courserole]; + } + $showcourses{$cid} = $courses{$cid}; + } + } + } + %courses = %showcourses; + } + return %courses; +} + +=pod + +=back + +=head1 Routines for version requirements for current course. + +=over 4 + +=item * &check_release_required() + +Compares required LON-CAPA version with version on server, and +if required version is newer looks for a server with the required version. + +Looks first at servers in user's owen domain; if none suitable, looks at +servers in course's domain are permitted to host sessions for user's domain. + +Inputs: + +$loncaparev - Version on current server (format: Major.Minor.Subrelease-datestamp) + +$courseid - Course ID of current course + +$rolecode - User's current role in course (for switchserver query string). + +$required - LON-CAPA version needed by course (format: Major.Minor). + + +Returns: + +$switchserver - query string tp append to /adm/switchserver call (if + current server's LON-CAPA version is too old. + +$warning - Message is displayed if no suitable server could be found. + +=cut + +sub check_release_required { + my ($loncaparev,$courseid,$rolecode,$required) = @_; + my ($switchserver,$warning); + if ($required ne '') { + my ($reqdmajor,$reqdminor) = ($required =~ /^(\d+)\.(\d+)$/); + my ($major,$minor) = ($loncaparev =~ /^\'?(\d+)\.(\d+)\.[\w.\-]+\'?$/); + if ($reqdmajor ne '' && $reqdminor ne '') { + my $otherserver; + if (($major eq '' && $minor eq '') || + (($reqdmajor > $major) || (($reqdmajor == $major) && ($reqdminor > $minor)))) { + my ($userdomserver) = &Apache::lonnet::choose_server($env{'user.domain'},undef,$required,1); + my $switchlcrev = + &Apache::lonnet::get_server_loncaparev($env{'user.domain'}, + $userdomserver); + my ($swmajor,$swminor) = ($switchlcrev =~ /^\'?(\d+)\.(\d+)\.[\w.\-]+\'?$/); + if (($swmajor eq '' && $swminor eq '') || ($reqdmajor > $swmajor) || + (($reqdmajor == $swmajor) && ($reqdminor > $swminor))) { + my $cdom = $env{'course.'.$courseid.'.domain'}; + if ($cdom ne $env{'user.domain'}) { + my ($coursedomserver,$coursehostname) = &Apache::lonnet::choose_server($cdom,undef,$required,1); + my $serverhomeID = &Apache::lonnet::get_server_homeID($coursehostname); + my $serverhomedom = &Apache::lonnet::host_domain($serverhomeID); + my %defdomdefaults = &Apache::lonnet::get_domain_defaults($serverhomedom); + my %udomdefaults = &Apache::lonnet::get_domain_defaults($env{'user.domain'}); + my $remoterev = &Apache::lonnet::get_server_loncaparev($serverhomedom,$coursedomserver); + my $canhost = + &Apache::lonnet::can_host_session($env{'user.domain'}, + $coursedomserver, + $remoterev, + $udomdefaults{'remotesessions'}, + $defdomdefaults{'hostedsessions'}); + + if ($canhost) { + $otherserver = $coursedomserver; + } else { + $warning = &mt('Requires LON-CAPA version [_1].',$env{'course.'.$courseid.'.internal.releaserequired'}).'
'. &mt("No suitable server could be found amongst servers in either your own domain or in the course's domain."); + } + } else { + $warning = &mt('Requires LON-CAPA version [_1].',$env{'course.'.$courseid.'.internal.releaserequired'}).'
'.&mt("No suitable server could be found amongst servers in your own domain (which is also the course's domain)."); + } + } else { + $otherserver = $userdomserver; + } + } + if ($otherserver ne '') { + $switchserver = 'otherserver='.$otherserver.'&role='.$rolecode; + } + } + } + return ($switchserver,$warning); +} + +=pod + +=item * &check_release_result() + +Inputs: + +$switchwarning - Warning message if no suitable server found to host session. + +$switchserver - query string to append to /adm/switchserver containing lonHostID + and current role. + +Returns: HTML to display with information about requirement to switch server. + Either displaying warning with link to Roles/Courses screen or + display link to switchserver. + +=cut + +sub check_release_result { + my ($switchwarning,$switchserver) = @_; + my $output = &start_page('Selected course unavailable on this server'). + '

'; + if ($switchwarning) { + $output .= $switchwarning.'
'; + if (&show_course()) { + $output .= &mt('Display courses'); + } else { + $output .= &mt('Display roles'); + } + $output .= ''; + } elsif ($switchserver) { + $output .= &mt('This course requires a newer version of LON-CAPA than is installed on this server.'). + '
'. + ''. + &mt('Switch Server'). + ''; + } + $output .= '

'.&end_page(); + return $output; +} + +=pod + +=item * &needs_coursereinit() + +Determine if course contents stored for user's session needs to be +refreshed, because content has changed since "Big Hash" last tied. + +Check for change is made if time last checked is more than 10 minutes ago +(by default). + +Inputs: + +$loncaparev - Version on current server (format: Major.Minor.Subrelease-datestamp) + +$interval (optional) - Time which may elapse (in s) between last check for content + change in current course. (default: 600 s). + +Returns: an array; first element is: + +=over 4 + +'switch' - if content updates mean user's session + needs to be switched to a server running a newer LON-CAPA version + +'update' - if course session needs to be refreshed (i.e., Big Hash needs to be reloaded) + on current server hosting user's session + +'' - if no action required. + +=back + +If first item element is 'switch': + +second item is $switchwarning - Warning message if no suitable server found to host session. + +third item is $switchserver - query string to append to /adm/switchserver containing lonHostID + and current role. + +otherwise: no other elements returned. + +=back + +=cut + +sub needs_coursereinit { + my ($loncaparev,$interval) = @_; + return() unless ($env{'request.course.id'} && $env{'request.course.tied'}); + my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; + my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; + my $now = time; + if ($interval eq '') { + $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); + } + } + } + return ('update'); + } + } + return (); +} + sub update_content_constraints { my ($cdom,$cnum,$chome,$cid) = @_; my %curr_reqd_hash = &Apache::lonnet::userenvironment($cdom,$cnum,'internal.releaserequired'); @@ -14569,6 +16868,30 @@ sub parse_supplemental_title { return $title; } +sub recurse_supplemental { + my ($cnum,$cdom,$suppmap,$numfiles,$errors) = @_; + if ($suppmap) { + 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); + if (($src ne '') && ($status eq 'res')) { + if ($src =~ m{^\Q/uploaded/$cdom/$cnum/\E(supplemental_\d+\.sequence)$}) { + ($numfiles,$errors) = &recurse_supplemental($cnum,$cdom,$1,$numfiles,$errors); + } else { + $numfiles ++; + } + } + } + } + } + } + return ($numfiles,$errors); +} + sub symb_to_docspath { my ($symb) = @_; return unless ($symb); @@ -14598,7 +16921,7 @@ sub symb_to_docspath { my $thistitle = $res->title(); $path .= '&'. &Apache::lonhtmlcommon::entity_encode($thisurl).'&'. - &Apache::lonhtmlcommon::entity_encode($thistitle). + &escape($thistitle). ':'.$res->randompick(). ':'.$res->randomout(). ':'.$res->encrypted(). @@ -14614,7 +16937,7 @@ sub symb_to_docspath { } $path .= (($path ne '')? '&' : ''). &Apache::lonhtmlcommon::entity_encode($mapurl).'&'. - &Apache::lonhtmlcommon::entity_encode($maptitle). + &escape($maptitle). ':'.$mapresobj->randompick(). ':'.$mapresobj->randomout(). ':'.$mapresobj->encrypted(). @@ -14627,11 +16950,11 @@ sub symb_to_docspath { $maptitle = 'Main Content'; } $path = &Apache::lonhtmlcommon::entity_encode($mapurl).'&'. - &Apache::lonhtmlcommon::entity_encode($maptitle).':::::'.$ispage; + &escape($maptitle).':::::'.$ispage; } unless ($mapurl eq 'default') { $path = 'default&'. - &Apache::lonhtmlcommon::entity_encode('Main Content'). + &escape('Main Content'). ':::::&'.$path; } return $path; @@ -14640,29 +16963,30 @@ sub symb_to_docspath { sub captcha_display { my ($context,$lonhost) = @_; my ($output,$error); - my ($captcha,$pubkey,$privkey) = &get_captcha_config($context,$lonhost); + my ($captcha,$pubkey,$privkey,$version) = + &get_captcha_config($context,$lonhost); if ($captcha eq 'original') { $output = &create_captcha(); unless ($output) { - $error = 'captcha'; + $error = 'captcha'; } } elsif ($captcha eq 'recaptcha') { - $output = &create_recaptcha($pubkey); + $output = &create_recaptcha($pubkey,$version); unless ($output) { - $error = 'recaptcha'; + $error = 'recaptcha'; } } - return ($output,$error); + return ($output,$error,$captcha,$version); } sub captcha_response { my ($context,$lonhost) = @_; my ($captcha_chk,$captcha_error); - my ($captcha,$pubkey,$privkey) = &get_captcha_config($context,$lonhost); + my ($captcha,$pubkey,$privkey,$version) = &get_captcha_config($context,$lonhost); if ($captcha eq 'original') { ($captcha_chk,$captcha_error) = &check_captcha(); } elsif ($captcha eq 'recaptcha') { - $captcha_chk = &check_recaptcha($privkey); + $captcha_chk = &check_recaptcha($privkey,$version); } else { $captcha_chk = 1; } @@ -14671,7 +16995,7 @@ sub captcha_response { sub get_captcha_config { my ($context,$lonhost) = @_; - my ($captcha,$pubkey,$privkey,$hashtocheck); + my ($captcha,$pubkey,$privkey,$version,$hashtocheck); my $hostname = &Apache::lonnet::hostname($lonhost); my $serverhomeID = &Apache::lonnet::get_server_homeID($hostname); my $serverhomedom = &Apache::lonnet::host_domain($serverhomeID); @@ -14687,6 +17011,10 @@ sub get_captcha_config { } if ($privkey && $pubkey) { $captcha = 'recaptcha'; + $version = $hashtocheck->{'recaptchaversion'}; + if ($version ne '2') { + $version = 1; + } } else { $captcha = 'original'; } @@ -14704,6 +17032,10 @@ sub get_captcha_config { $privkey = $domconfhash{$serverhomedom.'.login.recaptchakeys_private'}; if ($privkey && $pubkey) { $captcha = 'recaptcha'; + $version = $domconfhash{$serverhomedom.'.login.recaptchaversion'}; + if ($version ne '2') { + $version = 1; + } } else { $captcha = 'original'; } @@ -14711,7 +17043,7 @@ sub get_captcha_config { $captcha = 'original'; } } - return ($captcha,$pubkey,$privkey); + return ($captcha,$pubkey,$privkey,$version); } sub create_captcha { @@ -14728,8 +17060,9 @@ sub create_captcha { if (-e $Apache::lonnet::perlvar{'lonCaptchaDir'}.'/'.$md5sum.'.png') { $output = ''."\n". &mt('Type in the letters/numbers shown below').' '. - '
'. - ''; + ''. + '
'. + 'captcha'; last; } } @@ -14769,37 +17102,164 @@ sub check_captcha { } sub create_recaptcha { - my ($pubkey) = @_; - my $captcha = Captcha::reCAPTCHA->new; - return $captcha->get_options_setter({theme => 'white'})."\n". - $captcha->get_html($pubkey). - &mt('If either word is hard to read, [_1] will replace them.', - 'reCAPTCHA refresh'). - '

'; + my ($pubkey,$version) = @_; + if ($version >= 2) { + return '
'; + } else { + my $use_ssl; + if ($ENV{'SERVER_PORT'} == 443) { + $use_ssl = 1; + } + my $captcha = Captcha::reCAPTCHA->new; + return $captcha->get_options_setter({theme => 'white'})."\n". + $captcha->get_html($pubkey,undef,$use_ssl). + &mt('If the text is hard to read, [_1] will replace them.', + 'reCAPTCHA refresh'). + '

'; + } } sub check_recaptcha { - my ($privkey) = @_; + my ($privkey,$version) = @_; my $captcha_chk; - my $captcha = Captcha::reCAPTCHA->new; - my $captcha_result = - $captcha->check_answer( - $privkey, - $ENV{'REMOTE_ADDR'}, - $env{'form.recaptcha_challenge_field'}, - $env{'form.recaptcha_response_field'}, - ); - if ($captcha_result->{is_valid}) { - $captcha_chk = 1; + 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'}, + ); + my $response = $ua->post('https://www.google.com/recaptcha/api/siteverify',\%info); + if ($response->is_success) { + my $data = JSON::DWIW->from_json($response->decoded_content); + if (ref($data) eq 'HASH') { + if ($data->{'success'}) { + $captcha_chk = 1; + } + } + } + } else { + my $captcha = Captcha::reCAPTCHA->new; + my $captcha_result = + $captcha->check_answer( + $privkey, + $ENV{'REMOTE_ADDR'}, + $env{'form.recaptcha_challenge_field'}, + $env{'form.recaptcha_response_field'}, + ); + if ($captcha_result->{is_valid}) { + $captcha_chk = 1; + } } return $captcha_chk; } -=pod +sub emailusername_info { + my @fields = ('firstname','lastname','institution','web','location','officialemail','id'); + my %titles = &Apache::lonlocal::texthash ( + lastname => 'Last Name', + firstname => 'First Name', + institution => 'School/college/university', + location => "School's city, state/province, country", + web => "School's web address", + officialemail => 'E-mail address at institution (if different)', + id => 'Student/Employee ID', + ); + return (\@fields,\%titles); +} -=back +sub cleanup_html { + my ($incoming) = @_; + my $outgoing; + if ($incoming ne '') { + $outgoing = $incoming; + $outgoing =~ s/;/;/g; + $outgoing =~ s/\#/#/g; + $outgoing =~ s/\&/&/g; + $outgoing =~ s//>/g; + $outgoing =~ s/\(/(/g; + $outgoing =~ s/\)/)/g; + $outgoing =~ s/"/"/g; + $outgoing =~ s/'/'/g; + $outgoing =~ s/\$/$/g; + $outgoing =~ s{/}{/}g; + $outgoing =~ s/=/=/g; + $outgoing =~ s/\\/\/g + } + return $outgoing; +} + +# Checks for critical messages and returns a redirect url if one exists. +# $interval indicates how often to check for messages. +sub critical_redirect { + my ($interval) = @_; + if ((time-$env{'user.criticalcheck.time'})>$interval) { + 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\:/)) { + $redirecturl='/adm/email?critical=display'; + my $url=&Apache::lonnet::absolute_url().$redirecturl; + return (1, $url); + } + } + } + return (); +} -=cut +# Use: +# my $answer=reply("encrypt:passwd:$udom:$uname:$upass",$tryserver); +# +################################################## +# password associated functions # +################################################## +sub des_keys { + # Make a new key for DES encryption. + # Each key has two parts which are returned separately. + # Please note: Each key must be passed through the &hex function + # before it is output to the web browser. The hex versions cannot + # be used to decrypt. + my @hexstr=('0','1','2','3','4','5','6','7', + '8','9','a','b','c','d','e','f'); + my $lkey=''; + for (0..7) { + $lkey.=$hexstr[rand(15)]; + } + my $ukey=''; + for (0..7) { + $ukey.=$hexstr[rand(15)]; + } + return ($lkey,$ukey); +} + +sub des_decrypt { + my ($key,$cyphertext) = @_; + my $keybin=pack("H16",$key); + my $cypher; + if ($Crypt::DES::VERSION>=2.03) { + $cypher=new Crypt::DES $keybin; + } else { + $cypher=new DES $keybin; + } + my $plaintext=''; + my $cypherlength = length($cyphertext); + my $numchunks = int($cypherlength/32); + for (my $j=0; $j<$numchunks; $j++) { + my $start = $j*32; + my $cypherblock = substr($cyphertext,$start,32); + my $chunk = + $cypher->decrypt(unpack("a8",pack("H16",substr($cypherblock,0,16)))); + $chunk .= + $cypher->decrypt(unpack("a8",pack("H16",substr($cypherblock,16,16)))); + $chunk=substr($chunk,1,ord(substr($chunk,0,1)) ); + $plaintext .= $chunk; + } + return $plaintext; +} 1; __END__;