4eed5eae20ad54151a841438974fa0bf9ed94c4f
[kivitendo-erp.git] / SL / Template / OpenDocument.pm
1 package SL::Template::OpenDocument;
2
3 use parent qw(SL::Template::Simple);
4
5 use Archive::Zip;
6 use Encode;
7 use POSIX 'setsid';
8
9 use SL::Iconv;
10
11 use Cwd;
12 # use File::Copy;
13 # use File::Spec;
14 # use File::Temp qw(:mktemp);
15 use IO::File;
16
17 use strict;
18
19 sub new {
20   my $type = shift;
21
22   my $self = $type->SUPER::new(@_);
23
24   $self->{"rnd"}   = int(rand(1000000));
25   $self->{"iconv"} = SL::Iconv->new($::lx_office_conf{system}->{dbcharset}, "UTF-8");
26
27   $self->set_tag_style('<%', '%>');
28   $self->{quot_re} = '"';
29
30   return $self;
31 }
32
33 sub parse_foreach {
34   my ($self, $var, $text, $start_tag, $end_tag, @indices) = @_;
35
36   my ($form, $new_contents) = ($self->{"form"}, "");
37
38   my $ary = $self->_get_loop_variable($var, 1, @indices);
39
40   for (my $i = 0; $i < scalar(@{$ary || []}); $i++) {
41     $form->{"__first__"} = $i == 0;
42     $form->{"__last__"} = ($i + 1) == scalar(@{$ary});
43     $form->{"__odd__"} = (($i + 1) % 2) == 1;
44     $form->{"__counter__"} = $i + 1;
45     my $new_text = $self->parse_block($text, (@indices, $i));
46     return undef unless (defined($new_text));
47     $new_contents .= $start_tag . $new_text . $end_tag;
48   }
49   map({ delete($form->{"__${_}__"}); } qw(first last odd counter));
50
51   return $new_contents;
52 }
53
54 sub find_end {
55   my ($self, $text, $pos, $var, $not) = @_;
56
57   my $depth = 1;
58   $pos = 0 unless ($pos);
59
60   while ($pos < length($text)) {
61     $pos++;
62
63     next if (substr($text, $pos - 1, 5) ne '&lt;%');
64
65     if ((substr($text, $pos + 4, 2) eq 'if') || (substr($text, $pos + 4, 3) eq 'for')) {
66       $depth++;
67
68     } elsif ((substr($text, $pos + 4, 4) eq 'else') && (1 == $depth)) {
69       if (!$var) {
70         $self->{"error"} = '<%else%> outside of <%if%> / <%ifnot%>.';
71         return undef;
72       }
73
74       my $block = substr($text, 0, $pos - 1);
75       substr($text, 0, $pos - 1) = "";
76       $text =~ s!^\&lt;\%[^\%]+\%\&gt;!!;
77       $text = '&lt;%if' . ($not ?  " " : "not ") . $var . '%&gt;' . $text;
78
79       return ($block, $text);
80
81     } elsif (substr($text, $pos + 4, 3) eq 'end') {
82       $depth--;
83       if ($depth == 0) {
84         my $block = substr($text, 0, $pos - 1);
85         substr($text, 0, $pos - 1) = "";
86         $text =~ s!^\&lt;\%[^\%]+\%\&gt;!!;
87
88         return ($block, $text);
89       }
90     }
91   }
92
93   return undef;
94 }
95
96 sub parse_block {
97   $main::lxdebug->enter_sub();
98
99   my ($self, $contents, @indices) = @_;
100
101   my $new_contents = "";
102
103   while ($contents ne "") {
104     if (substr($contents, 0, 1) eq "<") {
105       $contents =~ m|^<[^>]+>|;
106       my $tag = $&;
107       substr($contents, 0, length($&)) = "";
108
109       if ($tag =~ m|<table:table-row|) {
110         $contents =~ m|^(.*?)(</table:table-row[^>]*>)|;
111         my $table_row = $1;
112         my $end_tag = $2;
113
114         if ($table_row =~ m|\&lt;\%foreachrow\s+(.*?)\%\&gt;|) {
115           my $var = $1;
116
117           $contents =~ m|\&lt;\%foreachrow\s+.*?\%\&gt;|;
118           substr($contents, length($`), length($&)) = "";
119
120           ($table_row, $contents) = $self->find_end($contents, length($`));
121           if (!$table_row) {
122             $self->{"error"} = "Unclosed <\%foreachrow\%>." unless ($self->{"error"});
123             $main::lxdebug->leave_sub();
124             return undef;
125           }
126
127           $contents   =~ m|^(.*?)(</table:table-row[^>]*>)|;
128           $table_row .=  $1;
129           $end_tag    =  $2;
130
131           substr $contents, 0, length($&), '';
132
133           my $new_text = $self->parse_foreach($var, $table_row, $tag, $end_tag, @indices);
134           if (!defined($new_text)) {
135             $main::lxdebug->leave_sub();
136             return undef;
137           }
138           $new_contents .= $new_text;
139
140         } else {
141           substr($contents, 0, length($table_row) + length($end_tag)) = "";
142           my $new_text = $self->parse_block($table_row, @indices);
143           if (!defined($new_text)) {
144             $main::lxdebug->leave_sub();
145             return undef;
146           }
147           $new_contents .= $tag . $new_text . $end_tag;
148         }
149
150       } else {
151         $new_contents .= $tag;
152       }
153
154     } else {
155       $contents =~ /^[^<]+/;
156       my $text = $&;
157
158       my $pos_if = index($text, '&lt;%if');
159       my $pos_foreach = index($text, '&lt;%foreach');
160
161       if ((-1 == $pos_if) && (-1 == $pos_foreach)) {
162         substr($contents, 0, length($text)) = "";
163         $new_contents .= $self->substitute_vars($text, @indices);
164         next;
165       }
166
167       if ((-1 == $pos_if) || ((-1 != $pos_foreach) && ($pos_if > $pos_foreach))) {
168         $new_contents .= $self->substitute_vars(substr($contents, 0, $pos_foreach), @indices);
169         substr($contents, 0, $pos_foreach) = "";
170
171         if ($contents !~ m|^\&lt;\%foreach (.*?)\%\&gt;|) {
172           $self->{"error"} = "Malformed <\%foreach\%>.";
173           $main::lxdebug->leave_sub();
174           return undef;
175         }
176
177         my $var = $1;
178
179         substr($contents, 0, length($&)) = "";
180
181         my $block;
182         ($block, $contents) = $self->find_end($contents);
183         if (!$block) {
184           $self->{"error"} = "Unclosed <\%foreach\%>." unless ($self->{"error"});
185           $main::lxdebug->leave_sub();
186           return undef;
187         }
188
189         my $new_text = $self->parse_foreach($var, $block, "", "", @indices);
190         if (!defined($new_text)) {
191           $main::lxdebug->leave_sub();
192           return undef;
193         }
194         $new_contents .= $new_text;
195
196       } else {
197         if (!$self->_parse_block_if(\$contents, \$new_contents, $pos_if, @indices)) {
198           $main::lxdebug->leave_sub();
199           return undef;
200         }
201       }
202     }
203   }
204
205   $main::lxdebug->leave_sub();
206
207   return $new_contents;
208 }
209
210 sub parse {
211   $main::lxdebug->enter_sub();
212   my $self = $_[0];
213   local *OUT = $_[1];
214   my $form = $self->{"form"};
215
216   close(OUT);
217
218   my $file_name;
219   if ($form->{"IN"} =~ m|^/|) {
220     $file_name = $form->{"IN"};
221   } else {
222     $file_name = $form->{"templates"} . "/" . $form->{"IN"};
223   }
224
225   my $zip = Archive::Zip->new();
226   if (Archive::Zip->AZ_OK != $zip->read($file_name)) {
227     $self->{"error"} = "File not found/is not a OpenDocument file.";
228     $main::lxdebug->leave_sub();
229     return 0;
230   }
231
232   my $contents = Encode::decode('utf-8-strict', $zip->contents("content.xml"));
233   if (!$contents) {
234     $self->{"error"} = "File is not a OpenDocument file.";
235     $main::lxdebug->leave_sub();
236     return 0;
237   }
238
239   my $rnd = $self->{"rnd"};
240   my $new_styles = qq|<style:style style:name="TLXO${rnd}BOLD" style:family="text">
241 <style:text-properties fo:font-weight="bold" style:font-weight-asian="bold" style:font-weight-complex="bold"/>
242 </style:style>
243 <style:style style:name="TLXO${rnd}ITALIC" style:family="text">
244 <style:text-properties fo:font-style="italic" style:font-style-asian="italic" style:font-style-complex="italic"/>
245 </style:style>
246 <style:style style:name="TLXO${rnd}UNDERLINE" style:family="text">
247 <style:text-properties style:text-underline-style="solid" style:text-underline-width="auto" style:text-underline-color="font-color"/>
248 </style:style>
249 <style:style style:name="TLXO${rnd}STRIKETHROUGH" style:family="text">
250 <style:text-properties style:text-line-through-style="solid"/>
251 </style:style>
252 <style:style style:name="TLXO${rnd}SUPER" style:family="text">
253 <style:text-properties style:text-position="super 58%"/>
254 </style:style>
255 <style:style style:name="TLXO${rnd}SUB" style:family="text">
256 <style:text-properties style:text-position="sub 58%"/>
257 </style:style>
258 |;
259
260   $contents =~ s|</office:automatic-styles>|${new_styles}</office:automatic-styles>|;
261   $contents =~ s|[\n\r]||gm;
262
263   my $new_contents;
264   if ($self->{use_template_toolkit}) {
265     my $additional_params = $::form;
266
267     $::form->init_template->process(\$contents, $additional_params, \$new_contents) || die $::form->template->error;
268   } else {
269     $new_contents = $self->parse_block($contents);
270   }
271   if (!defined($new_contents)) {
272     $main::lxdebug->leave_sub();
273     return 0;
274   }
275
276 #   $new_contents =~ s|>|>\n|g;
277
278   $zip->contents("content.xml", Encode::encode('utf-8-strict', $new_contents));
279
280   my $styles = Encode::decode('utf-8-strict', $zip->contents("styles.xml"));
281   if ($contents) {
282     my $new_styles = $self->parse_block($styles);
283     if (!defined($new_contents)) {
284       $main::lxdebug->leave_sub();
285       return 0;
286     }
287     $zip->contents("styles.xml", Encode::encode('utf-8-strict', $new_styles));
288   }
289
290   $zip->writeToFileNamed($form->{"tmpfile"}, 1);
291
292   my $res = 1;
293   if ($form->{"format"} =~ /pdf/) {
294     $res = $self->convert_to_pdf();
295   }
296
297   $main::lxdebug->leave_sub();
298   return $res;
299 }
300
301 sub is_xvfb_running {
302   $main::lxdebug->enter_sub();
303
304   my ($self) = @_;
305
306   local *IN;
307   my $dfname = $self->{"userspath"} . "/xvfb_display";
308   my $display;
309
310   $main::lxdebug->message(LXDebug->DEBUG2(), "    Looking for $dfname\n");
311   if ((-f $dfname) && open(IN, $dfname)) {
312     my $pid = <IN>;
313     chomp($pid);
314     $display = <IN>;
315     chomp($display);
316     my $xauthority = <IN>;
317     chomp($xauthority);
318     close(IN);
319
320     $main::lxdebug->message(LXDebug->DEBUG2(), "      found with $pid and $display\n");
321
322     if ((! -d "/proc/$pid") || !open(IN, "/proc/$pid/cmdline")) {
323       $main::lxdebug->message(LXDebug->DEBUG2(), "  no/wrong process #1\n");
324       unlink($dfname, $xauthority);
325       $main::lxdebug->leave_sub();
326       return undef;
327     }
328     my $line = <IN>;
329     close(IN);
330     if ($line !~ /xvfb/i) {
331       $main::lxdebug->message(LXDebug->DEBUG2(), "      no/wrong process #2\n");
332       unlink($dfname, $xauthority);
333       $main::lxdebug->leave_sub();
334       return undef;
335     }
336
337     $ENV{"XAUTHORITY"} = $xauthority;
338     $ENV{"DISPLAY"} = $display;
339   } else {
340     $main::lxdebug->message(LXDebug->DEBUG2(), "      not found\n");
341   }
342
343   $main::lxdebug->leave_sub();
344
345   return $display;
346 }
347
348 sub spawn_xvfb {
349   $main::lxdebug->enter_sub();
350
351   my ($self) = @_;
352
353   $main::lxdebug->message(LXDebug->DEBUG2, "spawn_xvfb()\n");
354
355   my $display = $self->is_xvfb_running();
356
357   if ($display) {
358     $main::lxdebug->leave_sub();
359     return $display;
360   }
361
362   $display = 99;
363   while ( -f "/tmp/.X${display}-lock") {
364     $display++;
365   }
366   $display = ":${display}";
367   $main::lxdebug->message(LXDebug->DEBUG2(), "  display $display\n");
368
369   my $mcookie = `mcookie`;
370   die("Installation error: mcookie not found.") if ($? != 0);
371   chomp($mcookie);
372
373   $main::lxdebug->message(LXDebug->DEBUG2(), "  mcookie $mcookie\n");
374
375   my $xauthority = "/tmp/.Xauthority-" . $$ . "-" . time() . "-" . int(rand(9999999));
376   $ENV{"XAUTHORITY"} = $xauthority;
377
378   $main::lxdebug->message(LXDebug->DEBUG2(), "  xauthority $xauthority\n");
379
380   system("xauth add \"${display}\" . \"${mcookie}\"");
381   if ($? != 0) {
382     $self->{"error"} = "Conversion to PDF failed because OpenOffice could not be started (xauth: $!)";
383     $main::lxdebug->leave_sub();
384     return undef;
385   }
386
387   $main::lxdebug->message(LXDebug->DEBUG2(), "  about to fork()\n");
388
389   my $pid = fork();
390   if (0 == $pid) {
391     $main::lxdebug->message(LXDebug->DEBUG2(), "  Child execing\n");
392     exec($::lx_office_conf{applications}->{xvfb}, $display, "-screen", "0", "640x480x8", "-nolisten", "tcp");
393   }
394   sleep(3);
395   $main::lxdebug->message(LXDebug->DEBUG2(), "  parent dont sleeping\n");
396
397   local *OUT;
398   my $dfname = $self->{"userspath"} . "/xvfb_display";
399   if (!open(OUT, ">", $dfname)) {
400     $self->{"error"} = "Conversion to PDF failed because OpenOffice could not be started ($dfname: $!)";
401     unlink($xauthority);
402     kill($pid);
403     $main::lxdebug->leave_sub();
404     return undef;
405   }
406   print(OUT "$pid\n$display\n$xauthority\n");
407   close(OUT);
408
409   $main::lxdebug->message(LXDebug->DEBUG2(), "  parent re-testing\n");
410
411   if (!$self->is_xvfb_running()) {
412     $self->{"error"} = "Conversion to PDF failed because OpenOffice could not be started.";
413     unlink($xauthority, $dfname);
414     kill($pid);
415     $main::lxdebug->leave_sub();
416     return undef;
417   }
418
419   $main::lxdebug->message(LXDebug->DEBUG2(), "  spawn OK\n");
420
421   $main::lxdebug->leave_sub();
422
423   return $display;
424 }
425
426 sub _run_python_uno {
427   my ($self, @args) = @_;
428
429   local $ENV{PYTHONPATH};
430   $ENV{PYTHONPATH} = $::lx_office_conf{environment}->{python_uno_path} . ':' . $ENV{PYTHONPATH} if $::lx_office_conf{environment}->{python_uno_path};
431   my $cmd          = $::lx_office_conf{applications}->{python_uno} . ' ' . join(' ', @args);
432   return `$cmd`;
433 }
434
435 sub is_openoffice_running {
436   my ($self) = @_;
437
438   $main::lxdebug->enter_sub();
439
440   my $output = $self->_run_python_uno('./scripts/oo-uno-test-conn.py', $::lx_office_conf{print_templates}->{openofficeorg_daemon_port}, ' 2> /dev/null');
441   chomp $output;
442
443   my $res = ($? == 0) || $output;
444   $main::lxdebug->message(LXDebug->DEBUG2(), "  is_openoffice_running(): res $res\n");
445
446   $main::lxdebug->leave_sub();
447
448   return $res;
449 }
450
451 sub spawn_openoffice {
452   $main::lxdebug->enter_sub();
453
454   my ($self) = @_;
455
456   $main::lxdebug->message(LXDebug->DEBUG2(), "spawn_openoffice()\n");
457
458   my ($try, $spawned_oo, $res);
459
460   $res = 0;
461   for ($try = 0; $try < 15; $try++) {
462     if ($self->is_openoffice_running()) {
463       $res = 1;
464       last;
465     }
466
467     if ($::dispatcher->interface_type eq 'FastCGI') {
468       $::dispatcher->{request}->Detach;
469     }
470
471     if (!$spawned_oo) {
472       my $pid = fork();
473       if (0 == $pid) {
474         $main::lxdebug->message(LXDebug->DEBUG2(), "  Child daemonizing\n");
475
476         if ($::dispatcher->interface_type eq 'FastCGI') {
477           $::dispatcher->{request}->Finish;
478           $::dispatcher->{request}->LastCall;
479         }
480         chdir('/');
481         open(STDIN, '/dev/null');
482         open(STDOUT, '>/dev/null');
483         my $new_pid = fork();
484         exit if ($new_pid);
485         my $ssres = setsid();
486         $main::lxdebug->message(LXDebug->DEBUG2(), "  Child execing\n");
487         my @cmdline = ($::lx_office_conf{applications}->{openofficeorg_writer},
488                        "-minimized", "-norestore", "-nologo", "-nolockcheck",
489                        "-headless",
490                        "-accept=socket,host=localhost,port=" .
491                        $::lx_office_conf{print_templates}->{openofficeorg_daemon_port} . ";urp;");
492         exec(@cmdline);
493       } else {
494         # parent
495         if ($::dispatcher->interface_type eq 'FastCGI') {
496           $::dispatcher->{request}->Attach;
497         }
498       }
499
500       $main::lxdebug->message(LXDebug->DEBUG2(), "  Parent after fork\n");
501       $spawned_oo = 1;
502       sleep(3);
503     }
504
505     sleep($try >= 5 ? 2 : 1);
506   }
507
508   if (!$res) {
509     $self->{"error"} = "Conversion from OpenDocument to PDF failed because " .
510       "OpenOffice could not be started.";
511   }
512
513   $main::lxdebug->leave_sub();
514
515   return $res;
516 }
517
518 sub convert_to_pdf {
519   $main::lxdebug->enter_sub();
520
521   my ($self) = @_;
522
523   my $form = $self->{"form"};
524
525   my $filename = $form->{"tmpfile"};
526   $filename =~ s/.odt$//;
527   if (substr($filename, 0, 1) ne "/") {
528     $filename = getcwd() . "/${filename}";
529   }
530
531   if (substr($self->{"userspath"}, 0, 1) eq "/") {
532     $ENV{'HOME'} = $self->{"userspath"};
533   } else {
534     $ENV{'HOME'} = getcwd() . "/" . $self->{"userspath"};
535   }
536
537   if (!$self->spawn_xvfb()) {
538     $main::lxdebug->leave_sub();
539     return 0;
540   }
541
542   if (!$::lx_office_conf{print_templates}->{openofficeorg_daemon}) {
543     system($::lx_office_conf{applications}->{openofficeorg_writer},
544            "-minimized", "-norestore", "-nologo", "-nolockcheck", "-headless",
545            "file:${filename}.odt",
546            "macro://" . (split('/', $filename))[-1] . "/Standard.Conversion.ConvertSelfToPDF()");
547   } else {
548     if (!$self->spawn_openoffice()) {
549       $main::lxdebug->leave_sub();
550       return 0;
551     }
552
553     $self->_run_python_uno('./scripts/oo-uno-convert-pdf.py', $::lx_office_conf{print_templates}->{openofficeorg_daemon_port}, "${filename}.odt");
554   }
555
556   my $res = $?;
557   if ((0 == $?) || (-f "${filename}.pdf" && -s "${filename}.pdf")) {
558     $form->{"tmpfile"} =~ s/odt$/pdf/;
559
560     unlink($filename . ".odt");
561
562     $main::lxdebug->leave_sub();
563     return 1;
564
565   }
566
567   unlink($filename . ".odt", $filename . ".pdf");
568   $self->{"error"} = "Conversion from OpenDocument to PDF failed. " .
569     "Exit code: $res";
570
571   $main::lxdebug->leave_sub();
572   return 0;
573 }
574
575 sub format_string {
576   my ($self, $variable) = @_;
577   my $form = $self->{"form"};
578   my $iconv = $self->{"iconv"};
579
580   $variable = $main::locale->quote_special_chars('Template/OpenDocument', $variable);
581
582   # Allow some HTML markup to be converted into the output format's
583   # corresponding markup code, e.g. bold or italic.
584   my $rnd = $self->{"rnd"};
585   my %markup_replace = ("b" => "BOLD", "i" => "ITALIC", "s" => "STRIKETHROUGH",
586                         "u" => "UNDERLINE", "sup" => "SUPER", "sub" => "SUB");
587
588   foreach my $key (keys(%markup_replace)) {
589     my $value = $markup_replace{$key};
590     $variable =~ s|\&lt;${key}\&gt;|<text:span text:style-name=\"TLXO${rnd}${value}\">|gi; #"
591     $variable =~ s|\&lt;/${key}\&gt;|</text:span>|gi;
592   }
593
594   return $iconv->convert($variable);
595 }
596
597 sub get_mime_type() {
598   my ($self) = @_;
599
600   if ($self->{"form"}->{"format"} =~ /pdf/) {
601     return "application/pdf";
602   } else {
603     return "application/vnd.oasis.opendocument.text";
604   }
605 }
606
607 sub uses_temp_file {
608   return 1;
609 }
610
611 1;