test action
[kivitendo-erp.git] / SL / Template / OpenDocument.pm
index b4ea1f2..e570321 100644 (file)
@@ -4,9 +4,11 @@ use parent qw(SL::Template::Simple);
 
 use Archive::Zip;
 use Encode;
+use HTML::Entities;
 use POSIX 'setsid';
 
 use SL::Iconv;
+use SL::Template::OpenDocument::Styles;
 
 use Cwd;
 # use File::Copy;
@@ -16,14 +18,139 @@ use IO::File;
 
 use strict;
 
+my %text_markup_replace = (
+  b   => "BOLD",
+  i   => "ITALIC",
+  s   => "STRIKETHROUGH",
+  u   => "UNDERLINE",
+  sup => "SUPER",
+  sub => "SUB",
+);
+
+sub _format_text {
+  my ($self, $content, %params) = @_;
+
+  $content = $::locale->quote_special_chars('Template/OpenDocument', $content);
+
+  # Allow some HTML markup to be converted into the output format's
+  # corresponding markup code, e.g. bold or italic.
+  foreach my $key (keys(%text_markup_replace)) {
+    my $value = $text_markup_replace{$key};
+    $content =~ s|\&lt;${key}\&gt;|<text:span text:style-name=\"TKIVITENDO${value}\">|gi; #"
+    $content =~ s|\&lt;/${key}\&gt;|</text:span>|gi;
+  }
+
+  return $content;
+}
+
+my %html_replace = (
+  '</ul>'     => '</text:list>',
+  '</ol>'     => '</text:list>',
+  '</li>'     => '</text:p></text:list-item>',
+  '<b>'       => '<text:span text:style-name="TKIVITENDOBOLD">',
+  '</b>'      => '</text:span>',
+  '<strong>'  => '<text:span text:style-name="TKIVITENDOBOLD">',
+  '</strong>' => '</text:span>',
+  '<i>'       => '<text:span text:style-name="TKIVITENDOITALIC">',
+  '</i>'      => '</text:span>',
+  '<em>'      => '<text:span text:style-name="TKIVITENDOITALIC">',
+  '</em>'     => '</text:span>',
+  '<u>'       => '<text:span text:style-name="TKIVITENDOUNDERLINE">',
+  '</u>'      => '</text:span>',
+  '<s>'       => '<text:span text:style-name="TKIVITENDOSTRIKETHROUGH">',
+  '</s>'      => '</text:span>',
+  '<sub>'     => '<text:span text:style-name="TKIVITENDOSUB">',
+  '</sub>'    => '</text:span>',
+  '<sup>'     => '<text:span text:style-name="TKIVITENDOSUPER">',
+  '</sup>'    => '</text:span>',
+  '<br/>'     => '<text:line-break/>',
+  '<br>'      => '<text:line-break/>',
+);
+
+sub _format_html {
+  my ($self, $content, %params) = @_;
+
+  my $in_p        = 0;
+  my $p_start_tag = qq|<text:p text:style-name="@{[ $self->{current_text_style} ]}">|;
+  my $prefix      = '';
+  my $suffix      = '';
+
+  my (@tags_to_open, @tags_to_close);
+  for (my $idx = scalar(@{ $self->{tag_stack} }) - 1; $idx >= 0; --$idx) {
+    my $tag = $self->{tag_stack}->[$idx];
+
+    next if $tag =~ m{/>$};
+    last if $tag =~ m{^<table};
+
+    if ($tag =~ m{^<text:p}) {
+      $in_p        = 1;
+      $p_start_tag = $tag;
+      last;
+
+    } else {
+      $suffix  =  "${tag}${suffix}";
+      $tag     =~ s{ .*>}{>};
+      $prefix .=  '</' . substr($tag, 1);
+    }
+  }
+
+  $content            =~ s{ ^<p> | </p>$ }{}gx if $in_p;
+  $content            =~ s{ \r+ }{}gx;
+  $content            =~ s{ \n+ }{ }gx;
+  $content            =~ s{ (?:\&nbsp;|\s)+ }{ }gx;
+
+  my $ul_start_tag    = qq|<text:list xml:id="list@{[ int rand(9999999999999999) ]}" text:style-name="LKIVITENDOitemize@{[ $self->{current_text_style} ]}">|;
+  my $ol_start_tag    = qq|<text:list xml:id="list@{[ int rand(9999999999999999) ]}" text:style-name="LKIVITENDOenumerate@{[ $self->{current_text_style} ]}">|;
+  my $ul_li_start_tag = qq|<text:list-item><text:p text:style-name="PKIVITENDOitemize@{[ $self->{current_text_style} ]}">|;
+  my $ol_li_start_tag = qq|<text:list-item><text:p text:style-name="PKIVITENDOenumerate@{[ $self->{current_text_style} ]}">|;
+
+  my @parts = map {
+    if (substr($_, 0, 1) eq '<') {
+      s{ +}{}g;
+      if ($_ eq '</p>') {
+        $in_p--;
+        $in_p == 0 ? '</text:p>' : '';
+
+      } elsif ($_ eq '<p>') {
+        $in_p++;
+        $in_p == 1 ? $p_start_tag : '';
+
+      } elsif ($_ eq '<ul>') {
+        $self->{used_list_styles}->{itemize}->{$self->{current_text_style}}   = 1;
+        $html_replace{'<li>'}                                                 = $ul_li_start_tag;
+        $ul_start_tag;
+
+      } elsif ($_ eq '<ol>') {
+        $self->{used_list_styles}->{enumerate}->{$self->{current_text_style}} = 1;
+        $html_replace{'<li>'}                                                 = $ol_li_start_tag;
+        $ol_start_tag;
+
+      } else {
+        $html_replace{$_} || '';
+      }
+
+    } else {
+      $::locale->quote_special_chars('Template/OpenDocument', HTML::Entities::decode_entities($_));
+    }
+  } split(m{(<.*?>)}x, $content);
+
+  my $out  = join('', $prefix, @parts, $suffix);
+
+  # $::lxdebug->dump(0, "prefix parts suffix", [ $prefix, join('', @parts), $suffix ]);
+
+  return $out;
+}
+
+my %formatters = (
+  html => \&_format_html,
+  text => \&_format_text,
+);
+
 sub new {
   my $type = shift;
 
   my $self = $type->SUPER::new(@_);
 
-  $self->{"rnd"}   = int(rand(1000000));
-  $self->{"iconv"} = SL::Iconv->new($::lx_office_conf{system}->{dbcharset}, "UTF-8");
-
   $self->set_tag_style('&lt;%', '%&gt;');
   $self->{quot_re} = '&quot;';
 
@@ -102,9 +229,13 @@ sub parse_block {
 
   while ($contents ne "") {
     if (substr($contents, 0, 1) eq "<") {
-      $contents =~ m|^<[^>]+>|;
-      my $tag = $&;
-      substr($contents, 0, length($&)) = "";
+      $contents =~ m|^(<[^>]+>)|;
+      my $tag = $1;
+      substr($contents, 0, length($1)) = "";
+
+      $self->{current_text_style} = $1 if $tag =~ m|text:style-name\s*=\s*"([^"]+)"|;
+
+      push @{ $self->{tag_stack} }, $tag;
 
       if ($tag =~ m|<table:table-row|) {
         $contents =~ m|^(.*?)(</table:table-row[^>]*>)|;
@@ -114,10 +245,10 @@ sub parse_block {
         if ($table_row =~ m|\&lt;\%foreachrow\s+(.*?)\%\&gt;|) {
           my $var = $1;
 
-          $contents =~ m|\&lt;\%foreachrow\s+.*?\%\&gt;|;
-          substr($contents, length($`), length($&)) = "";
+          $contents =~ m|^(.*?)(\&lt;\%foreachrow\s+.*?\%\&gt;)|;
+          substr($contents, length($1), length($2)) = "";
 
-          ($table_row, $contents) = $self->find_end($contents, length($`));
+          ($table_row, $contents) = $self->find_end($contents, length($1));
           if (!$table_row) {
             $self->{"error"} = "Unclosed <\%foreachrow\%>." unless ($self->{"error"});
             $main::lxdebug->leave_sub();
@@ -128,7 +259,7 @@ sub parse_block {
           $table_row .=  $1;
           $end_tag    =  $2;
 
-          substr $contents, 0, length($&), '';
+          substr $contents, 0, length($1) + length($2), '';
 
           my $new_text = $self->parse_foreach($var, $table_row, $tag, $end_tag, @indices);
           if (!defined($new_text)) {
@@ -151,9 +282,14 @@ sub parse_block {
         $new_contents .= $tag;
       }
 
+      if ($tag =~ m{^</ | />$}x) {
+        # $::lxdebug->message(0, "popping top tag is $tag top " . $self->{tag_stack}->[-1]);
+        pop @{ $self->{tag_stack} };
+      }
+
     } else {
-      $contents =~ /^[^<]+/;
-      my $text = $&;
+      $contents =~ /^([^<]+)/;
+      my $text = $1;
 
       my $pos_if = index($text, '&lt;%if');
       my $pos_foreach = index($text, '&lt;%foreach');
@@ -168,15 +304,15 @@ sub parse_block {
         $new_contents .= $self->substitute_vars(substr($contents, 0, $pos_foreach), @indices);
         substr($contents, 0, $pos_foreach) = "";
 
-        if ($contents !~ m|^\&lt;\%foreach (.*?)\%\&gt;|) {
+        if ($contents !~ m|^(\&lt;\%foreach (.*?)\%\&gt;)|) {
           $self->{"error"} = "Malformed <\%foreach\%>.";
           $main::lxdebug->leave_sub();
           return undef;
         }
 
-        my $var = $1;
+        my $var = $2;
 
-        substr($contents, 0, length($&)) = "";
+        substr($contents, 0, length($1)) = "";
 
         my $block;
         ($block, $contents) = $self->find_end($contents);
@@ -236,36 +372,40 @@ sub parse {
     return 0;
   }
 
-  my $rnd = $self->{"rnd"};
-  my $new_styles = qq|<style:style style:name="TLXO${rnd}BOLD" style:family="text">
-<style:text-properties fo:font-weight="bold" style:font-weight-asian="bold" style:font-weight-complex="bold"/>
-</style:style>
-<style:style style:name="TLXO${rnd}ITALIC" style:family="text">
-<style:text-properties fo:font-style="italic" style:font-style-asian="italic" style:font-style-complex="italic"/>
-</style:style>
-<style:style style:name="TLXO${rnd}UNDERLINE" style:family="text">
-<style:text-properties style:text-underline-style="solid" style:text-underline-width="auto" style:text-underline-color="font-color"/>
-</style:style>
-<style:style style:name="TLXO${rnd}STRIKETHROUGH" style:family="text">
-<style:text-properties style:text-line-through-style="solid"/>
-</style:style>
-<style:style style:name="TLXO${rnd}SUPER" style:family="text">
-<style:text-properties style:text-position="super 58%"/>
-</style:style>
-<style:style style:name="TLXO${rnd}SUB" style:family="text">
-<style:text-properties style:text-position="sub 58%"/>
-</style:style>
-|;
-
-  $contents =~ s|</office:automatic-styles>|${new_styles}</office:automatic-styles>|;
-  $contents =~ s|[\n\r]||gm;
-
-  my $new_contents = $self->parse_block($contents);
+  $self->{current_text_style} =  '';
+  $self->{used_list_styles}   =  {
+    itemize                   => {},
+    enumerate                 => {},
+  };
+
+  my $new_contents;
+  if ($self->{use_template_toolkit}) {
+    my $additional_params = $::form;
+
+    $::form->template->process(\$contents, $additional_params, \$new_contents) || die $::form->template->error;
+  } else {
+    $self->{tag_stack} = [];
+    $new_contents = $self->parse_block($contents);
+  }
   if (!defined($new_contents)) {
     $main::lxdebug->leave_sub();
     return 0;
   }
 
+  my $new_styles = SL::Template::OpenDocument::Styles->get_style('text_basic');
+
+  foreach my $type (qw(itemize enumerate)) {
+    foreach my $parent (sort { $a cmp $b } keys %{ $self->{used_list_styles}->{$type} }) {
+      $new_styles .= SL::Template::OpenDocument::Styles->get_style('text_list_item', TYPE => $type, PARENT => $parent)
+                   .  SL::Template::OpenDocument::Styles->get_style("list_${type}",  TYPE => $type, PARENT => $parent);
+    }
+  }
+
+  # $::lxdebug->dump(0, "new_Styles", $new_styles);
+
+  $new_contents =~ s|</office:automatic-styles>|${new_styles}</office:automatic-styles>|;
+  $new_contents =~ s|[\n\r]||gm;
+
 #   $new_contents =~ s|>|>\n|g;
 
   $zip->contents("content.xml", Encode::encode('utf-8-strict', $new_contents));
@@ -370,7 +510,9 @@ sub spawn_xvfb {
 
   $main::lxdebug->message(LXDebug->DEBUG2(), "  xauthority $xauthority\n");
 
-  system("xauth add \"${display}\" . \"${mcookie}\"");
+  if (system("xauth add \"${display}\" . \"${mcookie}\"") == -1) {
+    die "system call to xauth failed: $!";
+  }
   if ($? != 0) {
     $self->{"error"} = "Conversion to PDF failed because OpenOffice could not be started (xauth: $!)";
     $main::lxdebug->leave_sub();
@@ -416,11 +558,21 @@ sub spawn_xvfb {
   return $display;
 }
 
+sub _run_python_uno {
+  my ($self, @args) = @_;
+
+  local $ENV{PYTHONPATH};
+  $ENV{PYTHONPATH} = $::lx_office_conf{environment}->{python_uno_path} . ':' . $ENV{PYTHONPATH} if $::lx_office_conf{environment}->{python_uno_path};
+  my $cmd          = $::lx_office_conf{applications}->{python_uno} . ' ' . join(' ', @args);
+  return `$cmd`;
+}
+
 sub is_openoffice_running {
+  my ($self) = @_;
+
   $main::lxdebug->enter_sub();
 
-  my $cmd    = "./scripts/oo-uno-test-conn.py " . $::lx_office_conf{print_templates}->{openofficeorg_daemon_port} . " 2> /dev/null";
-  my $output = `$cmd`;
+  my $output = $self->_run_python_uno('./scripts/oo-uno-test-conn.py', $::lx_office_conf{print_templates}->{openofficeorg_daemon_port}, ' 2> /dev/null');
   chomp $output;
 
   my $res = ($? == 0) || $output;
@@ -447,10 +599,19 @@ sub spawn_openoffice {
       last;
     }
 
+    if ($::dispatcher->interface_type eq 'FastCGI') {
+      $::dispatcher->{request}->Detach;
+    }
+
     if (!$spawned_oo) {
       my $pid = fork();
       if (0 == $pid) {
         $main::lxdebug->message(LXDebug->DEBUG2(), "  Child daemonizing\n");
+
+        if ($::dispatcher->interface_type eq 'FastCGI') {
+          $::dispatcher->{request}->Finish;
+          $::dispatcher->{request}->LastCall;
+        }
         chdir('/');
         open(STDIN, '/dev/null');
         open(STDOUT, '>/dev/null');
@@ -464,6 +625,11 @@ sub spawn_openoffice {
                        "-accept=socket,host=localhost,port=" .
                        $::lx_office_conf{print_templates}->{openofficeorg_daemon_port} . ";urp;");
         exec(@cmdline);
+      } else {
+        # parent
+        if ($::dispatcher->interface_type eq 'FastCGI') {
+          $::dispatcher->{request}->Attach;
+        }
       }
 
       $main::lxdebug->message(LXDebug->DEBUG2(), "  Parent after fork\n");
@@ -508,27 +674,22 @@ sub convert_to_pdf {
     return 0;
   }
 
-  my @cmdline;
   if (!$::lx_office_conf{print_templates}->{openofficeorg_daemon}) {
-    @cmdline = ($::lx_office_conf{applications}->{openofficeorg_writer},
-                "-minimized", "-norestore", "-nologo", "-nolockcheck",
-                "-headless",
-                "file:${filename}.odt",
-                "macro://" . (split('/', $filename))[-1] .
-                "/Standard.Conversion.ConvertSelfToPDF()");
+    if (system($::lx_office_conf{applications}->{openofficeorg_writer},
+               "-minimized", "-norestore", "-nologo", "-nolockcheck", "-headless",
+               "file:${filename}.odt",
+               "macro://" . (split('/', $filename))[-1] . "/Standard.Conversion.ConvertSelfToPDF()") == -1) {
+      die "system call to $::lx_office_conf{applications}->{openofficeorg_writer} failed: $!";
+    }
   } else {
     if (!$self->spawn_openoffice()) {
       $main::lxdebug->leave_sub();
       return 0;
     }
 
-    @cmdline = ("./scripts/oo-uno-convert-pdf.py",
-                $::lx_office_conf{print_templates}->{openofficeorg_daemon_port},
-                "${filename}.odt");
+    $self->_run_python_uno('./scripts/oo-uno-convert-pdf.py', $::lx_office_conf{print_templates}->{openofficeorg_daemon_port}, "${filename}.odt");
   }
 
-  system(@cmdline);
-
   my $res = $?;
   if ((0 == $?) || (-f "${filename}.pdf" && -s "${filename}.pdf")) {
     $form->{"tmpfile"} =~ s/odt$/pdf/;
@@ -549,25 +710,14 @@ sub convert_to_pdf {
 }
 
 sub format_string {
-  my ($self, $variable) = @_;
-  my $form = $self->{"form"};
-  my $iconv = $self->{"iconv"};
-
-  $variable = $main::locale->quote_special_chars('Template/OpenDocument', $variable);
+  my ($self, $content, $variable) = @_;
 
-  # Allow some HTML markup to be converted into the output format's
-  # corresponding markup code, e.g. bold or italic.
-  my $rnd = $self->{"rnd"};
-  my %markup_replace = ("b" => "BOLD", "i" => "ITALIC", "s" => "STRIKETHROUGH",
-                        "u" => "UNDERLINE", "sup" => "SUPER", "sub" => "SUB");
-
-  foreach my $key (keys(%markup_replace)) {
-    my $value = $markup_replace{$key};
-    $variable =~ s|\&lt;${key}\&gt;|<text:span text:style-name=\"TLXO${rnd}${value}\">|gi; #"
-    $variable =~ s|\&lt;/${key}\&gt;|</text:span>|gi;
-  }
+  my $formatter =
+       $formatters{ $self->{variable_content_types}->{$variable} }
+    // $formatters{ $self->{default_content_type} }
+    // $formatters{ text };
 
-  return $iconv->convert($variable);
+  return $formatter->($self, $content, variable => $variable);
 }
 
 sub get_mime_type() {