# The LearningOnline Network with CAPA
# Publication Handler
#
# $Id: lonpublisher.pm,v 1.294 2014/12/12 18:27:34 raeburn Exp $
#
# Copyright Michigan State University Board of Trustees
#
# This file is part of the LearningOnline Network with CAPA (LON-CAPA).
#
# LON-CAPA is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# LON-CAPA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with LON-CAPA; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# /home/httpd/html/adm/gpl.txt
#
# http://www.lon-capa.org/
#
###
###############################################################################
## ##
## ORGANIZATION OF THIS PERL MODULE ##
## ##
## 1. Modules used by this module ##
## 2. Various subroutines ##
## 3. Publication Step One ##
## 4. Phase Two ##
## 5. Main Handler ##
## ##
###############################################################################
######################################################################
######################################################################
=pod
=head1 NAME
lonpublisher - LON-CAPA publishing handler
=head1 SYNOPSIS
B is used by B inside B. This is the
invocation by F:
PerlAccessHandler Apache::lonacc
SetHandler perl-script
PerlHandler Apache::lonpublisher
ErrorDocument 403 /adm/login
ErrorDocument 404 /adm/notfound.html
ErrorDocument 406 /adm/unauthorized.html
ErrorDocument 500 /adm/errorhandler
=head1 OVERVIEW
Authors can only write-access the C space.
They can copy resources into the resource area through the
publication step, and move them back through a recover step.
Authors do not have direct write-access to their resource space.
During the publication step, several events will be
triggered. Metadata is gathered, where a wizard manages default
entries on a hierarchical per-directory base: The wizard imports the
metadata (including access privileges and royalty information) from
the most recent published resource in the current directory, and if
that is not available, from the next directory above, etc. The Network
keeps all previous versions of a resource and makes them available by
an explicit version number, which is inserted between the file name
and extension, for example C, while the most recent
version does not carry a version number (C). Servers
subscribing to a changed resource are notified that a new version is
available.
=head1 DESCRIPTION
B takes the proper steps to add resources to the LON-CAPA
digital library. This includes updating the metadata table in the
LON-CAPA database.
B is many things to many people.
This module publishes a file. This involves gathering metadata,
versioning the file, copying file from construction space to
publication space, and copying metadata from construction space
to publication space.
=head2 SUBROUTINES
Many of the undocumented subroutines implement various magical
parsing shortcuts.
=cut
######################################################################
######################################################################
package Apache::lonpublisher;
# ------------------------------------------------- modules used by this module
use strict;
use Apache::File;
use File::Copy;
use Apache::Constants qw(:common :http :methods);
use HTML::LCParser;
use HTML::Entities;
use Encode::Encoder;
use Apache::lonxml;
use DBI;
use Apache::lonnet;
use Apache::loncommon();
use Apache::lonhtmlcommon;
use Apache::lonmysql;
use Apache::lonlocal;
use Apache::loncfile;
use LONCAPA::lonmetadata;
use Apache::lonmsg;
use vars qw(%metadatafields %metadatakeys);
use LONCAPA qw(:DEFAULT :match);
my %addid;
my %nokey;
my $docroot;
my $cuname;
my $cudom;
my $registered_cleanup;
my $modified_urls;
my $lock;
=pod
=over 4
=item B
Evaluates a string that contains metadata. This subroutine
stores values inside I<%metadatafields> and I<%metadatakeys>.
The hash key is a I<$unikey> corresponding to a unique id
that is descriptive of the parser location inside the XML tree.
Parameters:
=over 4
=item I<$metastring>
A string that contains metadata.
=back
Returns:
nothing
=cut
#########################################
#########################################
#
# Modifies global %metadatafields %metadatakeys
#
sub metaeval {
my ($metastring,$prefix)=@_;
my $parser=HTML::LCParser->new(\$metastring);
my $token;
while ($token=$parser->get_token) {
if ($token->[0] eq 'S') {
my $entry=$token->[1];
my $unikey=$entry;
next if ($entry =~ m/^(?:parameter|stores)_/);
if (defined($token->[2]->{'package'})) {
$unikey.="\0package\0".$token->[2]->{'package'};
}
if (defined($token->[2]->{'part'})) {
$unikey.="\0".$token->[2]->{'part'};
}
if (defined($token->[2]->{'id'})) {
$unikey.="\0".$token->[2]->{'id'};
}
if (defined($token->[2]->{'name'})) {
$unikey.="\0".$token->[2]->{'name'};
}
foreach my $item (@{$token->[3]}) {
$metadatafields{$unikey.'.'.$item}=$token->[2]->{$item};
if ($metadatakeys{$unikey}) {
$metadatakeys{$unikey}.=','.$item;
} else {
$metadatakeys{$unikey}=$item;
}
}
my $newentry=$parser->get_text('/'.$entry);
if (($entry eq 'customdistributionfile') ||
($entry eq 'sourcerights')) {
$newentry=~s/^\s*//;
if ($newentry !~m|^/res|) { $newentry=$prefix.$newentry; }
}
# actually store
if ( $entry eq 'rule' && exists($metadatafields{$unikey})) {
$metadatafields{$unikey}.=','.$newentry;
} else {
$metadatafields{$unikey}=$newentry;
}
}
}
}
#########################################
#########################################
=pod
=item B
Read a metadata file
Parameters:
=over
=item I<$logfile>
File output stream to output errors and warnings to.
=item I<$fn>
File name (including path).
=back
Returns:
=over 4
=item Scalar string (if successful)
XHTML text that indicates successful reading of the metadata.
=back
=cut
#########################################
#########################################
sub metaread {
my ($logfile,$fn,$prefix)=@_;
unless (-e $fn) {
print($logfile 'No file '.$fn."\n");
return '
';
}
#########################################
#########################################
sub coursedependencies {
my $url=&Apache::lonnet::declutter(shift);
$url=~s/\.meta$//;
my ($adomain,$aauthor)=($url=~ m{^($match_domain)/($match_username)/});
my $regexp=quotemeta($url);
$regexp='___'.$regexp.'___course';
my %evaldata=&Apache::lonnet::dump('nohist_resevaldata',$adomain,
$aauthor,$regexp);
my %courses=();
foreach my $item (keys(%evaldata)) {
if ($item=~/^([a-zA-Z0-9]+_[a-zA-Z0-9]+)___.+___course$/) {
$courses{$1}=1;
}
}
return %courses;
}
#########################################
#########################################
=pod
=item Form-field-generating subroutines.
For input parameters, these subroutines take in values
such as I<$name>, I<$value> and other form field metadata.
The output (scalar string that is returned) is an XHTML
string which presents the form field (foreseeably inside
tags).
=over 4
=item B
=item B
=item B
=back
=cut
#########################################
#########################################
sub textfield {
my ($title,$name,$value,$noline)=@_;
$value=~s/^\s+//gs;
$value=~s/\s+$//gs;
$value=~s/\s+/ /gs;
$title=&mt($title);
$env{'form.'.$name}=$value;
return "\n".&Apache::lonhtmlcommon::row_title($title)
.''
.&Apache::lonhtmlcommon::row_closure($noline);
}
sub text_with_browse_field {
my ($title,$name,$value,$restriction,$noline)=@_;
$value=~s/^\s+//gs;
$value=~s/\s+$//gs;
$value=~s/\s+/ /gs;
$title=&mt($title);
$env{'form.'.$name}=$value;
return "\n".&Apache::lonhtmlcommon::row_title($title)
.''
.' '
.''
.&mt('Select')
.' '
.''
.&mt('Search')
.''
.&Apache::lonhtmlcommon::row_closure($noline);
}
sub hiddenfield {
my ($name,$value)=@_;
$env{'form.'.$name}=$value;
return "\n".'';
}
sub checkbox {
my ($name,$text)=@_;
return "\n ";
}
sub selectbox {
my ($title,$name,$value,$functionref,@idlist)=@_;
$title=&mt($title);
$value=(split(/\s*,\s*/,$value))[-1];
if (defined($value)) {
$env{'form.'.$name}=$value;
} else {
$env{'form.'.$name}=$idlist[0];
}
my $selout="\n".&Apache::lonhtmlcommon::row_title($title)
.''.&Apache::lonhtmlcommon::row_closure();
return $selout;
}
sub select_level_form {
my ($value,$name)=@_;
$env{'form.'.$name}=$value;
if (!defined($value)) { $env{'form.'.$name}=0; }
return &Apache::loncommon::select_level_form($value,$name);
}
#########################################
#########################################
=pod
=item B
Fix up a url? First step of publication
=cut
#########################################
#########################################
sub urlfixup {
my ($url,$target)=@_;
unless ($url) { return ''; }
#javascript code needs no fixing
if ($url =~ /^javascript:/i) { return $url; }
if ($url =~ /^mailto:/i) { return $url; }
#internal document links need no fixing
if ($url =~ /^\#/) { return $url; }
my ($host)=($url=~m{(?:(?:http|https|ftp)://)*([^/]+)});
my @lonids = &Apache::lonnet::machine_ids($host);
if (@lonids) {
$url=~s{^(?:http|https|ftp)://}{};
$url=~s/^\Q$host\E//;
}
if ($url=~m{^(?:http|https|ftp)://}) { return $url; }
$url=~s{\Q~$cuname\E}{res/$cudom/$cuname};
return $url;
}
#########################################
#########################################
=pod
=item B
Currently undocumented.
=cut
#########################################
#########################################
sub absoluteurl {
my ($url,$target)=@_;
unless ($url) { return ''; }
if ($target) {
$target=~s/\/[^\/]+$//;
$url=&Apache::lonnet::hreflocation($target,$url);
}
return $url;
}
#########################################
#########################################
=pod
=item B
Currently undocumented
=cut
#########################################
#########################################
sub set_allow {
my ($allow,$logfile,$target,$tag,$oldurl,$type)=@_;
my $newurl=&urlfixup($oldurl,$target);
my $return_url=$oldurl;
print $logfile 'GUYURL: '.$tag.':'.$oldurl.' - '.$newurl."\n";
if ($newurl ne $oldurl) {
$return_url=$newurl;
print $logfile 'URL: '.$tag.':'.$oldurl.' - '.$newurl."\n";
}
if (($newurl !~ /^javascript:/i) &&
($newurl !~ /^mailto:/i) &&
($newurl !~ /^(?:http|https|ftp):/i) &&
($newurl !~ /^\#/)) {
if (($type eq 'src') || ($type eq 'href')) {
if ($newurl =~ /^([^?]+)\?[^?]*$/) {
$newurl = $1;
}
}
$$allow{&absoluteurl($newurl,$target)}=1;
}
return $return_url;
}
#########################################
#########################################
=pod
=item B
Currently undocumented
=cut
#########################################
#########################################
sub get_subscribed_hosts {
my ($target)=@_;
my @subscribed;
my $filename;
$target=~/(.*)\/([^\/]+)$/;
my $srcf=$2;
opendir(DIR,$1);
# cycle through listed files, subscriptions used to exist
# as "filename.lonid"
while ($filename=readdir(DIR)) {
if ($filename=~/\Q$srcf\E\.($match_lonid)$/) {
my $subhost=$1;
if (($subhost ne 'meta'
&& $subhost ne 'subscription'
&& $subhost ne 'meta.subscription'
&& $subhost ne 'tmp') &&
($subhost ne $Apache::lonnet::perlvar{'lonHostID'})) {
push(@subscribed,$subhost);
}
}
}
closedir(DIR);
my $sh;
if ( $sh=Apache::File->new("$target.subscription") ) {
while (my $subline=<$sh>) {
if ($subline =~ /^($match_lonid):/) {
if ($1 ne $Apache::lonnet::perlvar{'lonHostID'}) {
push(@subscribed,$1);
}
}
}
}
return @subscribed;
}
#########################################
#########################################
=pod
=item B
Currently undocumented
=cut
#########################################
#########################################
sub get_max_ids_indices {
my ($content)=@_;
my $maxindex=10;
my $maxid=10;
my $needsfixup=0;
my $duplicateids=0;
my %allids;
my %duplicatedids;
my $parser=HTML::LCParser->new($content);
$parser->xml_mode(1);
my $token;
while ($token=$parser->get_token) {
if ($token->[0] eq 'S') {
my $counter;
if ($counter=$addid{$token->[1]}) {
if ($counter eq 'id') {
if (defined($token->[2]->{'id'}) &&
$token->[2]->{'id'} !~ /^\s*$/) {
$maxid=($token->[2]->{'id'}>$maxid)?$token->[2]->{'id'}:$maxid;
if (exists($allids{$token->[2]->{'id'}})) {
$duplicateids=1;
$duplicatedids{$token->[2]->{'id'}}=1;
} else {
$allids{$token->[2]->{'id'}}=1;
}
} else {
$needsfixup=1;
}
} else {
if (defined($token->[2]->{'index'}) &&
$token->[2]->{'index'} !~ /^\s*$/) {
$maxindex=($token->[2]->{'index'}>$maxindex)?$token->[2]->{'index'}:$maxindex;
} else {
$needsfixup=1;
}
}
}
}
}
return ($needsfixup,$maxid,$maxindex,$duplicateids,
(keys(%duplicatedids)));
}
#########################################
#########################################
=pod
=item B
Currently undocumented
=cut
#########################################
#########################################
sub get_all_text_unbalanced {
#there is a copy of this in lonxml.pm
my($tag,$pars)= @_;
my $token;
my $result='';
$tag='<'.$tag.'>';
while ($token = $$pars[-1]->get_token) {
if (($token->[0] eq 'T')||($token->[0] eq 'C')||($token->[0] eq 'D')) {
$result.=$token->[1];
} elsif ($token->[0] eq 'PI') {
$result.=$token->[2];
} elsif ($token->[0] eq 'S') {
$result.=$token->[4];
} elsif ($token->[0] eq 'E') {
$result.=$token->[2];
}
if ($result =~ /\Q$tag\E/s) {
($result,my $redo)=$result =~ /(.*)\Q$tag\E(.*)/is;
#&Apache::lonnet::logthis('Got a winner with leftovers ::'.$2);
#&Apache::lonnet::logthis('Result is :'.$1);
$redo=$tag.$redo;
push (@$pars,HTML::LCParser->new(\$redo));
$$pars[-1]->xml_mode('1');
last;
}
}
return $result
}
#########################################
#########################################
=pod
=item B
Currently undocumented
=cut
#########################################
#########################################
#Arguably this should all be done as a lonnet::ssi instead
sub fix_ids_and_indices {
my ($logfile,$source,$target)=@_;
my %allow;
my $content;
{
my $org=Apache::File->new($source);
$content=join('',<$org>);
}
my ($needsfixup,$maxid,$maxindex,$duplicateids,@duplicatedids)=
&get_max_ids_indices(\$content);
print $logfile ("Got $needsfixup,$maxid,$maxindex,$duplicateids--".
join(', ',@duplicatedids));
if ($duplicateids) {
print $logfile "Duplicate ID(s) exist, ".join(', ',@duplicatedids)."\n";
my $outstring=''.&mt('Unable to publish file, it contains duplicated ID(s), ID(s) need to be unique. The duplicated ID(s) are').': '.join(', ',@duplicatedids).'';
return ($outstring,1);
}
if ($needsfixup) {
print $logfile "Needs ID and/or index fixup\n".
"Max ID : $maxid (min 10)\n".
"Max Index: $maxindex (min 10)\n";
}
my $outstring='';
my $responsecounter=1;
my @parser;
$parser[0]=HTML::LCParser->new(\$content);
$parser[-1]->xml_mode(1);
my $token;
while (@parser) {
while ($token=$parser[-1]->get_token) {
if ($token->[0] eq 'S') {
my $counter;
my $tag=$token->[1];
my $lctag=lc($tag);
if ($lctag eq 'allow') {
$allow{$token->[2]->{'src'}}=1;
next;
}
if ($lctag eq 'base') { next; }
if (($lctag eq 'part') || ($lctag eq 'problem')) {
$responsecounter=0;
}
if ($lctag=~/response$/) { $responsecounter++; }
if ($lctag eq 'import') { $responsecounter++; }
my %parms=%{$token->[2]};
$counter=$addid{$tag};
if (!$counter) { $counter=$addid{$lctag}; }
if ($counter) {
if ($counter eq 'id') {
unless (defined($parms{'id'}) &&
$parms{'id'}!~/^\s*$/) {
$maxid++;
$parms{'id'}=$maxid;
print $logfile 'ID(new) : '.$tag.':'.$maxid."\n";
} else {
print $logfile 'ID(kept): '.$tag.':'.$parms{'id'}."\n";
}
} elsif ($counter eq 'index') {
unless (defined($parms{'index'}) &&
$parms{'index'}!~/^\s*$/) {
$maxindex++;
$parms{'index'}=$maxindex;
print $logfile 'Index: '.$tag.':'.$maxindex."\n";
}
}
}
unless ($parms{'type'} eq 'zombie') {
foreach my $type ('src','href','background','bgimg') {
foreach my $key (keys(%parms)) {
if ($key =~ /^$type$/i) {
next if (($lctag eq 'img') && ($type eq 'src') &&
($parms{$key} =~ m{^data\:image/gif;base64,}));
$parms{$key}=&set_allow(\%allow,$logfile,
$target,$tag,
$parms{$key},$type);
}
}
}
}
# probably a image type
'
.&Apache::loncommon::start_data_table();
my $cols_per_row = 10;
my $colcount=0;
my $wordcount=0;
my $numkeywords = scalar(keys(%keywords));
foreach my $word (sort(keys(%keywords))) {
if ($colcount == 0) {
$keywordout .= &Apache::loncommon::start_data_table_row();
}
$colcount++;
$wordcount++;
if (($wordcount == $numkeywords) && ($colcount < $cols_per_row)) {
my $colspan = 1+$cols_per_row-$colcount;
$keywordout .= '
';
} else {
$keywordout .= '
';
}
$keywordout.='
';
if ($colcount == $cols_per_row) {
$keywordout.=&Apache::loncommon::end_data_table_row();
$colcount=0;
}
}
if ($colcount > 0) {
$keywordout .= &Apache::loncommon::end_data_table_row();
}
$env{'form.keywords'}=~s/\,$//;
$keywordout.=&Apache::loncommon::end_data_table_row()
.&Apache::loncommon::end_data_table()
.&Apache::lonhtmlcommon::row_closure();
$intr_scrout.=$keywordout;
$intr_scrout.=&textfield('Additional Keywords','addkey','');
$intr_scrout.=&textfield('Notes','notes',$metadatafields{'notes'});
$intr_scrout.="\n".&Apache::lonhtmlcommon::row_title(&mt('Abstract'))
.''
.&Apache::lonhtmlcommon::row_closure();
$source=~/\.(\w+)$/;
$intr_scrout.="\n".&Apache::lonhtmlcommon::row_title(&mt('Grade Levels'))
.&mt('Lowest Grade Level:').' '
.&select_level_form($metadatafields{'lowestgradelevel'},'lowestgradelevel')
# .&Apache::lonhtmlcommon::row_closure();
# $intr_scrout.="\n".&Apache::lonhtmlcommon::row_title(&mt('Highest Grade Level'))
.' '.&mt('Highest Grade Level:').' '
.&select_level_form($metadatafields{'highestgradelevel'},'highestgradelevel')
.&Apache::lonhtmlcommon::row_closure();
$intr_scrout.=&textfield('Standards','standards',$metadatafields{'standards'});
$intr_scrout.=&hiddenfield('mime',$1);
my $defaultlanguage=$metadatafields{'language'};
$defaultlanguage =~ s/\s*notset\s*//g;
$defaultlanguage =~ s/^,\s*//g;
$defaultlanguage =~ s/,\s*$//g;
$intr_scrout.=&selectbox('Language','language',
$defaultlanguage,
\&Apache::loncommon::languagedescription,
(&Apache::loncommon::languageids),
);
unless ($metadatafields{'creationdate'}) {
$metadatafields{'creationdate'}=time;
}
$intr_scrout.=&hiddenfield('creationdate',
&Apache::lonmysql::unsqltime($metadatafields{'creationdate'}));
$intr_scrout.=&hiddenfield('lastrevisiondate',time);
my $pubowner_last;
if ($style eq 'prv') {
$pubowner_last = 1;
}
$intr_scrout.=&textfield('Publisher/Owner','owner',
$metadatafields{'owner'},$pubowner_last);
# ---------------------------------------------- Retrofix for unused copyright
if ($metadatafields{'copyright'} eq 'free') {
$metadatafields{'copyright'}='default';
$metadatafields{'sourceavail'}='open';
}
if ($metadatafields{'copyright'} eq 'priv') {
$metadatafields{'copyright'}='domain';
}
# ------------------------------------------------ Dial in reasonable defaults
my $defaultoption=$metadatafields{'copyright'};
unless ($defaultoption) { $defaultoption='default'; }
my $defaultsourceoption=$metadatafields{'sourceavail'};
unless ($defaultsourceoption) { $defaultsourceoption='closed'; }
unless ($style eq 'prv') {
# -------------------------------------------------- Correct copyright for rat.
if ($style eq 'rat') {
# -------------------------------------- Retrofix for non-applicable copyright
if ($metadatafields{'copyright'} eq 'public') {
delete $metadatafields{'copyright'};
$defaultoption='default';
}
$intr_scrout.=&selectbox('Copyright/Distribution','copyright',
$defaultoption,
\&Apache::loncommon::copyrightdescription,
(grep !/^(public|priv)$/,(&Apache::loncommon::copyrightids)));
} else {
$intr_scrout.=&selectbox('Copyright/Distribution','copyright',
$defaultoption,
\&Apache::loncommon::copyrightdescription,
(grep !/^priv$/,(&Apache::loncommon::copyrightids)));
}
my $copyright_help =
&Apache::loncommon::help_open_topic('Publishing_Copyright');
my $replace=&mt('Copyright/Distribution:');
$intr_scrout =~ s/$replace/$replace.' '.$copyright_help/ge;
$intr_scrout.=&text_with_browse_field('Custom Distribution File','customdistributionfile',$metadatafields{'customdistributionfile'},'rights');
$intr_scrout.=&selectbox('Source Distribution','sourceavail',
$defaultsourceoption,
\&Apache::loncommon::source_copyrightdescription,
(&Apache::loncommon::source_copyrightids));
# $intr_scrout.=&text_with_browse_field('Source Custom Distribution File','sourcerights',$metadatafields{'sourcerights'},'rights');
my $uctitle=&mt('Obsolete');
my $obsolete_checked=($metadatafields{'obsolete'})?' checked="checked"':'';
$intr_scrout.="\n".&Apache::lonhtmlcommon::row_title($uctitle)
.''
.&Apache::lonhtmlcommon::row_closure(1);
$intr_scrout.=&text_with_browse_field('Suggested Replacement for Obsolete File',
'obsoletereplacement',
$metadatafields{'obsoletereplacement'},'',1);
} else {
$intr_scrout.=&hiddenfield('copyright','private');
}
} else {
$intr_scrout.=
&hiddenfield('title',$metadatafields{'title'}).
&hiddenfield('author',$metadatafields{'author'}).
&hiddenfield('subject',$metadatafields{'subject'}).
&hiddenfield('keywords',$metadatafields{'keywords'}).
&hiddenfield('abstract',$metadatafields{'abstract'}).
&hiddenfield('notes',$metadatafields{'notes'}).
&hiddenfield('mime',$metadatafields{'mime'}).
&hiddenfield('creationdate',$metadatafields{'creationdate'}).
&hiddenfield('lastrevisiondate',time).
&hiddenfield('owner',$metadatafields{'owner'}).
&hiddenfield('lowestgradelevel',$metadatafields{'lowestgradelevel'}).
&hiddenfield('standards',$metadatafields{'standards'}).
&hiddenfield('highestgradelevel',$metadatafields{'highestgradelevel'}).
&hiddenfield('language',$metadatafields{'language'}).
&hiddenfield('copyright',$metadatafields{'copyright'}).
&hiddenfield('sourceavail',$metadatafields{'sourceavail'}).
&hiddenfield('customdistributionfile',$metadatafields{'customdistributionfile'}).
&hiddenfield('obsolete',1).
&text_with_browse_field('Suggested Replacement for Obsolete File',
'obsoletereplacement',
$metadatafields{'obsoletereplacement'},'',1);
}
if (!$batch) {
$scrout.=$intr_scrout
.&Apache::lonhtmlcommon::end_pick_box()
.''
.'';
}
return($scrout,0);
}
#########################################
#########################################
=pod
=item B
Render second interface showing status of publication steps.
This is publication step two.
Parameters:
=over 4
=item I<$source>
=item I<$target>
=item I<$style>
=item I<$distarget>
=back
Returns:
=over 4
=item integer
0: fail
1: success
=back
=cut
#'stupid emacs
#########################################
#########################################
sub phasetwo {
my ($r,$source,$target,$style,$distarget,$batch)=@_;
$source=~s/\/+/\//g;
$target=~s/\/+/\//g;
#
# Unless trying to get rid of something, check name validity
#
unless ($env{'form.obsolete'}) {
if ($target=~/(\_\_\_|\&\&\&|\:\:\:)/) {
$r->print(''.
&mt('Unsupported character combination [_1] in filename, FAIL.',"'.$1.'").
'');
return 0;
}
unless ($target=~/\.(\w+)$/) {
$r->print(''.&mt('No valid extension found in filename, FAIL').'');
return 0;
}
if ($target=~/\.(\d+)\.(\w+)$/) {
$r->print(''.&mt('Filename of resource contains internal version number. Cannot publish such resources, FAIL').'');
return 0;
}
}
#
# End name check
#
$distarget=~s/\/+/\//g;
my $logfile;
unless ($logfile=Apache::File->new('>>'.$source.'.log')) {
$r->print(
''.
&mt('No write permission to user directory, FAIL').'');
return 0;
}
if ($source =~ /\.rights$/) {
$r->print('
'.&mt('Warning: It can take up to 1 hour for rights changes to fully propagate.').'
');
}
print $logfile
"\n================= Publish ".localtime()." Phase Two ================\n".$env{'user.name'}.':'.$env{'user.domain'}."\n";
%metadatafields=();
%metadatakeys=();
&metaeval(&unescape($env{'form.allmeta'}));
$metadatafields{'title'}=$env{'form.title'};
$metadatafields{'author'}=$env{'form.author'};
$metadatafields{'subject'}=$env{'form.subject'};
$metadatafields{'notes'}=$env{'form.notes'};
$metadatafields{'abstract'}=$env{'form.abstract'};
$metadatafields{'mime'}=$env{'form.mime'};
$metadatafields{'language'}=$env{'form.language'};
$metadatafields{'creationdate'}=$env{'form.creationdate'};
$metadatafields{'lastrevisiondate'}=$env{'form.lastrevisiondate'};
$metadatafields{'owner'}=$env{'form.owner'};
$metadatafields{'copyright'}=$env{'form.copyright'};
$metadatafields{'standards'}=$env{'form.standards'};
$metadatafields{'lowestgradelevel'}=$env{'form.lowestgradelevel'};
$metadatafields{'highestgradelevel'}=$env{'form.highestgradelevel'};
$metadatafields{'customdistributionfile'}=
$env{'form.customdistributionfile'};
$metadatafields{'sourceavail'}=$env{'form.sourceavail'};
$metadatafields{'obsolete'}=$env{'form.obsolete'};
$metadatafields{'obsoletereplacement'}=
$env{'form.obsoletereplacement'};
$metadatafields{'dependencies'}=$env{'form.dependencies'};
$metadatafields{'modifyinguser'}=$env{'user.name'}.':'.
$env{'user.domain'};
$metadatafields{'authorspace'}=$cuname.':'.$cudom;
$metadatafields{'domain'}=$cudom;
my $allkeywords=$env{'form.addkey'};
if (exists($env{'form.keywords'})) {
if (ref($env{'form.keywords'})) {
$allkeywords .= ','.join(',',@{$env{'form.keywords'}});
} else {
$allkeywords .= ','.$env{'form.keywords'};
}
}
$allkeywords=~s/[\"\']//g;
$allkeywords=~s/\s*[\;\,]\s*/\,/g;
$allkeywords=~s/\s+/ /g;
$allkeywords=~s/^[ \,]//;
$allkeywords=~s/[ \,]$//;
$metadatafields{'keywords'}=$allkeywords;
# check if custom distribution file is specified
if ($metadatafields{'copyright'} eq 'custom') {
my $file=$metadatafields{'customdistributionfile'};
unless ($file=~/\.rights$/) {
$r->print(
''.&mt('No valid custom distribution rights file specified, FAIL').
'');
return 0;
}
}
{
print $logfile "\nWrite metadata file for ".$source;
my $mfh;
unless ($mfh=Apache::File->new('>'.$source.'.meta')) {
$r->print(
''.&mt('Could not write metadata, FAIL').
'');
return 0;
}
foreach my $field (sort(keys(%metadatafields))) {
unless ($field=~/\./) {
my $unikey=$field;
$unikey=~/^([A-Za-z]+)/;
my $tag=$1;
$tag=~tr/A-Z/a-z/;
print $mfh "\n\<$tag";
foreach my $item (split(/\,/,$metadatakeys{$unikey})) {
my $value=$metadatafields{$unikey.'.'.$item};
$value=~s/\"/\'\'/g;
print $mfh ' '.$item.'="'.$value.'"';
}
print $mfh '>'.
&HTML::Entities::encode($metadatafields{$unikey},'<>&"')
.''.$tag.'>';
}
}
$r->print('
'.&mt('Wrote Metadata').'
');
print $logfile "\nWrote metadata";
}
# -------------------------------- Synchronize entry with SQL metadata database
$metadatafields{'url'} = $distarget;
$metadatafields{'version'} = 'current';
my ($error,$success) = &store_metadata(%metadatafields);
if ($success) {
$r->print('
');
print $logfile "\nRemoving error messages: $delresult";
# ----------------------------------------------------------- Copy old versions
if (-e $target) {
my $filename;
my $maxversion=0;
$target=~/(.*)\/([^\/]+)\.(\w+)$/;
my $srcf=$2;
my $srct=$3;
my $srcd=$1;
my $docroot = $Apache::lonnet::perlvar{'lonDocRoot'};
unless ($srcd=~/^\Q$docroot\E\/res/) {
print $logfile "\nPANIC: Target dir is ".$srcd;
$r->print(
"".&mt('Invalid target directory, FAIL')."");
return 0;
}
opendir(DIR,$srcd);
while ($filename=readdir(DIR)) {
if (-l $srcd.'/'.$filename) {
unlink($srcd.'/'.$filename);
unlink($srcd.'/'.$filename.'.meta');
} else {
if ($filename=~/^\Q$srcf\E\.(\d+)\.\Q$srct\E$/) {
$maxversion=($1>$maxversion)?$1:$maxversion;
}
}
}
closedir(DIR);
$maxversion++;
$r->print('
'.&mt('Creating old version [_1]',$maxversion).'
');
print $logfile "\nCreating old version ".$maxversion."\n";
my $copyfile=$srcd.'/'.$srcf.'.'.$maxversion.'.'.$srct;
if (copy($target,$copyfile)) {
print $logfile "Copied old target to ".$copyfile."\n";
$r->print(&Apache::lonhtmlcommon::confirm_success(&mt('Copied old target file')));
} else {
print $logfile "Unable to write ".$copyfile.':'.$!."\n";
$r->print(&Apache::lonhtmlcommon::confirm_success(&mt('Failed to copy old target').", $!",1));
return 0;
}
# --------------------------------------------------------------- Copy Metadata
$copyfile=$copyfile.'.meta';
if (copy($target.'.meta',$copyfile)) {
print $logfile "Copied old target metadata to ".$copyfile."\n";
$r->print(&Apache::lonhtmlcommon::confirm_success(&mt('Copied old metadata')));
} else {
print $logfile "Unable to write metadata ".$copyfile.':'.$!."\n";
if (-e $target.'.meta') {
$r->print(&Apache::lonhtmlcommon::confirm_success(
&mt('Failed to write old metadata copy').", $!",1));
return 0;
}
}
} else {
$r->print('
'.&mt('Initial version').'
');
print $logfile "\nInitial version";
}
# ---------------------------------------------------------------- Write Source
my $copyfile=$target;
my @parts=split(/\//,$copyfile);
my $path="/$parts[1]/$parts[2]/$parts[3]/$parts[4]";
my $count;
for ($count=5;$count<$#parts;$count++) {
$path.="/$parts[$count]";
if ((-e $path)!=1) {
print $logfile "\nCreating directory ".$path;
mkdir($path,0777);
$r->print('
'
);
}
}
if (copy($source,$copyfile)) {
print $logfile "\nCopied original source to ".$copyfile."\n";
$r->print(&Apache::lonhtmlcommon::confirm_success(&mt('Copied source file')));
} else {
print $logfile "\nUnable to write ".$copyfile.':'.$!."\n";
$r->print(&Apache::lonhtmlcommon::confirm_success(
&mt('Failed to copy source').", $!",1));
return 0;
}
# ---------------------------------------------- Delete local tmp-preview files
unlink($copyfile.'.tmp');
# --------------------------------------------------------------- Copy Metadata
$copyfile=$copyfile.'.meta';
if (copy($source.'.meta',$copyfile)) {
print $logfile "\nCopied original metadata to ".$copyfile."\n";
$r->print(&Apache::lonhtmlcommon::confirm_success(&mt('Copied metadata')));
} else {
print $logfile "\nUnable to write metadata ".$copyfile.':'.$!."\n";
$r->print(&Apache::lonhtmlcommon::confirm_success(
&mt('Failed to write metadata copy').", $!",1));
return 0;
}
$r->rflush;
# ------------------------------------------------------------- Trigger updates
push(@{$modified_urls},[$target,$source]);
unless ($registered_cleanup) {
my $handlers = $r->get_handlers('PerlCleanupHandler');
$r->set_handlers('PerlCleanupHandler' => [\¬ify,@{$handlers}]);
$registered_cleanup=1;
}
# ---------------------------------------------------------- Clear local caches
my $thisdistarget=$target;
$thisdistarget=~s/^\Q$docroot\E//;
&Apache::lonnet::devalidate_cache_new('resversion',$target);
&Apache::lonnet::devalidate_cache_new('meta',
&Apache::lonnet::declutter($thisdistarget));
# ------------------------------------------------------------- Everything done
$logfile->close();
$r->print('
'.&mt('Done').'
');
# ------------------------------------------------ Provide link to new resource
unless ($batch) {
my $thissrc=&Apache::loncfile::url($source);
my $thissrcdir=$thissrc;
$thissrcdir=~s/\/[^\/]+$/\//;
$r->print(
&Apache::lonhtmlcommon::actionbox([
''.
&mt('View Published Version').
'',
''.
&mt('Back to Source').
'',
''.
&mt('Back to Source Directory').
''])
);
}
return 1;
}
# =============================================================== Notifications
sub notify {
# --------------------------------------------------- Send update notifications
foreach my $targetsource (@{$modified_urls}){
my ($target,$source)=@{$targetsource};
my $logfile=Apache::File->new('>>'.$source.'.log');
print $logfile "\nCleanup phase: Notifications\n";
my @subscribed=&get_subscribed_hosts($target);
foreach my $subhost (@subscribed) {
print $logfile "\nNotifying host ".$subhost.':';
my $reply=&Apache::lonnet::critical('update:'.$target,$subhost);
print $logfile $reply;
}
# ---------------------------------------- Send update notifications, meta only
my @subscribedmeta=&get_subscribed_hosts("$target.meta");
foreach my $subhost (@subscribedmeta) {
print $logfile "\nNotifying host for metadata only ".$subhost.':';
my $reply=&Apache::lonnet::critical('update:'.$target.'.meta',
$subhost);
print $logfile $reply;
}
# --------------------------------------------------- Notify subscribed courses
my %courses=&coursedependencies($target);
my $now=time;
foreach my $course (keys(%courses)) {
print $logfile "\nNotifying course ".$course.':';
my ($cdom,$cname)=split(/\_/,$course);
my $reply=&Apache::lonnet::cput
('versionupdate',{$target => $now},$cdom,$cname);
print $logfile $reply;
}
print $logfile "\n============ Done ============\n";
$logfile->close();
}
if ($lock) { &Apache::lonnet::remove_lock($lock); }
return OK;
}
#########################################
sub batchpublish {
my ($r,$srcfile,$targetfile)=@_;
#publication pollutes %env with form.* values
my %oldenv=%env;
$srcfile=~s/\/+/\//g;
$targetfile=~s/\/+/\//g;
$srcfile=~s/\/+/\//g;
my $docroot=$r->dir_config('lonDocRoot');
my $thisdistarget=$targetfile;
$thisdistarget=~s/^\Q$docroot\E//;
%metadatafields=();
%metadatakeys=();
$srcfile=~/\.(\w+)$/;
my $thistype=$1;
my $thisembstyle=&Apache::loncommon::fileembstyle($thistype);
$r->print('
'
);
# phase one takes
# my ($source,$target,$style,$batch)=@_;
my ($outstring,$error)=&publish($srcfile,$targetfile,$thisembstyle,1);
$r->print('
'.$outstring.'
');
# phase two takes
# my ($source,$target,$style,$distarget,batch)=@_;
# $env{'form.allmeta'},$env{'form.title'},$env{'form.author'},...
if (!$error) {
$r->print('
');
} else {
return "".
&mt('Failed to copy source').", $!, ".&mt('FAIL')."";
}
# --------------------------------------------------- Send update notifications
my @subscribed=&get_subscribed_hosts($target);
foreach my $subhost (@subscribed) {
$r->print('
'.&mt('Notifying host').' '.$subhost.':');$r->rflush;
my $reply=&Apache::lonnet::critical('update:'.$target,$subhost);
$r->print($reply.'
');$r->rflush;
}
# ------------------------------------------------------------------- Link back
$r->print("".&mt('Back to Metadata').'');
$r->print(&Apache::loncommon::end_page());
return OK;
}
#########################################
=pod
=item B
A basic outline of the handler subroutine follows.
=over 4
=item *
Get query string for limited number of parameters.
=item *
Check filename.
=item *
File is there and owned, init lookup tables.
=item *
Start page output.
=item *
Evaluate individual file, and then output information.
=item *
Publishing from $thisfn to $thistarget with $thisembstyle.
=back
=cut
#########################################
#########################################
sub handler {
my $r=shift;
if ($r->header_only) {
&Apache::loncommon::content_type($r,'text/html');
$r->send_http_header;
return OK;
}
# Get query string for limited number of parameters
&Apache::loncommon::get_unprocessed_cgi($ENV{'QUERY_STRING'},
['filename']);
# -------------------------------------- Flag and buffer for registered cleanup
$registered_cleanup=0;
@{$modified_urls}=();
# -------------------------------------------------------------- Check filename
my $fn=&unescape($env{'form.filename'});
($cuname,$cudom)=&Apache::lonnet::constructaccess($fn);
# ----------------------------------------------------- Do we have permissions?
unless (($cuname) && ($cudom)) {
$r->log_reason($env{'user.name'}.' at '.$env{'user.domain'}.
' trying to publish file '.$env{'form.filename'}.
' - not authorized',
$r->filename);
return HTTP_NOT_ACCEPTABLE;
}
# ----------------------------------------------------------------- Get docroot
$docroot=$r->dir_config('lonDocRoot');
# special publication: default.meta file
if ($fn=~/\/default.meta$/) {
return &defaultmetapublish($r,$fn,$cuname,$cudom);
}
$fn=~s/\.meta$//;
# sanity test on the filename
unless ($fn) {
$r->log_reason($cuname.' at '.$cudom.
' trying to publish empty filename', $r->filename);
return HTTP_NOT_FOUND;
}
unless (-e $docroot.$fn) {
$r->log_reason($cuname.' at '.$cudom.
' trying to publish non-existing file '.
$env{'form.filename'}.' ('.$fn.')',
$r->filename);
return HTTP_NOT_FOUND;
}
# -------------------------------- File is there and owned, init lookup tables.
%addid=();
{
my $fh=Apache::File->new($r->dir_config('lonTabDir').'/addid.tab');
while (<$fh>=~/(\w+)\s+(\w+)/) {
$addid{$1}=$2;
}
}
%nokey=();
{
my $fh=Apache::File->new($r->dir_config('lonIncludes').'/un_keyword.tab');
while (<$fh>) {
my $word=$_;
chomp($word);
$nokey{$word}=1;
}
}
# ---------------------------------------------------------- Start page output.
&Apache::loncommon::content_type($r,'text/html');
$r->send_http_header;
# Breadcrumbs
&Apache::lonhtmlcommon::clear_breadcrumbs();
&Apache::lonhtmlcommon::add_breadcrumb({
'text' => 'Authoring Space',
'href' => &Apache::loncommon::authorspace($fn),
});
&Apache::lonhtmlcommon::add_breadcrumb({
'text' => 'Resource Publication',
'href' => '',
});
my $js='';
$r->print(&Apache::loncommon::start_page('Resource Publication',$js)
.&Apache::lonhtmlcommon::breadcrumbs()
.&Apache::loncommon::head_subbox(
&Apache::loncommon::CSTR_pageheader($docroot.$fn))
);
my $thisdisfn=&HTML::Entities::encode($fn,'<>&"');
my $thistarget=$fn;
$thistarget=~s/^\/priv\//\/res\//;
my $thisdistarget=&HTML::Entities::encode($thistarget,'<>&"');
if ($fn=~/\/$/) {
# -------------------------------------------------------- This is a directory
&publishdirectory($r,$docroot.$fn,$thisdisfn);
$r->print(
'
'.
&Apache::lonhtmlcommon::actionbox([
''.&mt('Return to Directory').'']));
} else {
# ---------------------- Evaluate individual file, and then output information.
$fn=~/\.(\w+)$/;
my $thistype=$1;
my $thisembstyle=&Apache::loncommon::fileembstyle($thistype);
if ($thistype eq 'page') { $thisembstyle = 'rat'; }
$r->print('
'
.&mt('Publishing [_1]'
,''.$thisdisfn.'')
.'
'
);
$r->print('
'.&mt('Resource Details').'
');
$r->print(&Apache::lonhtmlcommon::start_pick_box());
$r->print(&Apache::lonhtmlcommon::row_title(&mt('Type'))
.&Apache::loncommon::filedescription($thistype)
.&Apache::lonhtmlcommon::row_closure()
);
$r->print(&Apache::lonhtmlcommon::row_title(&mt('Link to Resource'))
.''
);
$r->print(<
$thisdisfn
ENDCAPTION
$r->print(''
.&Apache::lonhtmlcommon::row_closure()
);
$r->print(&Apache::lonhtmlcommon::row_title(&mt('Target'))
.''.$thisdistarget.''
);
if (($cuname ne $env{'user.name'})||($cudom ne $env{'user.domain'})) {
$r->print(&Apache::lonhtmlcommon::row_closure()
.&Apache::lonhtmlcommon::row_title(&mt('Co-Author'))
.''
.&Apache::loncommon::plainname($cuname,$cudom) .' ('.$cuname.':'.$cudom.')'
.''
);
}
if (&Apache::loncommon::fileembstyle($thistype) eq 'ssi') {
$r->print(&Apache::lonhtmlcommon::row_closure()
.&Apache::lonhtmlcommon::row_title(&mt('Diffs')));
$r->print(<
ENDDIFF
$r->print(&mt('Diffs with Current Version').'');
}
$r->print(&Apache::lonhtmlcommon::row_closure(1)
.&Apache::lonhtmlcommon::end_pick_box()
);
# ---------------------- Publishing from $fn to $thistarget with $thisembstyle.
unless ($env{'form.phase'} eq 'two') {
# ---------------------------------------------------------- Parse for problems
my ($warningcount,$errorcount);
if ($thisembstyle eq 'ssi') {
($warningcount,$errorcount)=&checkonthis($r,$fn);
}
unless ($errorcount) {
my ($outstring,$error)=
&publish($docroot.$fn,$docroot.$thistarget,$thisembstyle);
$r->print($outstring);
} else {
$r->print('
'.
&mt('The document contains errors and cannot be published.').
'