--- loncom/interface/londocs.pm 2003/09/08 18:56:09 1.74 +++ loncom/interface/londocs.pm 2012/07/13 13:36:24 1.491 @@ -1,7 +1,7 @@ # The LearningOnline Network # Documents # -# $Id: londocs.pm,v 1.74 2003/09/08 18:56:09 www Exp $ +# $Id: londocs.pm,v 1.491 2012/07/13 13:36:24 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -30,14 +30,23 @@ package Apache::londocs; use strict; use Apache::Constants qw(:common :http); +use Apache::imsexport; use Apache::lonnet; use Apache::loncommon; -use Apache::lonratedt; -use Apache::lonratsrv; +use Apache::lonhtmlcommon; +use LONCAPA::map(); +use Apache::lonratedt(); use Apache::lonxml; -use Apache::loncreatecourse; +use Apache::lonclonecourse; +use Apache::lonnavmaps; +use Apache::lonnavdisplay(); +use Apache::lonuserstate(); use HTML::Entities; +use HTML::TokeParser; use GDBM_File; +use Apache::lonlocal; +use Cwd; +use LONCAPA qw(:DEFAULT :match); my $iconpath; @@ -48,339 +57,2147 @@ my %alreadyseen=(); my $hadchanges; -# Available help topics my %help=(); -# Mapread read maps into lonratedt::global arrays -# @order and @resources, determines status -# sets @order - pointer to resources in right order -# sets @resources - array with the resources with correct idx -# sub mapread { my ($coursenum,$coursedom,$map)=@_; return - &Apache::lonratedt::mapread('/uploaded/'.$coursedom.'/'.$coursenum.'/'. - $map); + &LONCAPA::map::mapread('/uploaded/'.$coursedom.'/'.$coursenum.'/'. + $map); } sub storemap { my ($coursenum,$coursedom,$map)=@_; + my ($outtext,$errtext)= + &LONCAPA::map::storemap('/uploaded/'.$coursedom.'/'.$coursenum.'/'. + $map,1); + if ($errtext) { return ($errtext,2); } + $hadchanges=1; - return - &Apache::lonratedt::storemap('/uploaded/'.$coursedom.'/'.$coursenum.'/'. - $map,1); + return ($errtext,0); } -# ----------------------------------------- Return hash with valid author names + sub authorhosts { my %outhash=(); my $home=0; my $other=0; - foreach (keys %ENV) { - if ($_=~/^user\.role\.(au|ca)\.(.+)$/) { + foreach my $key (keys(%env)) { + if ($key=~/^user\.role\.(au|ca)\.(.+)$/) { my $role=$1; my $realm=$2; - my ($start,$end)=split(/\./,$ENV{$_}); + my ($start,$end)=split(/\./,$env{$key}); if (($start) && ($start>time)) { next; } if (($end) && (time>$end)) { next; } - my $ca; my $cd; + my ($ca,$cd); if ($1 eq 'au') { - $ca=$ENV{'user.name'}; - $cd=$ENV{'user.domain'}; + $ca=$env{'user.name'}; + $cd=$env{'user.domain'}; } else { - ($cd,$ca)=($realm=~/^\/(\w+)\/(\w+)$/); + ($cd,$ca)=($realm=~/^\/($match_domain)\/($match_username)$/); } - if (&Apache::lonnet::homeserver($ca,$cd) eq - $Apache::lonnet::perlvar{'lonHostID'}) { + my $allowed=0; + my $myhome=&Apache::lonnet::homeserver($ca,$cd); + my @ids=&Apache::lonnet::current_machine_ids(); + foreach my $id (@ids) { + if ($id eq $myhome) { + $allowed=1; + last; + } + } + if ($allowed) { $home++; - $outhash{'home_'.$ca.'@'.$cd}=1; + $outhash{'home_'.$ca.':'.$cd}=1; } else { - $outhash{'otherhome_'.$ca.'@'.$cd}= - &Apache::lonnet::homeserver($ca,$cd); + $outhash{'otherhome_'.$ca.':'.$cd}=$myhome; $other++; } } } return ($home,$other,%outhash); } -# ------------------------------------------------------ Generate "dump" button -sub dumpbutton { - my ($home,$other,%outhash)=&authorhosts(); - if ($home+$other==0) { return ''; } - my $output='
'.$errtext.'
'); + my $folderpath; + my $cpinfo=''; + my $plain=''; + my $randompick=-1; + my $isencrypted=0; + my $ishidden=0; + my $is_random_order=0; + while (@folders) { + my $folder=shift(@folders); + my $foldername=shift(@folders); + if ($folderpath) {$folderpath.='&';} + $folderpath.=$folder.'&'.$foldername; + my $url; + if ($allowed) { + $url = '/adm/coursedocs?folderpath='; + } else { + $url = '/adm/supplemental?folderpath='; + } + $url .= &escape($folderpath); + my $name=&unescape($foldername); +# randompick number, hidden, encrypted, random order, is appended with ":"s to the foldername + $name=~s/\:(\d*)\:(\w*)\:(\w*):(\d*)$//; + if ($1 ne '') { + $randompick=$1; + } else { + $randompick=-1; + } + if ($2) { $ishidden=1; } + if ($3) { $isencrypted=1; } + if ($4 ne '') { $is_random_order = 1; } + if ($folder eq 'supplemental') { + $name = &mt('Supplemental '.$crstype.' Content'); + } + &Apache::lonhtmlcommon::add_breadcrumb( + {'href'=>$url.$cpinfo, + 'title'=>$name, + 'text'=>$name, + 'no_mt'=>1, + }); + $plain.=$name.' > '; + } + $plain=~s/\>\;\s*$//; + return (&Apache::lonhtmlcommon::breadcrumbs(undef,undef,0,'nohelp', + undef, undef, 1 ),$randompick,$ishidden, + $isencrypted,$plain,$is_random_order); +} + +sub log_docs { + return &Apache::lonnet::instructor_log('docslog',@_); +} + +{ + my @oldresources=(); + my @oldorder=(); + my $parmidx; + my %parmaction=(); + my %parmvalue=(); + my $changedflag; + + sub snapshotbefore { + @oldresources=@LONCAPA::map::resources; + @oldorder=@LONCAPA::map::order; + $parmidx=undef; + %parmaction=(); + %parmvalue=(); + $changedflag=0; + } + + sub remember_parms { + my ($idx,$parameter,$action,$value)=@_; + $parmidx=$idx; + $parmaction{$parameter}=$action; + $parmvalue{$parameter}=$value; + $changedflag=1; + } + + sub log_differences { + my ($plain)=@_; + my %storehash=('folder' => $plain, + 'currentfolder' => $env{'form.folder'}); + if ($parmidx) { + $storehash{'parameter_res'}=$oldresources[$parmidx]; + foreach my $parm (keys(%parmaction)) { + $storehash{'parameter_action_'.$parm}=$parmaction{$parm}; + $storehash{'parameter_value_'.$parm}=$parmvalue{$parm}; + } + } + my $maxidx=$#oldresources; + if ($#LONCAPA::map::resources>$#oldresources) { + $maxidx=$#LONCAPA::map::resources; + } + for (my $idx=0; $idx<=$maxidx; $idx++) { + if ($LONCAPA::map::resources[$idx] ne $oldresources[$idx]) { + $storehash{'before_resources_'.$idx}=$oldresources[$idx]; + $storehash{'after_resources_'.$idx}=$LONCAPA::map::resources[$idx]; + $changedflag=1; + } + if ($LONCAPA::map::order[$idx] ne $oldorder[$idx]) { + $storehash{'before_order_res_'.$idx}=$oldresources[$oldorder[$idx]]; + $storehash{'after_order_res_'.$idx}=$LONCAPA::map::resources[$LONCAPA::map::order[$idx]]; + $changedflag=1; + } + } + $storehash{'maxidx'}=$maxidx; + if ($changedflag) { &log_docs(\%storehash); } + } +} + +sub docs_change_log { + my ($r,$coursenum,$coursedom,$folder,$allowed,$crstype,$iconpath)=@_; + my $supplementalflag=($env{'form.folderpath'}=~/^supplemental/); + my $js = ''."\n"; + $r->print(&Apache::loncommon::start_page('Content Change Log',$js)); + $r->print(&Apache::lonhtmlcommon::breadcrumbs('Content Change Log')); + $r->print(&startContentScreen(($supplementalflag?'suppdocs':'docs'))); + my %orderhash; + my $container='sequence'; + my $pathitem; + if ($env{'form.pagepath'}) { + $container='page'; + $pathitem = ''; + } else { + my $folderpath=$env{'form.folderpath'}; + if ($folderpath eq '') { + $folderpath = 'default&'.&escape(&mt('Main '.$crstype.' Documents')); + } + $pathitem = ''; + } + my $readfile="/uploaded/$coursedom/$coursenum/$folder.$container"; + my $jumpto = $readfile; + $jumpto =~ s{^/}{}; + my $tid = 1; + if ($supplementalflag) { + $tid = 2; + } + my ($breadcrumbtrail) = &breadcrumbs($allowed,$crstype); + $r->print($breadcrumbtrail. + &generate_edit_table($tid,\%orderhash,undef,$iconpath,$jumpto, + $readfile)); + my %docslog=&Apache::lonnet::dump('nohist_docslog', + $env{'course.'.$env{'request.course.id'}.'.domain'}, + $env{'course.'.$env{'request.course.id'}.'.num'}); + + if ((keys(%docslog))[0]=~/^error\:/) { undef(%docslog); } + + my %saveable_parameters = ('show' => 'scalar',); + &Apache::loncommon::store_course_settings('docs_log', + \%saveable_parameters); + &Apache::loncommon::restore_course_settings('docs_log', + \%saveable_parameters); + if (!$env{'form.show'}) { $env{'form.show'}=10; } +# FIXME: internationalization seems wrong here + my %lt=('hiddenresource' => 'Resources hidden', + 'encrypturl' => 'URL hidden', + 'randompick' => 'Randomly pick', + 'randomorder' => 'Randomly ordered', + 'set' => 'set to', + 'del' => 'deleted'); + my $filter = &Apache::loncommon::display_filter('docslog')."\n". + $pathitem."\n". + ''. + (' 'x2).''; + $r->print(''.&mt('Paste into Supplemental Content unavailable for this type of content.').'
'); + } + $r->print(''); +} + +sub supp_pasteable { + my ($url) = @_; + if (($url =~ m{^(?:/adm/wrapper/ext|(?:http|https)(?::|:))//}) || + (($url =~ /\.sequence$/) && ($url =~ m{^/uploaded/})) || + ($url =~ m{^/uploaded/$match_domain/$match_courseid/(docs|supplemental)/(default|\d+)/\d+/}) || + ($url =~ m{^/adm/$match_domain/$match_username/aboutme}) || + ($url =~ m{^/public/$match_domain/$match_courseid/syllabus})) { + return 1; + } + return; +} + +sub do_paste_from_buffer { + my ($coursenum,$coursedom,$folder,$errors) = @_; + + if (!$env{'form.pastemarked'}) { + return; + } + +# Preparing to paste resource at end of list + my $url=&LONCAPA::map::qtescape($env{'docs.markedcopy_url'}); + my $title=&LONCAPA::map::qtescape($env{'docs.markedcopy_title'}); + + my ($is_map,$srcdom,$srcnum,$prefixchg,%before,%after,%mapchanges); + if ($url=~/\.(page|sequence)$/) { + $is_map = 1; + } + if ($url =~ m{^/uploaded/($match_domain)/($match_courseid)/([^/]+)}) { + $srcdom = $1; + $srcnum = $2; + my $oldprefix = $3; + if (($srcdom ne $coursedom) || ($srcnum ne $coursenum)) { + unless ($env{"user.priv.cm./$srcdom/$srcnum"} =~ /\Q:mdc&F\E/) { + return &mt('Paste failed: Item is from a different course which you do not have rights to edit.'); + } + } + if (($folder =~ /^supplemental/) && (($oldprefix =~ /^default/) || ($oldprefix eq 'docs'))) { + $prefixchg = 1; + %before = ( map => 'default', + doc => 'docs'); + %after = ( map => 'supplemental', + doc => 'supplemental' ); + } elsif (($folder =~ /^default/) && ($oldprefix =~ /^supplemental/)) { + $prefixchg = 1; + %before = ( map => 'supplemental', + doc => 'supplemental'); + %after = ( map => 'default', + doc => 'docs'); + } + } + +# Supplemental content may only include certain types of content + if ($folder =~ /^supplemental/) { + unless (&supp_pasteable($env{'docs.markedcopy_url'})) { + return &mt('Paste failed: content type is not supported within Supplemental Content'); + } + } + +# Maps need to be copied first + my ($oldurl,%removefrommap,%addedmaps,%rewrites,%retitles,%copies,%dbcopies,%zombies, + %params,%docmoves,%mapmoves); + $oldurl = $url; + if ($is_map) { +# If pasting a map, check if map contains other maps + my %allmaps; + &contained_map_check($url,$folder,\%removefrommap,\%addedmaps); + if ($folder =~ /^default/) { + if (keys(%addedmaps) > 0) { + &reinit_role($coursedom,$coursenum,$env{"course.$env{'request.course.id'}.home"}); + } + my $navmap = Apache::lonnavmaps::navmap->new(); + if (defined($navmap)) { + foreach my $res ($navmap->retrieveResources(undef,sub { $_[0]->is_map() },1,0,1)) { + $allmaps{$res->src()} = 1; + } + } + } + if ($url=~ m{^/uploaded/}) { + $title=&mt('Copy of').' '.$title; + } + my $now = time; + my $suffix=$$.int(rand(100)).$now; + my ($oldid,$ext) = ($url=~/^(.+)\.(\w+)$/); + if ($oldid =~ m{^(/uploaded/$match_domain/$match_courseid/)(\D+)(\d+)$}) { + my $path = $1; + my $prefix = $2; + my $ancestor = $3; + if (length($ancestor) > 10) { + $ancestor = substr($ancestor,-10,10); + } + my ($newurl,$newid); + if ($prefixchg) { + if ($folder =~ /^supplemental/) { + $prefix =~ s/^default/supplemental/; + } else { + $prefix =~ s/^supplemental/default/; + } + } + if (($srcdom eq $coursedom) && ($srcnum eq $coursenum)) { + $newurl = $path.$prefix.$ancestor.$suffix.'.'.$ext; + } else { + $newurl = "/uploaded/$coursedom/$coursenum/$prefix".$now.'.'.$ext; + } + my $counter = 0; + my $is_unique = &uniqueness_check($newurl); + if ($folder =~ /^default/) { + if ($allmaps{$newurl}) { + $is_unique = 0; + } + } + while (!$is_unique && $allmaps{$newurl} && $counter < 100) { + $counter ++; + $suffix ++; + if (($srcdom eq $coursedom) && ($srcnum eq $coursenum)) { + $newurl = $path.$prefix.$ancestor.$suffix.'.'.$ext; + } else { + $newurl = "/uploaded/$coursedom/$coursenum/$prefix".$ancestor.$suffix.'.'.$ext; + } + $is_unique = &uniqueness_check($newurl); + } + if ($is_unique) { + if ($newurl ne $oldurl) { + $mapchanges{$oldurl} = 1; + } + if (($srcdom ne $coursedom) || ($srcnum ne $coursenum) || ($prefixchg)) { + &url_paste_fixups($url,$prefixchg,$coursedom,$coursenum,\%allmaps, + \%rewrites,\%retitles,\%copies,\%dbcopies,\%zombies, + \%params,\%mapmoves,\%mapchanges); + } + } else { + if ($url=~/\.page$/) { + return &mt('Paste failed: an error occurred creating a unique URL for the composite page'); + } else { + return &mt('Paste failed: an error occurred creating a unique URL for the folder'); + } + } + my $storefn=$newurl; + $storefn=~s{^/\w+/$match_domain/$match_username/}{}; + my $paste_map_result = + &Apache::lonclonecourse::writefile($env{'request.course.id'},$storefn, + &Apache::lonnet::getfile($url)); + if ($paste_map_result eq '/adm/notfound.html') { + if ($url=~/\.page$/) { + return &mt('Paste failed: an error occurred saving the composite page.'); + } else { + return &mt('Paste failed: an error occurred saving the folder.'); + } + } + $url = $newurl; + } elsif ($url=~m {^/res/}) { +# published maps can only exists once, so remove it from paste buffer when done + &Apache::lonnet::delenv('docs.markedcopy'); + if ($folder =~ /^default/) { + if ($allmaps{$url}) { + return &mt('Paste failed: only one instance of a particular published sequence or page is allowed within each course.'); + } + } + } + } + if ($url=~ m{/smppg$}) { + my $db_name = &Apache::lonsimplepage::get_db_name($url); + if ($db_name =~ /^smppage_/) { + #simple pages, need to copy the db contents to a new one. + my %contents=&Apache::lonnet::dump($db_name,$coursedom,$coursenum); + my $now = time(); + $db_name =~ s{_\d*$ }{_$now}x; + my $dbresult=&Apache::lonnet::put($db_name,\%contents, + $coursedom,$coursenum); + if ($dbresult eq 'ok') { + $url =~ s{/(\d*)/smppg$ }{/$now/smppg}x; + $title=&mt('Copy of').' '.$title; + } else { + return &mt('Paste failed: An error occurred when copying the simple page.'); + } + } + } + $title = &LONCAPA::map::qtunescape($title); + my $ext='false'; + if ($url=~m{^http(|s)://}) { $ext='true'; } + $url = &LONCAPA::map::qtunescape($url); +# Now insert the URL at the bottom + my $newidx = &LONCAPA::map::getresidx($url); + +# For uploaded files (excluding pages/sequences) path in copied file is changed +# if paste is from Main to Supplemental (or vice versa), or if pasting between +# courses. + + unless ($is_map) { + if ($url =~ m{^/uploaded/$match_domain/$match_courseid/(?:docs|supplemental)/(.+)$}) { + my $relpath = $1; + if ($relpath ne '') { + my ($prefix,$subdir,$rem) = ($relpath =~ m{^(default|\d+)/(\d+)/(.+)$}); + my ($newloc,$newsubdir) = ($folder =~ /^(default|supplemental)_?(\d*)/); + my $newprefix = $newloc; + if ($newloc eq 'default') { + $newprefix = 'docs'; + } + if ($newsubdir eq '') { + $newsubdir = 'default'; + } + if (($prefixchg) || ($srcdom ne $coursedom) || ($srcnum ne $coursenum)) { + my $newpath = "$newprefix/$newsubdir/$newidx/$rem"; + $url = + &Apache::lonclonecourse::writefile($env{'request.course.id'},$newpath, + &Apache::lonnet::getfile($oldurl)); + if ($url eq '/adm/notfound.html') { + return &mt('Paste failed: an error occurred saving the file.'); + } else { + my ($newsubpath) = ($newpath =~ m{^(.*/)[^/]*$}); + $newsubpath =~ s{/+$}{/}; + $docmoves{$oldurl} = $newsubpath; + } + } + } + } + } + my $result = + &apply_fixups($is_map,$prefixchg,$coursedom,$coursenum,$oldurl,$url, + \%removefrommap,\%rewrites,\%retitles,\%copies,\%dbcopies, + \%zombies,\%params,\%docmoves,\%mapmoves,$errors,\%before,\%after); + if ($result eq 'ok') { + if ($env{'docs.markedcopy_supplemental'}) { + if ($folder =~ /^supplemental/) { + $title = $env{'docs.markedcopy_supplemental'}; + } else { + (undef,undef,$title) = + &Apache::loncommon::parse_supplemental_title($env{'docs.markedcopy_supplemental'}); + } + } else { + if ($folder=~/^supplemental/) { + $title=time.'___&&&___'.$env{'user.name'}.'___&&&___'. + $env{'user.domain'}.'___&&&___'.$title; + } + } + $LONCAPA::map::resources[$newidx]= $title.':'.$url.':'.$ext.':normal:res'; + push(@LONCAPA::map::order, $newidx); + } + return $result; +} + +sub dbcopy { + my ($url,$coursedom,$coursenum) = @_; + if ($url=~ m{/smppg$}) { + my $db_name = &Apache::lonsimplepage::get_db_name($url); + if ($db_name =~ /^smppage_/) { + #simple pages, need to copy the db contents to a new one. + my %contents=&Apache::lonnet::dump($db_name,$coursedom,$coursenum); + my $now = time(); + $db_name =~ s{_\d*$ }{_$now}x; + my $result=&Apache::lonnet::put($db_name,\%contents, + $coursedom,$coursenum); + $url =~ s{/(\d*)/smppg$ }{/$now/smppg}x; + } + } + return $url; +} + +sub uniqueness_check { + my ($newurl) = @_; + my $unique = 1; + foreach my $res (@LONCAPA::map::order) { + my ($name,$url)=split(/\:/,$LONCAPA::map::resources[$res]); + $url=&LONCAPA::map::qtescape($url); + if ($newurl eq $url) { + $unique = 0; + last; + } + } + return $unique; +} + +sub contained_map_check { + my ($url,$folder,$removefrommap,$addedmaps) = @_; + my $content = &Apache::lonnet::getfile($url); + unless ($content eq '-1') { + my $parser = HTML::TokeParser->new(\$content); + $parser->attr_encoded(1); + while (my $token = $parser->get_token) { + next if ($token->[0] ne 'S'); + if ($token->[1] eq 'resource') { + next if ($token->[2]->{'type'} eq 'zombie'); + my $ressrc = $token->[2]->{'src'}; + if ($folder =~ /^supplemental/) { + unless (&supp_pasteable($ressrc)) { + $removefrommap->{$url}{$token->[2]->{'id'}}; + next; + } + } + if ($ressrc =~ /\.(sequence|page)$/) { + if (ref($addedmaps->{$ressrc}) eq 'ARRAY') { + push(@{$addedmaps->{$ressrc}},$url); + } else { + $addedmaps->{$ressrc} = [$url]; + } + &contained_map_check($ressrc,$folder,$removefrommap,$addedmaps); + } + } elsif ($token->[1] !~ /^resource|map|link$/) { + if ($folder =~ /^supplemental/) { + $removefrommap->{$url}{$token->[1]}; + } + } + } + } + return; +} + +sub reinit_role { + my ($cdom,$cnum,$chome) = @_; + my ($furl,$ferr) = &Apache::lonuserstate::readmap("$cdom/$cnum"); + unless ($ferr) { + &Apache::loncommon::update_content_constraints($cdom,$cnum,$chome,$cdom.'_'.$cnum); + } + return; +} + +sub url_paste_fixups { + my ($oldurl,$prefixchg,$cdom,$cnum,$allmaps,$rewrites,$retitles,$copies, + $dbcopies,$zombies,$params,$mapmoves,$mapchanges) = @_; + my $checktitle; + if (($prefixchg) && + ($oldurl =~ m{^/uploaded/($match_domain)/($match_courseid)/supplemental})) { + $checktitle = 1; + } + my $file = &Apache::lonnet::getfile($oldurl); + return if ($file eq '-1'); + my $parser = HTML::TokeParser->new(\$file); + $parser->attr_encoded(1); + my $changed = 0; + while (my $token = $parser->get_token) { + next if ($token->[0] ne 'S'); + if ($token->[1] eq 'resource') { + my $ressrc = $token->[2]->{'src'}; + next if ($ressrc eq ''); + my $id = $token->[2]->{'id'}; + if ($checktitle) { + my $title = $token->[2]->{'title'}; + if ($title =~ m{\d+\Q___&&&___\E$match_username\Q___&&&___\E$match_domain\Q___&&&___\E(.+)$}) { + $retitles->{$oldurl}{$ressrc} = $id; + + } + } + next if ($token->[2]->{'type'} eq 'external'); + if ($token->[2]->{'type'} eq 'zombie') { + $zombies->{$oldurl}{$ressrc} = $id; + $changed = 1; + } elsif ($ressrc =~ m{^/uploaded/($match_domain)/($match_courseid)/(.+)$}) { + my $srccdom = $1; + my $srccnum = $2; + my $rem = $3; + if (($srccdom ne $cdom) || ($srccnum ne $cnum) || ($prefixchg) || + ($mapchanges->{$oldurl})) { + if ($rem =~ /^(default|supplemental)(_?\d*).(sequence|page)$/) { + $rewrites->{$oldurl}{$ressrc} = $id; + $mapchanges->{$ressrc} = 1; + unless (&url_paste_fixups($ressrc,$prefixchg,$cdom,$cnum,$allmaps, + $rewrites,$retitles,$copies,$dbcopies,$zombies, + $params,$mapmoves,$mapchanges)) { + $mapmoves->{$ressrc} = 1; + } + $changed = 1; + } else { + $rewrites->{$oldurl}{$ressrc} = $id; + $copies->{$oldurl}{$ressrc} = $id; + $changed = 1; + } + } + } elsif ($ressrc =~ m{^/adm/($match_domain)/($match_courseid)/(.+)$}) { + my $srccdom = $1; + my $srccnum = $2; + if (($srccdom ne $cdom) || ($srccnum ne $cnum)) { + $rewrites->{$oldurl}{$ressrc} = $id; + $dbcopies->{$oldurl}{$ressrc} = $id; + $changed = 1; + } + } elsif ($ressrc =~ m{^/public/($match_domain)/($match_courseid)/(.+)$}) { + my $srccdom = $1; + my $srccnum = $2; + if (($srccdom ne $cdom) || ($srccnum ne $cnum)) { + $rewrites->{$oldurl}{$ressrc} = $id; + $dbcopies->{$oldurl}{$ressrc} = $id; + $changed = 1; + } + } + } elsif ($token->[1] eq 'param') { + my $to = $token->[2]->{'to'}; + if ($to ne '') { + if (ref($params->{$oldurl}{$to}) eq 'ARRAY') { + push (@{$params->{$oldurl}{$to}},$token->[2]->{'name'}); + } else { + @{$params->{$oldurl}{$to}} = ($token->[2]->{'name'}); + } + } + } + } + return $changed; +} + +sub apply_fixups { + my ($is_map,$prefixchg,$cdom,$cnum,$oldurl,$url,$removefrommap,$rewrites, + $retitles,$copies,$dbcopies,$zombies,$params,$docmoves,$mapmoves,$errors, + $before,$after) = @_; + my ($oldsubdir,$newsubdir,$subdirchg); + if ($is_map) { + ($oldsubdir) = + ($oldurl =~ m{^/uploaded/$match_domain/$match_courseid/(?:default|supplemental)_?(\d*)}); + if ($oldsubdir eq '') { + $oldsubdir = 'default'; + } + ($newsubdir) = + ($url =~ m{^/uploaded/$match_domain/$match_courseid/(?:default|supplemental)_?(\d*)}); + if ($newsubdir eq '') { + $newsubdir = 'default'; + } + if ($oldsubdir ne $newsubdir) { + $subdirchg = 1; + } + } + foreach my $key (keys(%{$copies}),keys(%{$docmoves})) { + my @allcopies; + if (ref($copies->{$key}) eq 'HASH') { + my %added; + foreach my $innerkey (keys(%{$copies->{$key}})) { + if (($innerkey ne '') && (!$added{$innerkey})) { + push(@allcopies,$innerkey); + $added{$innerkey} = 1; + } + } + undef(%added); + } + if ($key eq $oldurl) { + if ((exists($docmoves->{$key}))) { + unless (grep(/^\Q$oldurl\E/,@allcopies)) { + push(@allcopies,$oldurl); + } + } + } + if (@allcopies > 0) { + foreach my $item (@allcopies) { + my ($relpath,$fname) = + ($item =~ m{^(/uploaded/$match_domain/$match_courseid/(?:docs|supplemental)/(?:default|\d+)/.*/)([^/]+)$}); + if ($fname ne '') { + my $content = &Apache::lonnet::getfile($item); + unless ($content eq '-1') { + my $storefn; + if (($key eq $oldurl) && (ref($docmoves) eq 'HASH') && (exists($docmoves->{$key}))) { + $storefn = $docmoves->{$key}; + } else { + $storefn = $relpath; + $storefn =~s{^/uploaded/$match_domain/$match_courseid/}{}; + if ($prefixchg) { + $storefn =~ s/^\Q$before->{'doc'}\E/$after->{'doc'}/; + } + if (($key eq $oldurl) && ($subdirchg)) { + $storefn =~ s{^(docs|supplemental)/\Q$oldsubdir\E/}{$1/$newsubdir/}; + } + } + ©_dependencies($item,$storefn,$relpath,$errors,\$content); + my $copyurl = + &Apache::lonclonecourse::writefile($env{'request.course.id'}, + $storefn.$fname,$content); + if ($copyurl eq '/adm/notfound.html') { + if ((ref($docmoves) eq 'HASH') && (exists($docmoves->{$oldurl}))) { + return &mt('Paste failed: an error occurred copying the file.'); + } elsif (ref($errors) eq 'HASH') { + $errors->{$item} = 1; + } + } + } + } + } + } + } + foreach my $key (keys(%{$mapmoves})) { + my $storefn=$key; + $storefn=~s{^/uploaded/$match_domain/$match_courseid/}{}; + if ($prefixchg) { + $storefn =~ s/^\Q$before->{'map'}\E/$after->{'map'}/; + } + my $mapcontent = &Apache::lonnet::getfile($key); + if ($mapcontent eq '-1') { + if (ref($errors) eq 'HASH') { + $errors->{$key} = 1; + } + } else { + my $newmap = + &Apache::lonclonecourse::writefile($env{'request.course.id'},$storefn, + $mapcontent); + if ($newmap eq '/adm/notfound.html') { + if (ref($errors) eq 'HASH') { + $errors->{$key} = 1; + } + } + } + } + my %updates; + if ($is_map) { + foreach my $key (keys(%{$rewrites})) { + $updates{$key} = 1; + } + foreach my $key (keys(%{$zombies})) { + $updates{$key} = 1; + } + foreach my $key (keys(%{$removefrommap})) { + $updates{$key} = 1; + } + foreach my $key (keys(%{$dbcopies})) { + $updates{$key} = 1; + } + foreach my $key (keys(%{$retitles})) { + $updates{$key} = 1; + } + foreach my $key (keys(%updates)) { + my (%torewrite,%toretitle,%toremove,%zombie,%newdb); + if (ref($rewrites->{$key}) eq 'HASH') { + %torewrite = %{$rewrites->{$key}}; + } + if (ref($retitles->{$key}) eq 'HASH') { + %toretitle = %{$retitles->{$key}}; + } + if (ref($removefrommap->{$key}) eq 'HASH') { + %toremove = %{$removefrommap->{$key}}; + } + if (ref($zombies->{$key}) eq 'HASH') { + %zombie = %{$zombies->{$key}}; + } + if (ref($dbcopies->{$key}) eq 'HASH') { + foreach my $item (keys(%{$dbcopies->{$key}})) { + $newdb{$item} = &dbcopy($item); + } + } + my $map = &Apache::lonnet::getfile($key); + my $newcontent; + if ($map eq '-1') { + return &mt('Paste failed: an error occurred reading a folder or page: [_1].',$key); + } else { + my $parser = HTML::TokeParser->new(\$map); + $parser->attr_encoded(1); + while (my $token = $parser->get_token) { + if ($token->[0] eq 'S') { + if ($token->[2]->{'type'} eq 'zombie') { + next if (($token->[2]->{'src'} ne '') && + ($zombie{$token->[2]->{'src'}} eq $token->[2]->{'id'})); + } + if ($token->[1] eq 'resource') { + my $src = $token->[2]->{'src'}; + my $id = $token->[2]->{'id'}; + my $title = $token->[2]->{'title'}; + my $changed; + if ((exists($toretitle{$src})) && ($toretitle{$src} eq $id)) { + if ($title =~ m{^\d+\Q___&&&___\E$match_username\Q___&&&___\E$match_domain\Q___&&&___\E(.+)$}) { + $token->[2]->{'title'} = $1; + $changed = 1; + } + } + if ((exists($torewrite{$src})) && ($torewrite{$src} eq $id)) { + $src =~ s{^/(uploaded|adm|public)/$match_domain/$match_courseid/}{/$1/$cdom/$cnum/}; + if ($src =~ m{^/uploaded/}) { + if ($prefixchg) { + if ($src =~ /\.(page|sequence)$/) { + $src =~ s#^(/uploaded/$match_domain/$match_courseid/)\Q$before->{'map'}\E#$1$after->{'map'}#; + } else { + $src =~ s#^(/uploaded/$match_domain/$match_courseid/)\Q$before->{'doc'}\E#$1$after->{'doc'}#; + } + } + if (($key eq $oldurl) && ($src !~ /\.(page|sequence)$/) && ($subdirchg)) { + $src =~ s{^(/uploaded/$match_domain/$match_courseid/\w+/)\Q$oldsubdir\E}{$1$newsubdir}; + } + } + $token->[2]->{'src'} = $src; + $changed = 1; + } elsif ($newdb{$src} ne '') { + $token->[2]->{'src'} = $newdb{$src}; + $changed = 1; + } + if ($changed) { + $newcontent .= "<$token->[1]"; + foreach my $attr (@{$token->[3]}) { + if ($attr =~ /^\w+$/) { + $newcontent .= ' '.$attr.'="'.$token->[2]->{$attr}.'"'; + } + } + $newcontent .= ' />'."\n"; + } else { + $newcontent .= $token->[4]."\n"; + } + } elsif (($token->[2]->{'id'} ne '') && + (exists($toremove{$token->[2]->{'id'}}))) { + next; + } else { + $newcontent .= $token->[4]."\n"; + } + } elsif ($token->[0] eq 'E') { + $newcontent .= $token->[2]."\n"; + } + } + } + my $storefn; + if ($key eq $oldurl) { + $storefn = $url; + $storefn=~s{^/uploaded/$match_domain/$match_courseid/}{}; + } else { + $storefn = $key; + $storefn=~s{^/uploaded/$match_domain/$match_courseid/}{}; + if ($prefixchg) { + $storefn =~ s/^\Q$before->{'map'}\E/$after->{'map'}/; + } + } + my $newmapurl = + &Apache::lonclonecourse::writefile($env{'request.course.id'},$storefn, + $newcontent); + if ($newmapurl eq '/adm/notfound.html') { + return &mt('Paste failed: an error occurred saving the folder or page.'); + } + } + } + return 'ok'; +} + +sub copy_dependencies { + my ($item,$storefn,$relpath,$errors,$contentref) = @_; + my $content; + if (ref($contentref)) { + $content = $$contentref; + } else { + $content = &Apache::lonnet::getfile($item); + } + unless ($content eq '-1') { + my $mm = new File::MMagic; + my $mimetype = $mm->checktype_contents($content); + if ($mimetype eq 'text/html') { + my (%allfiles,%codebase,$state); + my $res = &Apache::lonnet::extract_embedded_items(undef,\%allfiles,\%codebase,\$content); + if ($res eq 'ok') { + my ($numexisting,$numpathchanges,$existing); + (undef,$numexisting,$numpathchanges,$existing) = + &Apache::loncommon::ask_for_embedded_content( + '/adm/coursedocs',$state,\%allfiles,\%codebase, + {'error_on_invalid_names' => 1, + 'ignore_remote_references' => 1, + 'docs_url' => $item, + 'context' => 'paste'}); + if ($numexisting > 0) { + if (ref($existing) eq 'HASH') { + foreach my $dep (keys(%{$existing})) { + my $depfile = $dep; + unless ($depfile =~ m{^\Q$relpath\E}) { + $depfile = $relpath.$dep; + } + my $depcontent = &Apache::lonnet::getfile($depfile); + unless ($depcontent eq '-1') { + my $storedep = $dep; + $storedep =~ s{^\Q$relpath\E}{}; + my $dep_url = + &Apache::lonclonecourse::writefile( + $env{'request.course.id'}, + $storefn.$storedep,$depcontent); + if ($dep_url eq '/adm/notfound.html') { + if (ref($errors) eq 'HASH') { + $errors->{$depfile} = 1; + } + } else { + ©_dependencies($depfile,$storefn,$relpath,$errors,\$depcontent); + } + } + } + } + } + } + } + } + return; +} + +my %parameter_type = ( 'randompick' => 'int_pos', + 'hiddenresource' => 'string_yesno', + 'encrypturl' => 'string_yesno', + 'randomorder' => 'string_yesno',); +my $valid_parameters_re = join('|',keys(%parameter_type)); +# set parameters +sub update_parameter { + + return 0 if ($env{'form.changeparms'} !~ /^($valid_parameters_re)$/); + + my $which = $env{'form.changeparms'}; + my $idx = $env{'form.setparms'}; + if ($env{'form.'.$which.'_'.$idx}) { + my $value = ($which eq 'randompick') ? $env{'form.'.$which.'_'.$idx} + : 'yes'; + &LONCAPA::map::storeparameter($idx, 'parameter_'.$which, $value, + $parameter_type{$which}); + &remember_parms($idx,$which,'set',$value); } else { + &LONCAPA::map::delparameter($idx,'parameter_'.$which); + + &remember_parms($idx,$which,'del'); + } + return 1; +} + + +sub handle_edit_cmd { + my ($coursenum,$coursedom) =@_; + my ($cmd,$idx)=split('_',$env{'form.cmd'}); + + my $ratstr = $LONCAPA::map::resources[$LONCAPA::map::order[$idx]]; + my ($title, $url, @rrest) = split(':', $ratstr); + + if ($cmd eq 'del') { + if (($url=~m|/+uploaded/\Q$coursedom\E/\Q$coursenum\E/|) && + ($url!~/$LONCAPA::assess_page_seq_re/)) { + &Apache::lonnet::removeuploadedurl($url); + } else { + &LONCAPA::map::makezombie($LONCAPA::map::order[$idx]); + } + splice(@LONCAPA::map::order, $idx, 1); + + } elsif ($cmd eq 'cut') { + &LONCAPA::map::makezombie($LONCAPA::map::order[$idx]); + splice(@LONCAPA::map::order, $idx, 1); + + } elsif ($cmd eq 'up' + && ($idx) && (defined($LONCAPA::map::order[$idx-1]))) { + @LONCAPA::map::order[$idx-1,$idx] = @LONCAPA::map::order[$idx,$idx-1]; + + } elsif ($cmd eq 'down' + && defined($LONCAPA::map::order[$idx+1])) { + @LONCAPA::map::order[$idx+1,$idx] = @LONCAPA::map::order[$idx,$idx+1]; + + } elsif ($cmd eq 'rename') { + + my $comment = &LONCAPA::map::qtunescape($env{'form.title'}); + if ($comment=~/\S/) { + $LONCAPA::map::resources[$LONCAPA::map::order[$idx]]= + $comment.':'.join(':', $url, @rrest); + } +# Devalidate title cache + my $renamed_url=&LONCAPA::map::qtescape($url); + &Apache::lonnet::devalidate_title_cache($renamed_url); + } else { + return 0; + } + return 1; +} + +sub editor { + my ($r,$coursenum,$coursedom,$folder,$allowed,$upload_output,$crstype, + $supplementalflag,$orderhash,$iconpath)=@_; + my $container= ($env{'form.pagepath'}) ? 'page' + : 'sequence'; + + my ($breadcrumbtrail,$randompick,$ishidden,$isencrypted,$plain,$is_random_order) = + &breadcrumbs($allowed,$crstype); + $r->print($breadcrumbtrail); + + my $jumpto = "uploaded/$coursedom/$coursenum/$folder.$container"; + + unless ($allowed) { + $randompick = -1; + } + + my ($errtext,$fatal) = &mapread($coursenum,$coursedom, + $folder.'.'.$container); + return $errtext if ($fatal); + + if ($#LONCAPA::map::order<1) { + my $idx=&LONCAPA::map::getresidx(); + if ($idx<=0) { $idx=1; } + $LONCAPA::map::order[0]=$idx; + $LONCAPA::map::resources[$idx]=''; + } + # ------------------------------------------------------------ Process commands + # ---------------- if they are for this folder and user allowed to make changes - if (($allowed) && ($ENV{'form.folder'} eq $folder)) { -# upload a file, if present - if (($ENV{'form.uploaddoc.filename'}) && - ($ENV{'form.cmd'}=~/^upload_(\w+)/)) { - if ($folder=~/^$1/) { -# this is for a course, not a user, so set coursedoc flag -# probably the only place in the system where this should be "1" - my $url=&Apache::lonnet::userfileupload('uploaddoc',1); - my $ext='false'; - if ($url=~/^http\:\/\//) { $ext='true'; } - $url=~s/\:/\:/g; - my $comment=$ENV{'form.comment'}; - $comment=~s/\\<\;/g; - $comment=~s/\>/\>\;/g; - $comment=~s/\:/\:/g; - if ($folder=~/^supplemental/) { - $comment=time.'___&&&___'.$ENV{'user.name'}.'___&&&___'. - $ENV{'user.domain'}.'___&&&___'.$comment; - } - my $newidx=$#Apache::lonratedt::resources+1; - $Apache::lonratedt::resources[$newidx]= - $comment.':'.$url.':'.$ext.':normal:res'; - $Apache::lonratedt::order[$#Apache::lonratedt::order+1]= - $newidx; - &storemap($coursenum,$coursedom,$folder.'.sequence'); - } - } - if ($ENV{'form.cmd'}) { - my ($cmd,$idx)=split(/\_/,$ENV{'form.cmd'}); - if ($cmd eq 'del') { - for (my $i=$idx;$i<$#Apache::lonratedt::order;$i++) { - $Apache::lonratedt::order[$i]= - $Apache::lonratedt::order[$i+1]; - } - $#Apache::lonratedt::order--; - } elsif ($cmd eq 'up') { - if (($idx) && (defined($Apache::lonratedt::order[$idx-1]))) { - my $i=$Apache::lonratedt::order[$idx-1]; - $Apache::lonratedt::order[$idx-1]= - $Apache::lonratedt::order[$idx]; - $Apache::lonratedt::order[$idx]=$i; - } - } elsif ($cmd eq 'down') { - if (defined($Apache::lonratedt::order[$idx+1])) { - my $i=$Apache::lonratedt::order[$idx+1]; - $Apache::lonratedt::order[$idx+1]= - $Apache::lonratedt::order[$idx]; - $Apache::lonratedt::order[$idx]=$i; - } - } elsif ($cmd eq 'rename') { - my ($rtitle,@rrest)=split(/\:/, - $Apache::lonratedt::resources[ - $Apache::lonratedt::order[$idx]]); - my $comment= - &HTML::Entities::decode($ENV{'form.title'}); - $comment=~s/\\<\;/g; - $comment=~s/\>/\>\;/g; - $comment=~s/\:/\:/g; - $Apache::lonratedt::resources[ - $Apache::lonratedt::order[$idx]]= - $comment.':'.join(':',@rrest); - + if (($allowed) && ($env{'form.folder'} eq $folder)) { +# set parameters and change order + &snapshotbefore(); + + if (&update_parameter()) { + ($errtext,$fatal)=&storemap($coursenum,$coursedom,$folder.'.'.$container); + return $errtext if ($fatal); + } + + if ($env{'form.newpos'} && $env{'form.currentpos'}) { +# change order + my $res = splice(@LONCAPA::map::order,$env{'form.currentpos'}-1,1); + splice(@LONCAPA::map::order,$env{'form.newpos'}-1,0,$res); + + ($errtext,$fatal)=&storemap($coursenum,$coursedom,$folder.'.'.$container); + return $errtext if ($fatal); + } + + if ($env{'form.pastemarked'}) { + my %paste_errors; + my $paste_res = + &do_paste_from_buffer($coursenum,$coursedom,$folder,\%paste_errors); + if ($paste_res eq 'ok') { +# Store the result + ($errtext,$fatal) = &storemap($coursenum,$coursedom,$folder.'.'.$container); + return $errtext if ($fatal); + } elsif ($paste_res ne '') { + $r->print(''.$paste_res.'
'); + } + if (keys(%paste_errors) > 0) { + $r->print(''."\n". + &mt('The following files are either dependencies of a web page or references within a folder and/or composite page which could not be copied during the paste operation:')."\n". + '
'.&mt('No map selected.').'
'); -# Store the changed version - &storemap($coursenum,$coursedom,$folder.'.sequence'); - } - } + } + } + &log_differences($plain); + } # ---------------------------------------------------------------- End commands # ---------------------------------------------------------------- Print screen - my $idx=0; - $r->print(''.&mt('Move').' | ' + .''.&mt('Actions').' | ' + .''.&mt('Document').' | '; + if ($folder !~ /^supplemental/) { + $to_show .= ''.&mt('Settings').' | '; + } + $to_show .= &Apache::loncommon::end_data_table_header_row(); + } + $to_show .= $output.' ' + .&Apache::loncommon::end_data_table() + .'
---|
'.&mt('Uploaded [_1]',''. + $stored.'').'
'; + } else { + my ($filename) = ($env{'form.uploaddoc.filename'} =~ m{([^/]+)$}); + + $$upload_output = ''.&mt('This file contains embedded multimedia objects, which need to be uploaded.').'
'.$embedded; + $nextphase = $uploadphase; + } else { + $$upload_output .= $embedded; + } + } else { + $$upload_output .= &mt('Embedded item(s) already present, so no additional upload(s) required').'
-
-![]() |
-
-![]() |
'.&mt('Done').'
'); +} + + +sub devalidateversioncache { + my $src=shift; + &Apache::lonnet::devalidate_cache_new('courseresversion',$env{'request.course.id'}.'_'. + &Apache::lonnet::clutter($src)); +} + +sub checkversions { + my ($r) = @_; + my $crstype = &Apache::loncommon::course_type(); + $r->print(&Apache::loncommon::start_page("Check $crstype Document Versions")); + $r->print(&Apache::lonhtmlcommon::breadcrumbs("Check $crstype Document Versions")); + $r->print(&startContentScreen('tools')); + + my $header=''; + my $startsel=''; + my $monthsel=''; + my $weeksel=''; + my $daysel=''; + my $allsel=''; + my %changes=(); + my $starttime=0; + my $haschanged=0; + my %setversions=&Apache::lonnet::dump('resourceversions', + $env{'course.'.$env{'request.course.id'}.'.domain'}, + $env{'course.'.$env{'request.course.id'}.'.num'}); + + $hashtied=0; + &tiehash(); + my %newsetversions=(); + if ($env{'form.setmostrecent'}) { + $haschanged=1; + foreach my $key (keys(%hash)) { + if ($key=~/^ids\_(\/res\/.+)$/) { + $newsetversions{$1}='mostrecent'; + &devalidateversioncache($1); + } + } + } elsif ($env{'form.setcurrent'}) { + $haschanged=1; + foreach my $key (keys(%hash)) { + if ($key=~/^ids\_(\/res\/.+)$/) { + my $getvers=&Apache::lonnet::getversion($1); + if ($getvers>0) { + $newsetversions{$1}=$getvers; + &devalidateversioncache($1); + } + } + } + } elsif ($env{'form.setversions'}) { + $haschanged=1; + foreach my $key (keys(%env)) { + if ($key=~/^form\.set_version_(.+)$/) { + my $src=$1; + if (($env{$key}) && ($env{$key} ne $setversions{$src})) { + $newsetversions{$src}=$env{$key}; + &devalidateversioncache($src); + } + } + } + } + if ($haschanged) { + if (&Apache::lonnet::put('resourceversions',\%newsetversions, + $env{'course.'.$env{'request.course.id'}.'.domain'}, + $env{'course.'.$env{'request.course.id'}.'.num'}) eq 'ok') { + $r->print(&Apache::loncommon::confirmwrapper( + &Apache::lonhtmlcommon::confirm_success(&mt('Your Version Settings have been Saved')))); + } else { + $r->print(&Apache::loncommon::confirmwrapper( + &Apache::lonhtmlcommon::confirm_success(&mt('An Error Occured while Attempting to Save your Version Settings'),1))); + } + &mark_hash_old(); + } + &changewarning($r,''); + if ($env{'form.timerange'} eq 'all') { +# show all documents + $header=&mt('All Documents in '.$crstype); + $allsel=1; + foreach my $key (keys(%hash)) { + if ($key=~/^ids\_(\/res\/.+)$/) { + my $src=$1; + $changes{$src}=1; + } + } + } else { +# show documents which changed + %changes=&Apache::lonnet::dump + ('versionupdate',$env{'course.'.$env{'request.course.id'}.'.domain'}, + $env{'course.'.$env{'request.course.id'}.'.num'}); + my $firstkey=(keys(%changes))[0]; + unless ($firstkey=~/^error\:/) { + unless ($env{'form.timerange'}) { + $env{'form.timerange'}=604800; + } + my $seltext=&mt('during the last').' '.$env{'form.timerange'}.' ' + .&mt('seconds'); + if ($env{'form.timerange'}==-1) { + $seltext='since start of course'; + $startsel='selected'; + $env{'form.timerange'}=time; + } + $starttime=time-$env{'form.timerange'}; + if ($env{'form.timerange'}==2592000) { + $seltext=&mt('during the last month').' ('.&Apache::lonlocal::locallocaltime($starttime).')'; + $monthsel='selected'; + } elsif ($env{'form.timerange'}==604800) { + $seltext=&mt('during the last week').' ('.&Apache::lonlocal::locallocaltime($starttime).')'; + $weeksel='selected'; + } elsif ($env{'form.timerange'}==86400) { + $seltext=&mt('since yesterday').' ('.&Apache::lonlocal::locallocaltime($starttime).')'; + $daysel='selected'; + } + $header=&mt('Content changed').' '.$seltext; + } else { + $header=&mt('No content modifications yet.'); + } + } + %setversions=&Apache::lonnet::dump('resourceversions', + $env{'course.'.$env{'request.course.id'}.'.domain'}, + $env{'course.'.$env{'request.course.id'}.'.num'}); + my %lt=&Apache::lonlocal::texthash + ('st' => 'Version changes since start of '.$crstype, + 'lm' => 'Version changes since last Month', + 'lw' => 'Version changes since last Week', + 'sy' => 'Version changes since Yesterday', + 'al' => 'All Resources (possibly large output)', + 'cd' => 'Change display', + 'sd' => 'Display', + 'fi' => 'File', + 'md' => 'Modification Date', + 'mr' => 'Most recently published Version', + 've' => 'Version used in '.$crstype, + 'vu' => 'Set Version to be used in '.$crstype, +'sv' => 'Set Versions to be used in '.$crstype.' according to Selections below', +'sm' => 'Keep all Resources up-to-date with most recent Versions (default)', +'sc' => 'Set all Resource Versions to current Version (Fix Versions)', + 'di' => 'Differences', + 'save' => 'Save changes', + 'vers' => 'Version choice(s) for specific resources', + 'act' => 'Actions'); + $r->print(<