#-----------------------------------------------------------------------------
#
# $Id : File.pm 2.116 2008-09-16 JMG$
#
# Created and maintained by Jean-Marie Gouarne
# Copyright 2008 by Genicorp, S.A. (www.genicorp.com)
#
#-----------------------------------------------------------------------------
package OpenOffice::OODoc::File;
use 5.008_000;
our $VERSION = 2.116;
use Archive::Zip 1.14 qw ( :DEFAULT :CONSTANTS :ERROR_CODES );
use File::Temp;
#-----------------------------------------------------------------------------
# some defaults
our $DEFAULT_OFFICE_FORMAT = 2; # OpenDocument format
our $DEFAULT_COMPRESSION_METHOD = COMPRESSION_DEFLATED;
our $DEFAULT_COMPRESSION_LEVEL = COMPRESSION_LEVEL_BEST_COMPRESSION;
our $DEFAULT_EXPORT_PATH = './';
our $WORKING_DIRECTORY = '.';
our $TEMPLATE_PATH = '';
our $MIMETYPE_BASE = 'application/vnd.sun.xml.';
our %OOTYPE =
(
text => 'writer',
spreadsheet => 'calc',
presentation => 'impress',
drawing => 'draw',
);
our %ODF_SUFFIX =
(
text => 'odt',
spreadsheet => 'ods',
presentation => 'odp',
drawing => 'odg'
);
our %OOO_SUFFIX =
(
text => 'sxw',
spreadsheet => 'sxc',
presentation => 'sxi',
drawing => 'sxd'
);
#-----------------------------------------------------------------------------
# returns the mimetype string according to a document class
sub mime_type
{
my $class = shift;
return undef unless ($class && $OOTYPE{$class});
return $MIMETYPE_BASE . $OOTYPE{$class};
}
#-----------------------------------------------------------------------------
# get/set the path for XML templates
sub templatePath
{
my $newpath = shift;
$TEMPLATE_PATH = $newpath if defined $newpath;
return $TEMPLATE_PATH;
}
#-----------------------------------------------------------------------------
# member storage
sub store_member
{
my $zipfile = shift;
my %opt =
(
compress => 1,
@_
);
unless ($opt{'member'})
{
warn "[" . __PACKAGE__ . "::store_member] " .
"Missing member name\n";
return undef;
}
my $m = undef;
if ($opt{'string'})
{
$m = $zipfile->addString($opt{'string'}, $opt{'member'});
}
elsif ($opt{'file'})
{
$m = $zipfile->addFileOrDirectory($opt{'file'}, $opt{'member'});
}
else
{
warn "[" . __PACKAGE__ . "::store_member] " .
"Missing content to store\n";
return undef;
}
unless ($m)
{
warn "[" . __PACKAGE__ . "::store_member] " .
"Member storage failure\n[" . $opt{'file'} . "]\n";
return undef;
}
unless ($opt{'compress'})
{
$m->desiredCompressionMethod(COMPRESSION_STORED);
}
else
{
$m->desiredCompressionMethod($DEFAULT_COMPRESSION_METHOD);
$m->desiredCompressionLevel($DEFAULT_COMPRESSION_LEVEL);
}
return $m;
}
#-----------------------------------------------------------------------------
# new container creation from template
sub _load_template_file
{
my %opt =
(
template_path => $TEMPLATE_PATH,
@_
);
my $basepath = undef;
if ($opt{'template_path'})
{
$basepath = $opt{'template_path'};
}
else
{
require File::Basename;
$basepath =
File::Basename::dirname
($INC{"OpenOffice/OODoc/File.pm"}) .
'/templates/';
}
$basepath =~ s/\\/\//g;
my $suffix = $opt{'opendocument'} ?
$ODF_SUFFIX{$opt{'class'}} :
$OOO_SUFFIX{$opt{'class'}};
delete $opt{'class'};
my $source_file = $basepath . '/template.' . $suffix;
my $archive = Archive::Zip->new;
if ($archive->read($source_file) != AZ_OK)
{
$archive = undef;
}
return $archive;
}
#-----------------------------------------------------------------------------
# control & conversion of XML component names of the OO file
sub CtrlMemberName
{
my $self = shift;
my $member = shift;
my $m = lc $member;
foreach my $n ('content', 'meta', 'styles', 'settings')
{
if ($m eq $n)
{
$member = $n . '.xml';
last;
}
}
foreach $m ( @{ $self->{'members'} } )
{
return $member if ($member eq $m);
}
return undef;
}
#-----------------------------------------------------------------------------
# check working directory
sub checkWorkingDirectory
{
my $path = shift || $WORKING_DIRECTORY;
if (-d $path)
{
if (-w $path)
{
return 1;
}
else
{
warn "[" . __PACKAGE__ . "] " .
"No write permission in $path\n";
}
}
else
{
warn "[" . __PACKAGE__ . "] " .
"$path is not a directory\n";
}
return undef;
}
#-----------------------------------------------------------------------------
# unique temporary file name generation
sub new_temp_file_name
{
my $self = shift;
return File::Temp::mktemp($self->{'work_dir'} . '/oo_XXXXX');
}
#-----------------------------------------------------------------------------
# temporary data storage
sub store_temp_file
{
my $self = shift;
my $data = shift;
my $tmpfile = $self->new_temp_file_name;
unless (open FH, '>:utf8', $tmpfile)
{
warn "[" . __PACKAGE__ . "::store_temp_file] " .
"Unable to create temporary file $tmpfile\n";
return undef;
}
unless (print FH $data)
{
warn "[" . __PACKAGE__ . "::store_temp_file] " .
"Write error in temporary file $tmpfile\n";
return undef;
}
unless (close FH)
{
warn "[" . __PACKAGE__ . "::store_temp_file] " .
"Unknown error in temporary file $tmpfile\n";
return undef;
}
push @{$self->{'temporary_files'}}, $tmpfile;
return $tmpfile;
}
#-----------------------------------------------------------------------------
# temporary member extraction
sub extract_temp_file
{
my $self = shift;
my $member = shift;
my $m = ref $member ?
$member :
$self->{'archive'}->memberNamed($member);
my $tmpfile = $self->new_temp_file_name;
my $result = $m->extractToFileNamed($tmpfile);
if ($result == AZ_OK)
{
push @{$self->{'temporary_files'}}, $tmpfile;
return $tmpfile;
}
else
{
return undef;
}
}
#-----------------------------------------------------------------------------
# temporary storage cleanup
# returns the number of deleted files and clears the list of temp files
sub remove_temp_files
{
my $self = shift;
my $count = 0;
while (@{$self->{'temporary_files'}})
{
my $tmpfile = shift @{$self->{'temporary_files'}};
my $r = undef;
unless ( -d $tmpfile ) { $r = unlink $tmpfile; }
else { $r = rmdir $tmpfile; }
unless ($r > 0)
{
warn
"[" . __PACKAGE__ . "::remove_temp_files] " .
"Temporary file $tmpfile can't be removed\n";
}
else
{
$count++;
}
}
return $count;
}
#-----------------------------------------------------------------------------
# constructor; requires an existing regular OO file
sub new
{
my $caller = shift;
my $class = ref($caller) || $caller;
my $sourcefile = shift;
my $self =
{
'linked' => [],
'work_dir' =>
$OpenOffice::OODoc::File::WORKING_DIRECTORY,
'template_path' =>
$OpenOffice::OODoc::File::TEMPLATE_PATH,
'temporary_files' => [],
'raw_members' => [],
'to_be_deleted' => [],
@_
};
my $od = lc $self->{'opendocument'};
unless ($od)
{
if ($OpenOffice::OODoc::File::DEFAULT_OFFICE_FORMAT == 2)
{ $self->{'opendocument'} = 1; }
}
elsif (($od eq '1') || ($od eq 'on') || ($od eq 'true'))
{
$self->{'opendocument'} = 1;
}
elsif (($od eq '0') || ($od eq 'off') || ($od eq 'false'))
{
delete $self->{'opendocument'};
}
else
{
warn "[" . __PACKAGE__ . "::new] Wrong 'opendocument' option\n";
return undef;
}
$self->{'source_file'} = $sourcefile;
unless ($sourcefile)
{
warn "[" . __PACKAGE__ . "::new] Missing file name\n";
return undef;
}
unless ($self->{'create'})
{
unless ( -e $sourcefile && -f $sourcefile && -r $sourcefile )
{
warn "[" . __PACKAGE__ . "::new] " .
"File $sourcefile unavailable\n";
return undef;
}
$self->{'archive'} = Archive::Zip->new;
if ($self->{'archive'}->read($self->{'source_file'}) != AZ_OK)
{
delete $self->{'archive'};
warn "[" . __PACKAGE__ . "::new] Read error\n";
return undef;
}
}
else
{
$self->{'archive'} = _load_template_file
(
class => $self->{'create'},
template_path => $self->{'template_path'},
opendocument => $self->{'opendocument'}
);
unless ($self->{'archive'} && ref $self->{'archive'})
{
delete $self->{'archive'};
warn "[" . __PACKAGE__ . "::new] " .
"Bad or missing template\n";
return undef;
}
}
$self->{'members'} = [ $self->{'archive'}->memberNames ];
return bless $self, $class;
}
#-----------------------------------------------------------------------------
# individual zip XML member extraction/uncompression
sub extract
{
my $self = shift;
my $member = $self->CtrlMemberName(shift);
unless ($member)
{
warn "[" . __PACKAGE__ . "::extract] Unknown member\n";
return undef;
}
unless ($self->{'archive'})
{
warn "[" . __PACKAGE__ . "::extract] No archive\n";
return undef;
}
return $self->{'archive'}->contents($member);
}
#-----------------------------------------------------------------------------
# individual zip member raw export (see Archive::Zip::extractMember)
sub raw_export
{
my $self = shift;
unless ($self->{'archive'})
{
warn "[" . __PACKAGE__ . "::raw_export] No archive\n";
return undef;
}
my $source = shift;
my $target = shift;
if (defined $target)
{
unless ($target =~ /\//)
{
$target = $DEFAULT_EXPORT_PATH . $target;
}
unshift @_, $target;
}
unshift @_, $source;
if ($self->{'archive'}->extractMember(@_) == AZ_OK)
{
return $target ? $target : $source;
}
else
{
warn "[" . __PACKAGE__ . "::raw_export] File output error\n";
return undef;
}
}
#-----------------------------------------------------------------------------
# individual zip member raw import
# file to be imported is only registered here; real import by save()
sub raw_import
{
my $self = shift;
my $membername = shift;
my $filename = shift;
$filename = $membername unless $filename;
my %new_member = ('file' => $filename, 'member' => $membername);
push @{$self->{'raw_members'}}, \%new_member;
return %new_member;
}
#-----------------------------------------------------------------------------
# individual zip member removing (real deletion committed by save)
# WARNING: removing a member doesn't automatically update "manifest.xml"
sub raw_delete
{
my $self = shift;
my $member = $self->CtrlMemberName(shift)
or return undef;
my $mbcount = scalar @{$self->{'members'}};
for (my $i = 0 ; $i < $mbcount ; $i++)
{
if ($self->{'members'}[$i] eq $member)
{
splice(@{$self->{'members'}}, $i, 1);
push @{$self->{'to_be_deleted'}}, $member;
return 1;
}
}
return undef;
}
#-----------------------------------------------------------------------------
# archive list
sub getMemberList
{
my $self = shift;
return @{$self->{'members'}};
}
#-----------------------------------------------------------------------------
# connects the current OODoc::File instance to a client OODoc::XPath object
# and extracts the corresponding XML member (to be transparently invoked
# by the constructor of OODoc::XPath when activated with a 'file' parameter)
sub link
{
my $self = shift;
my $ooobject = shift;
push @{$self->{'linked'}}, $ooobject;
return $self->extract($ooobject->{'part'});
}
#-----------------------------------------------------------------------------
# copy an individual member from the current OODoc::File instance($self)
# to an external Archive::Zip object ($archive), using a temporary flat file
sub copyMember
{
my $self = shift;
my $archive = shift;
my $member = shift;
my $m = $self->{'archive'}->memberNamed($member);
unless ($m)
{
warn "[" . __PACKAGE__ .
"::copyMember] Unknown source member\n";
return undef;
}
my $tmpfile = $self->extract_temp_file($m);
unless ($tmpfile)
{
warn "[" . __PACKAGE__ .
"::copyMember] File extraction error\n";
return undef;
}
store_member
(
$archive,
member => $member,
file => $tmpfile,
compress => ($member eq 'meta.xml') ? 0 : 1
);
}
#-----------------------------------------------------------------------------
# inserts $data as a new member in an external Archive::Zip object
sub addNewMember
{
my $self = shift;
my ($archive, $member, $data) = @_;
unless ($archive && $member && $data)
{
warn "[" . __PACKAGE__ .
"::addNewMember] Missing argument(s)\n";
return undef;
}
# temporary file creation --------------------
my $tmpfile = $self->store_temp_file($data);
unless ($tmpfile)
{
warn "[" . __PACKAGE__ . "::addNewMember] " .
"Temporary file error\n";
return undef;
}
# member insertion/compression ---------------
return store_member
(
$archive,
member => $member,
file => $tmpfile,
compress => ($member eq 'meta.xml') ? 0 : 1
);
}
#-----------------------------------------------------------------------------
# update mimetype
sub change_mimetype
{
my $self = shift;
my $class = shift;
my $mimetype = mime_type($class);
my $ootool = $OOTYPE{$class};
return undef unless $mimetype;
my $tmpfile = $self->store_temp_file($mimetype);
$self->raw_delete('mimetype');
$self->raw_import('mimetype', $tmpfile);
return 1;
}
#-----------------------------------------------------------------------------
# creates a new OO file, copying unchanged members & updating
# modified ones (by default, the new OO file replaces the old one)
sub save
{
my $self = shift;
my $targetfile = shift;
unless
(
OpenOffice::OODoc::File::checkWorkingDirectory
($self->{'work_dir'})
)
{
warn "[" . __PACKAGE__ . "::save] " .
"Write operation not allowed - " .
"Working directory missing or non writable\n";
return undef;
}
my %newmembers = ();
foreach my $nm (@{$self->{'linked'}})
{
my $ro = $nm->{'read_only'};
next if $ro &&
(($ro eq '1') || ($ro eq 'on') || ($ro eq 'true'));
$newmembers{$nm->{'part'}} = $nm->getXMLContent;
}
my $outfile = undef;
my $tmpfile = undef;
# target file check --------------------------
$targetfile = $self->{'source_file'} unless $targetfile;
if ( -e $targetfile )
{
unless ( -f $targetfile )
{
warn "[" . __PACKAGE__ . "::save] " .
"$targetfile is not a regular file\n";
return undef;
}
unless ( -w $targetfile )
{
warn "[" . __PACKAGE__ . "::save " .
"$targetfile is read only\n";
return undef;
}
}
# output to temporary file if target eq source
if ($targetfile eq $self->{'source_file'})
{
$outfile = $self->new_temp_file_name;
}
else
{
$outfile = $targetfile;
}
# discriminate replaced/added members --------
my %replacedmembertable = ();
my @addedmemberlist = ();
foreach my $nmn (keys %newmembers)
{
my $tmn = $self->CtrlMemberName($nmn);
if ($tmn)
{
$replacedmembertable{$tmn} = $nmn;
}
else
{
push @addedmemberlist, $nmn;
}
}
# target archive operation -------------------
$self->{'archive'}->writeToFileNamed($outfile);
my $archive = Archive::Zip->new;
my $status = $archive->read($outfile);
unless ($status == AZ_OK)
{
warn "[" . __PACKAGE__ . "::save] Archive write error\n";
return undef;
}
foreach my $oldmember (@{$self->{'members'}})
{
my $k = $replacedmembertable{$oldmember};
if ($k) # (replaced member)
{
$archive->removeMember($oldmember);
$self->addNewMember
($archive, $oldmember, $newmembers{$k});
}
}
foreach my $name (@addedmemberlist) # (added member)
{
$self->addNewMember($archive, $name, $newmembers{$name});
}
foreach my $raw_member (@{$self->{'raw_members'}}) # optional raw data
{
$archive->removeMember($raw_member->{'member'});
store_member
(
$archive,
member => $raw_member->{'member'},
file => $raw_member->{'file'},
compress => 1
)
}
foreach my $member_to_be_deleted (@{$self->{'to_be_deleted'}})
{
$archive->removeMember($member_to_be_deleted);
}
$status = $archive->overwrite();
# post write control & cleanup ---------------
if ($status == AZ_OK)
{
unless ($outfile eq $targetfile)
{
require File::Copy;
unlink $targetfile;
File::Copy::move($outfile, $targetfile);
}
$self->remove_temp_files;
return 1;
}
else
{
warn "[" . __PACKAGE__ . "::save] Archive write error\n";
return undef;
}
}
#-----------------------------------------------------------------------------
1;