File Coverage

blib/lib/OpenOffice/OODoc/File.pm
Criterion Covered Total %
statement 12 304 3.9
branch 0 138 0.0
condition 0 56 0.0
subroutine 4 26 15.3
pod 8 20 40.0
total 24 544 4.4


line stmt bran cond sub pod time code
1             #-----------------------------------------------------------------------------
2             #
3             # $Id : File.pm 2.203 2010-07-07 JMG$
4             #
5             # Created and maintained by Jean-Marie Gouarne
6             # Copyright 2010 by Genicorp, S.A. (www.genicorp.com)
7             #
8             #-----------------------------------------------------------------------------
9            
10             package OpenOffice::OODoc::File;
11 2     2   48 use 5.008_000;
  2         10  
  2         79  
12 2     2   91 use strict;
  2         5  
  2         129  
13             our $VERSION = '2.203';
14 2     2   5155 use Archive::Zip 1.18 qw ( :DEFAULT :CONSTANTS :ERROR_CODES );
  2         528231  
  2         3732  
15 2     2   59 use File::Temp;
  2         6  
  2         19283  
16            
17             #-----------------------------------------------------------------------------
18             # some defaults
19            
20             our $DEFAULT_OFFICE_FORMAT = 2; # OpenDocument format
21             our $DEFAULT_COMPRESSION_METHOD = COMPRESSION_DEFLATED;
22             our $DEFAULT_COMPRESSION_LEVEL = COMPRESSION_LEVEL_BEST_COMPRESSION;
23             our $DEFAULT_EXPORT_PATH = './';
24             our $WORKING_DIRECTORY = '.';
25             our $TEMPLATE_PATH = '';
26             our $MIMETYPE_BASE = 'application/vnd.oasis.opendocument.';
27             our %OOTYPE =
28             (
29             text => 'writer',
30             spreadsheet => 'calc',
31             presentation => 'impress',
32             drawing => 'draw',
33             );
34             our %ODFTYPE =
35             (
36             text => 'text',
37             spreadsheet => 'spreadsheet',
38             presentation => 'presentation',
39             drawing => 'graphics'
40             );
41             our %ODF_SUFFIX =
42             (
43             text => 'odt',
44             spreadsheet => 'ods',
45             presentation => 'odp',
46             drawing => 'odg'
47             );
48             our %OOO_SUFFIX =
49             (
50             text => 'sxw',
51             spreadsheet => 'sxc',
52             presentation => 'sxi',
53             drawing => 'sxd'
54             );
55            
56             #-----------------------------------------------------------------------------
57             # returns the mimetype string according to a document class
58            
59             sub mime_type
60             {
61 0     0 0   my $class = shift;
62 0 0 0       return undef unless ($class && $ODFTYPE{$class});
63 0           return $MIMETYPE_BASE . $ODFTYPE{$class};
64             }
65            
66             #-----------------------------------------------------------------------------
67             # get/set the path for XML templates
68            
69             sub templatePath
70             {
71 0     0 1   my $newpath = shift;
72 0 0         $TEMPLATE_PATH = $newpath if defined $newpath;
73 0           return $TEMPLATE_PATH;
74             }
75            
76             #-----------------------------------------------------------------------------
77             # member storage
78            
79             sub store_member
80             {
81 0     0 0   my $zipfile = shift;
82 0           my %opt =
83             (
84             compress => 1,
85             @_
86             );
87 0 0         unless ($opt{'member'})
88             {
89 0           warn "[" . __PACKAGE__ . "::store_member] " .
90             "Missing member name\n";
91 0           return undef;
92             }
93 0           my $m = undef;
94 0 0         if ($opt{'string'})
    0          
95             {
96 0           $m = $zipfile->addString($opt{'string'}, $opt{'member'});
97             }
98             elsif ($opt{'file'})
99             {
100 0           my $f = $opt{'file'};
101 0 0 0       unless (-r $f && (-f $f || -d $f ))
      0        
102             {
103 0           warn "[" . __PACKAGE__ . "::store_member] " .
104             "Resource $f not available\n";
105 0           return undef;
106             }
107 0           $m = $zipfile->addFileOrDirectory($f, $opt{'member'});
108             }
109             else
110             {
111 0           warn "[" . __PACKAGE__ . "::store_member] " .
112             "Missing content to store\n";
113 0           return undef;
114             }
115 0 0         unless ($m)
116             {
117 0           warn "[" . __PACKAGE__ . "::store_member] " .
118             "Member storage failure\n[" . $opt{'file'} . "]\n";
119 0           return undef;
120             }
121 0 0         unless ($opt{'compress'})
122             {
123 0           $m->desiredCompressionMethod(COMPRESSION_STORED);
124             }
125             else
126             {
127 0           $m->desiredCompressionMethod($DEFAULT_COMPRESSION_METHOD);
128 0           $m->desiredCompressionLevel($DEFAULT_COMPRESSION_LEVEL);
129             }
130 0           return $m;
131             }
132            
133             #-----------------------------------------------------------------------------
134             # new container creation from template
135            
136             sub _load_template_file
137             {
138 0     0     my %opt =
139             (
140             template_path => $TEMPLATE_PATH,
141             @_
142             );
143            
144 0           my $basepath = undef;
145 0 0         if ($opt{'template_path'})
146             {
147 0           $basepath = $opt{'template_path'};
148             }
149             else
150             {
151 0           require File::Basename;
152 0           $basepath =
153             File::Basename::dirname
154             ($INC{"OpenOffice/OODoc/File.pm"}) .
155             '/templates/';
156             }
157 0           $basepath =~ s/\\/\//g;
158 0 0         my $suffix = $opt{'opendocument'} ?
159             $ODF_SUFFIX{$opt{'class'}} :
160             $OOO_SUFFIX{$opt{'class'}};
161 0           delete $opt{'class'};
162            
163 0           my $source_file = $basepath . '/template.' . $suffix;
164 0           my $archive = Archive::Zip->new;
165 0 0         if ($archive->read($source_file) != AZ_OK)
166             {
167 0           $archive = undef;
168             }
169 0           return $archive;
170             }
171              
172             #-----------------------------------------------------------------------------
173             # existing ZIP file container loader
174              
175             sub _load_container
176             {
177 0 0   0     my $container = shift or return undef;
178 0 0         my $source = shift or return undef;
179            
180 0           my $z = Archive::Zip->new;
181            
182 0 0         if (UNIVERSAL::isa($source, 'IO::File'))
183             {
184 0 0         if ($z->readFromFileHandle($source) != AZ_OK)
185             {
186 0           warn "[" . __PACKAGE__ . "::new] Handle read error\n";
187 0           return undef;
188             }
189             }
190             else
191             {
192 0 0 0       unless ( -e $source && -f $source && -r $source )
      0        
193             {
194 0           warn "[" . __PACKAGE__ . "::new] " .
195             "File $source unavailable\n";
196 0           return undef;
197             }
198 0 0         if ($z->read($source) != AZ_OK)
199             {
200 0           warn "[" . __PACKAGE__ . "::new] File read error\n";
201 0           return undef;
202             }
203             }
204 0           $container->{'source_file'} = $source;
205 0           return $z;
206             }
207            
208             #-----------------------------------------------------------------------------
209             # control & conversion of XML component names of the OO file
210            
211             sub CtrlMemberName
212             {
213 0     0 0   my $self = shift;
214 0           my $member = shift;
215            
216 0           my $m = lc $member;
217 0           foreach my $n ('content', 'meta', 'styles', 'settings')
218             {
219 0 0         if ($m eq $n)
220             {
221 0           $member = $n . '.xml';
222 0           last;
223             }
224             }
225            
226 0           foreach $m ( @{ $self->{'members'} } )
  0            
227             {
228 0 0         return $member if ($member eq $m);
229             }
230            
231 0           return undef;
232             }
233            
234             #-----------------------------------------------------------------------------
235             # check working directory
236            
237             sub checkWorkingDirectory
238             {
239 0   0 0 0   my $path = shift || $WORKING_DIRECTORY;
240            
241 0 0         if (-d $path)
242             {
243 0 0         if (-w $path)
244             {
245 0           return 1;
246             }
247             else
248             {
249 0           warn "[" . __PACKAGE__ . "] " .
250             "No write permission in $path\n";
251             }
252             }
253             else
254             {
255 0           warn "[" . __PACKAGE__ . "] " .
256             "$path is not a directory\n";
257             }
258 0           return undef;
259             }
260            
261             #-----------------------------------------------------------------------------
262             # unique temporary file name generation
263            
264             sub new_temp_file_name
265             {
266 0     0 0   my $self = shift;
267            
268 0           return File::Temp::mktemp($self->{'work_dir'} . '/oo_XXXXX');
269             }
270            
271             #-----------------------------------------------------------------------------
272             # temporary data storage
273            
274             sub store_temp_file
275             {
276 0     0 0   my $self = shift;
277 0           my $data = shift;
278            
279 0           my $tmpfile = $self->new_temp_file_name;
280            
281 0 0         unless (open FH, '>:utf8', $tmpfile)
282             {
283 0           warn "[" . __PACKAGE__ . "::store_temp_file] " .
284             "Unable to create temporary file $tmpfile\n";
285 0           return undef;
286             }
287 0 0         unless (print FH $data)
288             {
289 0           warn "[" . __PACKAGE__ . "::store_temp_file] " .
290             "Write error in temporary file $tmpfile\n";
291 0           return undef;
292             }
293 0 0         unless (close FH)
294             {
295 0           warn "[" . __PACKAGE__ . "::store_temp_file] " .
296             "Unknown error in temporary file $tmpfile\n";
297 0           return undef;
298             }
299 0           push @{$self->{'temporary_files'}}, $tmpfile;
  0            
300 0           return $tmpfile;
301             }
302            
303             #-----------------------------------------------------------------------------
304             # temporary member extraction
305            
306             sub extract_temp_file
307             {
308 0     0 0   my $self = shift;
309 0           my $member = shift;
310            
311 0 0         my $m = ref $member ?
312             $member :
313             $self->{'archive'}->memberNamed($member);
314            
315 0           my $tmpfile = $self->new_temp_file_name;
316 0           my $result = $m->extractToFileNamed($tmpfile);
317 0 0         if ($result == AZ_OK)
318             {
319 0           push @{$self->{'temporary_files'}}, $tmpfile;
  0            
320 0           return $tmpfile;
321             }
322             else
323             {
324 0           return undef;
325             }
326             }
327            
328             #-----------------------------------------------------------------------------
329             # temporary storage cleanup
330             # returns the number of deleted files and clears the list of temp files
331            
332             sub remove_temp_files
333             {
334 0     0 0   my $self = shift;
335 0           my $count = 0;
336            
337 0           while (@{$self->{'temporary_files'}})
  0            
338             {
339 0           my $tmpfile = shift @{$self->{'temporary_files'}};
  0            
340 0           my $r = undef;
341 0 0         unless ( -d $tmpfile ) { $r = unlink $tmpfile; }
  0            
342 0           else { $r = rmdir $tmpfile; }
343 0 0         unless ($r > 0)
344             {
345 0           warn
346             "[" . __PACKAGE__ . "::remove_temp_files] " .
347             "Temporary file $tmpfile can't be removed\n";
348             }
349             else
350             {
351 0           $count++;
352             }
353             }
354 0           return $count;
355             }
356            
357             #-----------------------------------------------------------------------------
358             # constructor
359            
360             sub new
361             {
362 0     0 1   my $caller = shift;
363 0   0       my $class = ref($caller) || $caller;
364 0           my $sourcefile = shift;
365 0           my $self =
366             {
367             'linked' => [],
368             'work_dir' =>
369             $OpenOffice::OODoc::File::WORKING_DIRECTORY,
370             'template_path' =>
371             $OpenOffice::OODoc::File::TEMPLATE_PATH,
372             'temporary_files' => [],
373             'raw_members' => [],
374             'to_be_deleted' => [],
375             @_
376             };
377            
378 0           my $od = lc $self->{'opendocument'};
379 0 0 0       unless ($od)
    0 0        
    0 0        
      0        
380             {
381 0 0         if ($OpenOffice::OODoc::File::DEFAULT_OFFICE_FORMAT == 2)
382 0           { $self->{'opendocument'} = 1; }
383             }
384             elsif (($od eq '1') || ($od eq 'on') || ($od eq 'true'))
385             {
386 0           $self->{'opendocument'} = 1;
387             }
388             elsif (($od eq '0') || ($od eq 'off') || ($od eq 'false'))
389             {
390 0           delete $self->{'opendocument'};
391             }
392             else
393             {
394 0           warn "[" . __PACKAGE__ . "::new] Wrong 'opendocument' option\n";
395 0           return undef;
396             }
397            
398 0 0         if ($self->{'create'}) # new ODF container
    0          
399             {
400 0 0         $self->{'create'} = 'drawing'
401             if $self->{'create'} eq 'graphics';
402 0           $self->{'archive'} = _load_template_file
403             (
404             class => $self->{'create'},
405             template_path => $self->{'template_path'},
406             opendocument => $self->{'opendocument'}
407             );
408 0 0 0       unless ($self->{'archive'} && ref $self->{'archive'})
409             {
410 0           delete $self->{'archive'};
411 0           warn "[" . __PACKAGE__ . "::new] " .
412             "Bad or missing template\n";
413 0           return undef;
414             }
415 0   0       $self->{'source_file'} = $sourcefile || "";
416             }
417             elsif ($sourcefile) # existing container
418             {
419 0           $self->{'archive'} = _load_container($self, $sourcefile);
420 0 0         return undef unless $self->{'archive'};
421             }
422             else
423             {
424 0           warn "[" . __PACKAGE__ . "::new] Missing source file\n";
425 0           return undef;
426             }
427            
428 0           $self->{'members'} = [ $self->{'archive'}->memberNames ];
429 0           return bless $self, $class;
430             }
431            
432             #-----------------------------------------------------------------------------
433             # individual zip XML member extraction/uncompression
434            
435             sub extract
436             {
437 0     0 1   my $self = shift;
438 0           my $member = $self->CtrlMemberName(shift);
439            
440 0 0         unless ($member)
441             {
442 0           warn "[" . __PACKAGE__ . "::extract] Unknown member\n";
443 0           return undef;
444             }
445            
446 0 0         unless ($self->{'archive'})
447             {
448 0           warn "[" . __PACKAGE__ . "::extract] No archive\n";
449 0           return undef;
450             }
451            
452 0           return $self->{'archive'}->contents($member);
453             }
454            
455             #-----------------------------------------------------------------------------
456             # individual zip member raw export (see Archive::Zip::extractMember)
457            
458             sub raw_export
459             {
460 0     0 1   my $self = shift;
461            
462 0 0         unless ($self->{'archive'})
463             {
464 0           warn "[" . __PACKAGE__ . "::raw_export] No archive\n";
465 0           return undef;
466             }
467            
468 0           my $source = shift;
469 0           my $target = shift;
470 0 0         if (defined $target)
471             {
472 0 0         unless ($target =~ /\//)
473             {
474 0           $target = $DEFAULT_EXPORT_PATH . $target;
475             }
476 0           unshift @_, $target;
477             }
478 0           unshift @_, $source;
479            
480 0 0         if ($self->{'archive'}->extractMember(@_) == AZ_OK)
481             {
482 0 0         return $target ? $target : $source;
483             }
484             else
485             {
486 0           warn "[" . __PACKAGE__ . "::raw_export] File output error\n";
487 0           return undef;
488             }
489             }
490            
491             #-----------------------------------------------------------------------------
492             # individual zip member raw import
493             # file to be imported is only registered here; real import by save()
494            
495             sub raw_import
496             {
497 0     0 1   my $self = shift;
498 0           my $membername = shift;
499 0           my $filename = shift;
500 0 0         $filename = $membername unless $filename;
501 0           my %new_member = ('file' => $filename, 'member' => $membername);
502            
503 0           push @{$self->{'raw_members'}}, \%new_member;
  0            
504 0           return %new_member;
505             }
506            
507             #-----------------------------------------------------------------------------
508             # individual zip member removing (real deletion committed by save)
509             # WARNING: removing a member doesn't automatically update "manifest.xml"
510            
511             sub raw_delete
512             {
513 0     0 1   my $self = shift;
514 0 0         my $member = $self->CtrlMemberName(shift)
515             or return undef;
516            
517 0           my $mbcount = scalar @{$self->{'members'}};
  0            
518 0           for (my $i = 0 ; $i < $mbcount ; $i++)
519             {
520 0 0         if ($self->{'members'}[$i] eq $member)
521             {
522 0           splice(@{$self->{'members'}}, $i, 1);
  0            
523 0           push @{$self->{'to_be_deleted'}}, $member;
  0            
524 0           return 1;
525             }
526             }
527 0           return undef;
528             }
529            
530             #-----------------------------------------------------------------------------
531             # archive list
532            
533             sub getMemberList
534             {
535 0     0 0   my $self = shift;
536            
537 0           return @{$self->{'members'}};
  0            
538             }
539            
540             #-----------------------------------------------------------------------------
541             # connects the current OODoc::File instance to a client OODoc::XPath object
542             # and extracts the corresponding XML member (to be transparently invoked
543             # by the constructor of OODoc::XPath when activated with a 'file' parameter)
544            
545             sub link
546             {
547 0     0 1   my $self = shift;
548 0           my $ooobject = shift;
549            
550 0           push @{$self->{'linked'}}, $ooobject;
  0            
551 0           return $self->extract($ooobject->{'part'});
552             }
553            
554             #-----------------------------------------------------------------------------
555             # copy an individual member from the current OODoc::File instance($self)
556             # to an external Archive::Zip object ($archive), using a temporary flat file
557            
558             sub copyMember
559             {
560 0     0 0   my $self = shift;
561 0           my $archive = shift;
562 0           my $member = shift;
563            
564 0           my $m = $self->{'archive'}->memberNamed($member);
565 0 0         unless ($m)
566             {
567 0           warn "[" . __PACKAGE__ .
568             "::copyMember] Unknown source member\n";
569 0           return undef;
570             }
571 0           my $tmpfile = $self->extract_temp_file($m);
572 0 0         unless ($tmpfile)
573             {
574 0           warn "[" . __PACKAGE__ .
575             "::copyMember] File extraction error\n";
576 0           return undef;
577             }
578            
579 0 0 0       my $compress =
580             (($member eq 'meta.xml') || ($member eq 'mimetype')) ? 0 : 1;
581 0           store_member
582             (
583             $archive,
584             member => $member,
585             file => $tmpfile,
586             compress => $compress
587             );
588             }
589            
590             #-----------------------------------------------------------------------------
591             # inserts $data as a new member in an external Archive::Zip object
592            
593             sub addNewMember
594             {
595 0     0 0   my $self = shift;
596 0           my ($archive, $member, $data) = @_;
597            
598 0 0 0       unless ($archive && $member && $data)
      0        
599             {
600 0           warn "[" . __PACKAGE__ .
601             "::addNewMember] Missing argument(s)\n";
602 0           return undef;
603             }
604            
605             # temporary file creation --------------------
606            
607 0           my $tmpfile = $self->store_temp_file($data);
608 0 0         unless ($tmpfile)
609             {
610 0           warn "[" . __PACKAGE__ . "::addNewMember] " .
611             "Temporary file error\n";
612 0           return undef;
613             }
614            
615             # member insertion/compression ---------------
616            
617 0 0 0       my $compress =
618             (($member eq 'meta.xml') || ($member eq 'mimetype')) ? 0 : 1;
619 0           return store_member
620             (
621             $archive,
622             member => $member,
623             file => $tmpfile,
624             compress => $compress
625             );
626             }
627            
628             #-----------------------------------------------------------------------------
629             # update mimetype
630            
631             sub change_mimetype
632             {
633 0     0 0   my $self = shift;
634 0           my $class = shift;
635 0           my $mimetype = mime_type($class);
636            
637 0 0         return undef unless $mimetype;
638 0           my $tmpfile = $self->store_temp_file($mimetype);
639 0           $self->raw_delete('mimetype');
640 0           $self->raw_import('mimetype', $tmpfile);
641 0           return 1;
642             }
643            
644             #-----------------------------------------------------------------------------
645             # creates a new OO file, copying unchanged members & updating
646             # modified ones (by default, the new OO file replaces the old one)
647            
648             sub save
649             {
650 0     0 1   my $self = shift;
651 0           my $targetfile = shift;
652            
653 0 0         unless
654             (
655             OpenOffice::OODoc::File::checkWorkingDirectory
656             ($self->{'work_dir'})
657             )
658             {
659 0           warn "[" . __PACKAGE__ . "::save] " .
660             "Write operation not allowed - " .
661             "Working directory missing or non writable\n";
662 0           return undef;
663             }
664            
665 0           my %newmembers = ();
666 0           foreach my $nm (@{$self->{'linked'}})
  0            
667             {
668 0           my $ro = $nm->{'read_only'};
669 0 0 0       next if $ro &&
      0        
670             (($ro eq '1') || ($ro eq 'on') || ($ro eq 'true'));
671 0           $newmembers{$nm->{'part'}} = $nm->getXMLContent;
672             }
673            
674 0           my $outfile = undef;
675 0           my $tmpfile = undef;
676            
677             # target file check --------------------------
678            
679 0 0         $targetfile = $self->{'source_file'} unless $targetfile;
680 0 0         if (UNIVERSAL::isa($targetfile, 'IO::File'))
681             {
682 0           $outfile = $self->new_temp_file_name();
683             }
684             else
685             {
686 0 0         if ( -e $targetfile )
687             {
688 0 0         unless ( -f $targetfile )
689             {
690 0           warn "[" . __PACKAGE__ . "::save] " .
691             "$targetfile is not a regular file\n";
692 0           return undef;
693             }
694 0 0         unless ( -w $targetfile )
695             {
696 0           warn "[" . __PACKAGE__ . "::save " .
697             "$targetfile is read only\n";
698 0           return undef;
699             }
700             }
701 0 0         $outfile = ($targetfile eq $self->{'source_file'}) ?
702             $self->new_temp_file_name() :
703             $targetfile;
704             }
705            
706            
707             # discriminate replaced/added members --------
708            
709 0           my %replacedmembertable = ();
710 0           my @addedmemberlist = ();
711 0           foreach my $nmn (keys %newmembers)
712             {
713 0           my $tmn = $self->CtrlMemberName($nmn);
714 0 0         if ($tmn)
715             {
716 0           $replacedmembertable{$tmn} = $nmn;
717             }
718             else
719             {
720 0           push @addedmemberlist, $nmn;
721             }
722             }
723            
724             # target archive operation -------------------
725            
726             # output to temporary archive
727 0           $self->{'archive'}->writeToFileNamed($outfile);
728              
729             # reload temporary archive
730 0           my $archive = Archive::Zip->new;
731 0           my $status = $archive->read($outfile);
732 0 0         unless ($status == AZ_OK)
733             {
734 0           warn "[" . __PACKAGE__ . "::save] Archive I/O error\n";
735 0           return undef;
736             }
737            
738 0           foreach my $oldmember (@{$self->{'members'}})
  0            
739             {
740 0           my $k = $replacedmembertable{$oldmember};
741 0 0         if ($k) # (replaced member)
742             {
743 0           $archive->removeMember($oldmember);
744 0           $self->addNewMember
745             ($archive, $oldmember, $newmembers{$k});
746             }
747             }
748 0           foreach my $name (@addedmemberlist) # (added member)
749             {
750 0           $self->addNewMember($archive, $name, $newmembers{$name});
751             }
752            
753 0           foreach my $raw_member (@{$self->{'raw_members'}}) # optional raw data
  0            
754             {
755 0           $archive->removeMember($raw_member->{'member'});
756 0 0         store_member
757             (
758             $archive,
759             member => $raw_member->{'member'},
760             file => $raw_member->{'file'},
761             compress =>
762             ($raw_member->{'member'} eq 'mimetype') ?
763             0 : 1
764             )
765             }
766            
767 0           foreach my $member_to_be_deleted (@{$self->{'to_be_deleted'}})
  0            
768             {
769 0           $archive->removeMember($member_to_be_deleted);
770             }
771            
772 0           $status = $archive->overwrite();
773            
774             # post write control & cleanup ---------------
775 0 0         if ($status == AZ_OK)
776             {
777 0 0         unless ($outfile eq $targetfile)
778             {
779 0           require File::Copy;
780              
781 0 0         if (UNIVERSAL::isa($targetfile, 'IO::File'))
782             {
783 0           File::Copy::cp($outfile, $targetfile);
784 0           unlink $outfile;
785             }
786             else
787             {
788 0           unlink $targetfile;
789 0           File::Copy::move($outfile, $targetfile);
790             }
791             }
792 0           $self->remove_temp_files;
793 0           return 1;
794             }
795             else
796             {
797 0           warn "[" . __PACKAGE__ . "::save] Archive write error\n";
798 0           return undef;
799             }
800             }
801            
802             #-----------------------------------------------------------------------------
803             1;