66d8f83ac3f5bbae8924020e4bb1f64cf5eeba41
[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 HTML::Entities;
8 use POSIX 'setsid';
9 use XML::LibXML;
10
11 use SL::Iconv;
12 use SL::Template::OpenDocument::Styles;
13
14 use SL::DB::BankAccount;
15 use SL::Helper::QrBill;
16 use SL::Helper::ISO3166;
17
18 use Cwd;
19 # use File::Copy;
20 # use File::Spec;
21 # use File::Temp qw(:mktemp);
22 use IO::File;
23 use List::Util qw(first);
24
25 use strict;
26
27 my %text_markup_replace = (
28   b   => "BOLD",
29   i   => "ITALIC",
30   s   => "STRIKETHROUGH",
31   u   => "UNDERLINE",
32   sup => "SUPER",
33   sub => "SUB",
34 );
35
36 sub _format_text {
37   my ($self, $content, %params) = @_;
38
39   $content = $::locale->quote_special_chars('Template/OpenDocument', $content);
40
41   # Allow some HTML markup to be converted into the output format's
42   # corresponding markup code, e.g. bold or italic.
43   foreach my $key (keys(%text_markup_replace)) {
44     my $value = $text_markup_replace{$key};
45     $content =~ s|\&lt;${key}\&gt;|<text:span text:style-name=\"TKIVITENDO${value}\">|gi; #"
46     $content =~ s|\&lt;/${key}\&gt;|</text:span>|gi;
47   }
48
49   return $content;
50 }
51
52 my %html_replace = (
53   '</ul>'     => '</text:list>',
54   '</ol>'     => '</text:list>',
55   '</li>'     => '</text:p></text:list-item>',
56   '<b>'       => '<text:span text:style-name="TKIVITENDOBOLD">',
57   '</b>'      => '</text:span>',
58   '<strong>'  => '<text:span text:style-name="TKIVITENDOBOLD">',
59   '</strong>' => '</text:span>',
60   '<i>'       => '<text:span text:style-name="TKIVITENDOITALIC">',
61   '</i>'      => '</text:span>',
62   '<em>'      => '<text:span text:style-name="TKIVITENDOITALIC">',
63   '</em>'     => '</text:span>',
64   '<u>'       => '<text:span text:style-name="TKIVITENDOUNDERLINE">',
65   '</u>'      => '</text:span>',
66   '<s>'       => '<text:span text:style-name="TKIVITENDOSTRIKETHROUGH">',
67   '</s>'      => '</text:span>',
68   '<sub>'     => '<text:span text:style-name="TKIVITENDOSUB">',
69   '</sub>'    => '</text:span>',
70   '<sup>'     => '<text:span text:style-name="TKIVITENDOSUPER">',
71   '</sup>'    => '</text:span>',
72   '<br/>'     => '<text:line-break/>',
73   '<br>'      => '<text:line-break/>',
74 );
75
76 sub _format_html {
77   my ($self, $content, %params) = @_;
78
79   my $in_p        = 0;
80   my $p_start_tag = qq|<text:p text:style-name="@{[ $self->{current_text_style} ]}">|;
81   my $prefix      = '';
82   my $suffix      = '';
83
84   my (@tags_to_open, @tags_to_close);
85   for (my $idx = scalar(@{ $self->{tag_stack} }) - 1; $idx >= 0; --$idx) {
86     my $tag = $self->{tag_stack}->[$idx];
87
88     next if $tag =~ m{/>$};
89     last if $tag =~ m{^<table};
90
91     if ($tag =~ m{^<text:p}) {
92       $in_p        = 1;
93       $p_start_tag = $tag;
94       last;
95
96     } else {
97       $suffix  =  "${tag}${suffix}";
98       $tag     =~ s{ .*>}{>};
99       $prefix .=  '</' . substr($tag, 1);
100     }
101   }
102
103   $content            =~ s{ ^<p> | </p>$ }{}gx if $in_p;
104   $content            =~ s{ \r+ }{}gx;
105   $content            =~ s{ \n+ }{ }gx;
106   $content            =~ s{ (?:\&nbsp;|\s)+ }{ }gx;
107
108   my $ul_start_tag    = qq|<text:list xml:id="list@{[ int rand(9999999999999999) ]}" text:style-name="LKIVITENDOitemize@{[ $self->{current_text_style} ]}">|;
109   my $ol_start_tag    = qq|<text:list xml:id="list@{[ int rand(9999999999999999) ]}" text:style-name="LKIVITENDOenumerate@{[ $self->{current_text_style} ]}">|;
110   my $ul_li_start_tag = qq|<text:list-item><text:p text:style-name="PKIVITENDOitemize@{[ $self->{current_text_style} ]}">|;
111   my $ol_li_start_tag = qq|<text:list-item><text:p text:style-name="PKIVITENDOenumerate@{[ $self->{current_text_style} ]}">|;
112
113   my @parts = map {
114     if (substr($_, 0, 1) eq '<') {
115       s{ +}{}g;
116       if ($_ eq '</p>') {
117         $in_p--;
118         $in_p == 0 ? '</text:p>' : '';
119
120       } elsif ($_ eq '<p>') {
121         $in_p++;
122         $in_p == 1 ? $p_start_tag : '';
123
124       } elsif ($_ eq '<ul>') {
125         $self->{used_list_styles}->{itemize}->{$self->{current_text_style}}   = 1;
126         $html_replace{'<li>'}                                                 = $ul_li_start_tag;
127         $ul_start_tag;
128
129       } elsif ($_ eq '<ol>') {
130         $self->{used_list_styles}->{enumerate}->{$self->{current_text_style}} = 1;
131         $html_replace{'<li>'}                                                 = $ol_li_start_tag;
132         $ol_start_tag;
133
134       } else {
135         $html_replace{$_} || '';
136       }
137
138     } else {
139       $::locale->quote_special_chars('Template/OpenDocument', HTML::Entities::decode_entities($_));
140     }
141   } split(m{(<.*?>)}x, $content);
142
143   my $out  = join('', $prefix, @parts, $suffix);
144
145   # $::lxdebug->dump(0, "prefix parts suffix", [ $prefix, join('', @parts), $suffix ]);
146
147   return $out;
148 }
149
150 my %formatters = (
151   html => \&_format_html,
152   text => \&_format_text,
153 );
154
155 sub new {
156   my $type = shift;
157
158   my $self = $type->SUPER::new(@_);
159
160   $self->set_tag_style('&lt;%', '%&gt;');
161   $self->{quot_re} = '&quot;';
162
163   return $self;
164 }
165
166 sub parse_foreach {
167   my ($self, $var, $text, $start_tag, $end_tag, @indices) = @_;
168
169   my ($form, $new_contents) = ($self->{"form"}, "");
170
171   my $ary = $self->_get_loop_variable($var, 1, @indices);
172
173   for (my $i = 0; $i < scalar(@{$ary || []}); $i++) {
174     $form->{"__first__"} = $i == 0;
175     $form->{"__last__"} = ($i + 1) == scalar(@{$ary});
176     $form->{"__odd__"} = (($i + 1) % 2) == 1;
177     $form->{"__counter__"} = $i + 1;
178     my $new_text = $self->parse_block($text, (@indices, $i));
179     return undef unless (defined($new_text));
180     $new_contents .= $start_tag . $new_text . $end_tag;
181   }
182   map({ delete($form->{"__${_}__"}); } qw(first last odd counter));
183
184   return $new_contents;
185 }
186
187 sub find_end {
188   my ($self, $text, $pos, $var, $not) = @_;
189
190   my $depth = 1;
191   $pos = 0 unless ($pos);
192
193   while ($pos < length($text)) {
194     $pos++;
195
196     next if (substr($text, $pos - 1, 5) ne '&lt;%');
197
198     if ((substr($text, $pos + 4, 2) eq 'if') || (substr($text, $pos + 4, 3) eq 'for')) {
199       $depth++;
200
201     } elsif ((substr($text, $pos + 4, 4) eq 'else') && (1 == $depth)) {
202       if (!$var) {
203         $self->{"error"} = '<%else%> outside of <%if%> / <%ifnot%>.';
204         return undef;
205       }
206
207       my $block = substr($text, 0, $pos - 1);
208       substr($text, 0, $pos - 1) = "";
209       $text =~ s!^\&lt;\%[^\%]+\%\&gt;!!;
210       $text = '&lt;%if' . ($not ?  " " : "not ") . $var . '%&gt;' . $text;
211
212       return ($block, $text);
213
214     } elsif (substr($text, $pos + 4, 3) eq 'end') {
215       $depth--;
216       if ($depth == 0) {
217         my $block = substr($text, 0, $pos - 1);
218         substr($text, 0, $pos - 1) = "";
219         $text =~ s!^\&lt;\%[^\%]+\%\&gt;!!;
220
221         return ($block, $text);
222       }
223     }
224   }
225
226   return undef;
227 }
228
229 sub parse_block {
230   $main::lxdebug->enter_sub();
231
232   my ($self, $contents, @indices) = @_;
233
234   my $new_contents = "";
235
236   while ($contents ne "") {
237     if (substr($contents, 0, 1) eq "<") {
238       $contents =~ m|^(<[^>]+>)|;
239       my $tag = $1;
240       substr($contents, 0, length($1)) = "";
241
242       $self->{current_text_style} = $1 if $tag =~ m|text:style-name\s*=\s*"([^"]+)"|;
243
244       push @{ $self->{tag_stack} }, $tag;
245
246       if ($tag =~ m|<table:table-row|) {
247         $contents =~ m|^(.*?)(</table:table-row[^>]*>)|;
248         my $table_row = $1;
249         my $end_tag = $2;
250
251         if ($table_row =~ m|\&lt;\%foreachrow\s+(.*?)\%\&gt;|) {
252           my $var = $1;
253
254           $contents =~ m|^(.*?)(\&lt;\%foreachrow\s+.*?\%\&gt;)|;
255           substr($contents, length($1), length($2)) = "";
256
257           ($table_row, $contents) = $self->find_end($contents, length($1));
258           if (!$table_row) {
259             $self->{"error"} = "Unclosed <\%foreachrow\%>." unless ($self->{"error"});
260             $main::lxdebug->leave_sub();
261             return undef;
262           }
263
264           $contents   =~ m|^(.*?)(</table:table-row[^>]*>)|;
265           $table_row .=  $1;
266           $end_tag    =  $2;
267
268           substr $contents, 0, length($1) + length($2), '';
269
270           my $new_text = $self->parse_foreach($var, $table_row, $tag, $end_tag, @indices);
271           if (!defined($new_text)) {
272             $main::lxdebug->leave_sub();
273             return undef;
274           }
275           $new_contents .= $new_text;
276
277         } else {
278           substr($contents, 0, length($table_row) + length($end_tag)) = "";
279           my $new_text = $self->parse_block($table_row, @indices);
280           if (!defined($new_text)) {
281             $main::lxdebug->leave_sub();
282             return undef;
283           }
284           $new_contents .= $tag . $new_text . $end_tag;
285         }
286
287       } else {
288         $new_contents .= $tag;
289       }
290
291       if ($tag =~ m{^</ | />$}x) {
292         # $::lxdebug->message(0, "popping top tag is $tag top " . $self->{tag_stack}->[-1]);
293         pop @{ $self->{tag_stack} };
294       }
295
296     } else {
297       $contents =~ /^([^<]+)/;
298       my $text = $1;
299
300       my $pos_if = index($text, '&lt;%if');
301       my $pos_foreach = index($text, '&lt;%foreach');
302
303       if ((-1 == $pos_if) && (-1 == $pos_foreach)) {
304         substr($contents, 0, length($text)) = "";
305         $new_contents .= $self->substitute_vars($text, @indices);
306         next;
307       }
308
309       if ((-1 == $pos_if) || ((-1 != $pos_foreach) && ($pos_if > $pos_foreach))) {
310         $new_contents .= $self->substitute_vars(substr($contents, 0, $pos_foreach), @indices);
311         substr($contents, 0, $pos_foreach) = "";
312
313         if ($contents !~ m|^(\&lt;\%foreach (.*?)\%\&gt;)|) {
314           $self->{"error"} = "Malformed <\%foreach\%>.";
315           $main::lxdebug->leave_sub();
316           return undef;
317         }
318
319         my $var = $2;
320
321         substr($contents, 0, length($1)) = "";
322
323         my $block;
324         ($block, $contents) = $self->find_end($contents);
325         if (!$block) {
326           $self->{"error"} = "Unclosed <\%foreach\%>." unless ($self->{"error"});
327           $main::lxdebug->leave_sub();
328           return undef;
329         }
330
331         my $new_text = $self->parse_foreach($var, $block, "", "", @indices);
332         if (!defined($new_text)) {
333           $main::lxdebug->leave_sub();
334           return undef;
335         }
336         $new_contents .= $new_text;
337
338       } else {
339         if (!$self->_parse_block_if(\$contents, \$new_contents, $pos_if, @indices)) {
340           $main::lxdebug->leave_sub();
341           return undef;
342         }
343       }
344     }
345   }
346
347   $main::lxdebug->leave_sub();
348
349   return $new_contents;
350 }
351
352 sub parse {
353   $main::lxdebug->enter_sub();
354   my $self = $_[0];
355
356   local *OUT = $_[1];
357   my $form = $self->{"form"};
358
359   close(OUT);
360
361   my $qr_image_path;
362   if ($::instance_conf->get_create_qrbill_invoices && $form->{formname} eq 'invoice') {
363     # the biller account information, biller address and the reference number,
364     # are needed in the template aswell as in the qr-code generation, therefore
365     # assemble these and add to $::form
366     $qr_image_path = $self->generate_qr_code;
367   }
368
369   my $file_name;
370   if ($form->{"IN"} =~ m|^/|) {
371     $file_name = $form->{"IN"};
372   } else {
373     $file_name = $form->{"templates"} . "/" . $form->{"IN"};
374   }
375
376   my $zip = Archive::Zip->new();
377   if (Archive::Zip->AZ_OK != $zip->read($file_name)) {
378     $self->{"error"} = "File not found/is not a OpenDocument file.";
379     $main::lxdebug->leave_sub();
380     return 0;
381   }
382
383   my $contents = Encode::decode('utf-8-strict', $zip->contents("content.xml"));
384   if (!$contents) {
385     $self->{"error"} = "File is not a OpenDocument file.";
386     $main::lxdebug->leave_sub();
387     return 0;
388   }
389
390   $self->{current_text_style} =  '';
391   $self->{used_list_styles}   =  {
392     itemize                   => {},
393     enumerate                 => {},
394   };
395
396   my $new_contents;
397   if ($self->{use_template_toolkit}) {
398     my $additional_params = $::form;
399
400     $::form->template->process(\$contents, $additional_params, \$new_contents) || die $::form->template->error;
401   } else {
402     $self->{tag_stack} = [];
403     $new_contents = $self->parse_block($contents);
404   }
405   if (!defined($new_contents)) {
406     $main::lxdebug->leave_sub();
407     return 0;
408   }
409
410   my $new_styles = SL::Template::OpenDocument::Styles->get_style('text_basic');
411
412   foreach my $type (qw(itemize enumerate)) {
413     foreach my $parent (sort { $a cmp $b } keys %{ $self->{used_list_styles}->{$type} }) {
414       $new_styles .= SL::Template::OpenDocument::Styles->get_style('text_list_item', TYPE => $type, PARENT => $parent)
415                    .  SL::Template::OpenDocument::Styles->get_style("list_${type}",  TYPE => $type, PARENT => $parent);
416     }
417   }
418
419   # $::lxdebug->dump(0, "new_Styles", $new_styles);
420
421   $new_contents =~ s|</office:automatic-styles>|${new_styles}</office:automatic-styles>|;
422   $new_contents =~ s|[\n\r]||gm;
423
424 #   $new_contents =~ s|>|>\n|g;
425
426   $zip->contents("content.xml", Encode::encode('utf-8-strict', $new_contents));
427
428   my $styles = Encode::decode('utf-8-strict', $zip->contents("styles.xml"));
429   if ($contents) {
430     my $new_styles = $self->parse_block($styles);
431     if (!defined($new_contents)) {
432       $main::lxdebug->leave_sub();
433       return 0;
434     }
435     $zip->contents("styles.xml", Encode::encode('utf-8-strict', $new_styles));
436   }
437
438   if ($::instance_conf->get_create_qrbill_invoices && $form->{formname} eq 'invoice') {
439     # get placeholder path from odt XML
440     my $qr_placeholder_path;
441     my $dom = XML::LibXML->load_xml(string => $contents);
442     my @nodelist = $dom->getElementsByTagName("draw:frame");
443     for my $node (@nodelist) {
444       my $attr = $node->getAttribute('draw:name');
445       if ($attr eq 'QRCodePlaceholder') {
446         my @children = $node->getChildrenByTagName('draw:image');
447         $qr_placeholder_path = $children[0]->getAttribute('xlink:href');
448       }
449     }
450     if (!defined($qr_placeholder_path)) {
451       $::form->error($::locale->text('QR-Code placeholder image: QRCodePlaceholder not found in template.'));
452     }
453     # replace QR-Code Placeholder Image in zip file (odt) with generated one
454     $zip->updateMember(
455      $qr_placeholder_path,
456      $qr_image_path
457     );
458   }
459
460   $zip->writeToFileNamed($form->{"tmpfile"}, 1);
461
462   my $res = 1;
463   if ($form->{"format"} =~ /pdf/) {
464     $res = $self->convert_to_pdf();
465   }
466
467   $main::lxdebug->leave_sub();
468   return $res;
469 }
470
471 sub get_qrbill_account {
472   $main::lxdebug->enter_sub();
473   my ($self) = @_;
474
475   my $qr_account;
476
477   my $bank_accounts     = SL::DB::Manager::BankAccount->get_all;
478   $qr_account = scalar(@{ $bank_accounts }) == 1 ?
479     $bank_accounts->[0] :
480     first { $_->use_for_qrbill } @{ $bank_accounts };
481
482   if (!$qr_account) {
483     $::form->error($::locale->text('No bank account flagged for QRBill usage was found.'));
484   }
485
486   $main::lxdebug->leave_sub();
487   return $qr_account;
488 }
489
490 sub remove_letters_prefix {
491   my $s = $_[0];
492   $s =~ s/^[a-zA-Z]+//;
493   return $s;
494 }
495
496 sub check_digits_and_max_length {
497   my $s = $_[0];
498   my $length = $_[1];
499
500   return 0 if (!($s =~ /^\d*$/) || length($s) > $length);
501   return 1;
502 }
503
504 sub calculate_check_digit {
505   # calculate ESR check digit using algorithm: "modulo 10, recursive"
506   my $ref_number_str = $_[0];
507
508   my @m = (0, 9, 4, 6, 8, 2, 7, 1, 3, 5);
509   my $carry = 0;
510
511   my @ref_number_split = map int($_), split(//, $ref_number_str);
512
513   for my $v (@ref_number_split) {
514     $carry = @m[($carry + $v) % 10];
515   }
516
517   return (10 - $carry) % 10;
518 }
519
520 sub assemble_ref_number {
521   $main::lxdebug->enter_sub();
522
523   my $bank_id = $_[0];
524   my $customer_number = $_[1];
525   my $order_number = $_[2] // "0";
526   my $invoice_number = $_[3] // "0";
527
528   # check values (analog to checks in makro)
529   # - bank_id
530   #     input: 6 digits, only numbers
531   #     output: 6 digits, only numbers
532   if (!($bank_id =~ /^\d*$/) || length($bank_id) != 6) {
533     $::form->error($::locale->text('Bank account id number invalid. Must be 6 digits.'));
534   }
535
536   # - customer_number
537   #     input: prefix (letters) + up to 6 digits (numbers)
538   #     output: prefix removed, 6 digits, filled with leading zeros
539   $customer_number = remove_letters_prefix($customer_number);
540   if (!check_digits_and_max_length($customer_number, 6)) {
541     $::form->error($::locale->text('Customer number invalid. Must be less then or equal to 6 digits after prefix.'));
542   }
543   # fill with zeros
544   $customer_number = sprintf "%06d", $customer_number;
545
546   # - order_number
547   #     input: prefix (letters) + up to 7 digits, may be zero
548   #     output: prefix removed, 7 digits, filled with leading zeros
549   $order_number = remove_letters_prefix($order_number);
550   if (!check_digits_and_max_length($order_number, 7)) {
551     $::form->error($::locale->text('Order number invalid. Must be less then or equal to 7 digits after prefix.'));
552   }
553   # fill with zeros
554   $order_number = sprintf "%07d", $order_number;
555
556   # - invoice_number
557   #     input: prefix (letters) + up to 7 digits, may be zero
558   #     output: prefix removed, 7 digits, filled with leading zeros
559   $invoice_number = remove_letters_prefix($invoice_number);
560   if (!check_digits_and_max_length($invoice_number, 7)) {
561     $::form->error($::locale->text('Invoice number invalid. Must be less then or equal to 7 digits after prefix.'));
562   }
563   # fill with zeros
564   $invoice_number = sprintf "%07d", $invoice_number;
565
566   # assemble ref. number
567   my $ref_number = $bank_id . $customer_number . $order_number . $invoice_number;
568
569   # calculate check digit
570   my $ref_number_cpl = $ref_number . calculate_check_digit($ref_number);
571
572   $main::lxdebug->leave_sub();
573   return $ref_number_cpl;
574 }
575
576 sub get_ref_number_formatted {
577   $main::lxdebug->enter_sub();
578
579   my $ref_number = $_[0];
580
581   # create ref. number in format:
582   # 'XX XXXXX XXXXX XXXXX XXXXX XXXXX' (2 digits + 5 x 5 digits)
583   my $ref_number_spaced = substr($ref_number, 0, 2) . ' ' .
584                           substr($ref_number, 2, 5) . ' ' .
585                           substr($ref_number, 7, 5) . ' ' .
586                           substr($ref_number, 12, 5) . ' ' .
587                           substr($ref_number, 17, 5) . ' ' .
588                           substr($ref_number, 22, 5);
589
590   $main::lxdebug->leave_sub();
591   return $ref_number_spaced;
592 }
593
594 sub get_iban_formatted {
595   $main::lxdebug->enter_sub();
596
597   my $iban = $_[0];
598
599   # create iban number in format:
600   # 'XXXX XXXX XXXX XXXX XXXX X' (5 x 4 + 1digits)
601   my $iban_spaced = substr($iban, 0, 4) . ' ' .
602                     substr($iban, 4, 4) . ' ' .
603                     substr($iban, 8, 4) . ' ' .
604                     substr($iban, 12, 4) . ' ' .
605                     substr($iban, 16, 4) . ' ' .
606                     substr($iban, 20, 1);
607
608   $main::lxdebug->leave_sub();
609   return $iban_spaced;
610 }
611
612 sub get_amount_formatted {
613   $main::lxdebug->enter_sub();
614
615   unless ($_[0] =~ /^\d+\.\d{2}$/) {
616     $::form->error($::locale->text('Amount has wrong format.'));
617   }
618
619   local $_ = shift;
620   $_ = reverse split //;
621   m/^\d{2}\./g;
622   s/\G(\d{3})(?=\d)/$1 /g;
623
624   $main::lxdebug->leave_sub();
625   return scalar reverse split //;
626 }
627
628 sub generate_qr_code {
629   $main::lxdebug->enter_sub();
630   my $self = $_[0];
631   my $form = $self->{"form"};
632
633   # assemble data for QR-Code
634
635   # get qr-account data
636   my $qr_account = $self->get_qrbill_account();
637
638   my %biller_information = (
639     'iban' => $qr_account->{'iban'}
640   );
641
642   my $biller_countrycode = SL::Helper::ISO3166::map_name_to_alpha_2_code(
643     $::instance_conf->get_address_country()
644   );
645   if (!$biller_countrycode) {
646     $::form->error($::locale->text('Error mapping biller countrycode.'));
647   }
648   my %biller_data = (
649     'address_type' => 'K',
650     'company' => $::instance_conf->get_company(),
651     'address_row1' => $::instance_conf->get_address_street1(),
652     'address_row2' => $::instance_conf->get_address_zipcode() . ' ' . $::instance_conf->get_address_city(),
653     'countrycode' => $biller_countrycode,
654   );
655
656   my $amount;
657   if ($form->{'qrbill_without_amount'}) {
658     $amount = '';
659   } else {
660     $amount = sprintf("%.2f", $form->parse_amount(\%::myconfig, $form->{'total'}));
661   }
662
663   my %payment_information = (
664     'amount' => $amount,
665     'currency' => $form->{'currency'},
666   );
667
668   my $customer_countrycode = SL::Helper::ISO3166::map_name_to_alpha_2_code($form->{'country'});
669   if (!$customer_countrycode) {
670     $::form->error($::locale->text('Error mapping customer countrycode.'));
671   }
672   my %invoice_recipient_data = (
673     'address_type' => 'K',
674     'name' => $form->{'name'},
675     'address_row1' => $form->{'street'},
676     'address_row2' => $form->{'zipcode'} . ' ' . $form->{'city'},
677     'countrycode' => $customer_countrycode,
678   );
679
680   my %ref_nr_data;
681   if ($::instance_conf->get_create_qrbill_invoices == 1) {
682     # generate ref.-no. with check digit
683     my $ref_number = assemble_ref_number(
684       $qr_account->{'bank_account_id'},
685       $form->{'customernumber'},
686       $form->{'ordnumber'},
687       $form->{'invnumber'},
688     );
689     %ref_nr_data = (
690       'type' => 'QRR',
691       'ref_number' => $ref_number,
692     );
693     # get ref. number/iban formatted with spaces and set into form for template
694     # processing
695     $form->{'ref_number'} = $ref_number;
696     $form->{'ref_number_formatted'} = get_ref_number_formatted($ref_number);
697   } elsif ($::instance_conf->get_create_qrbill_invoices == 2) {
698     %ref_nr_data = (
699       'type' => 'NON',
700       'ref_number' => '',
701     );
702   } else {
703     $::form->error($::locale->text('Error getting QR-Bill type.'));
704   }
705
706   # set into form for template processing
707   $form->{'biller_information'} = \%biller_information;
708   $form->{'biller_data'} = \%biller_data;
709   $form->{'iban_formatted'} = get_iban_formatted($qr_account->{'iban'});
710
711   # format amount for template
712   $form->{'amount_formatted'} = get_amount_formatted(
713     sprintf(
714       "%.2f",
715       $form->parse_amount(\%::myconfig, $form->{'total'})
716     )
717   );
718
719   # set outfile
720   my $outfile = $form->{"tmpdir"} . '/' . 'qr-code.png';
721
722   # generate QR-Code Image
723   eval {
724    my $qr_image = SL::Helper::QrBill->new(
725      \%biller_information,
726      \%biller_data,
727      \%payment_information,
728      \%invoice_recipient_data,
729      \%ref_nr_data,
730    );
731    $qr_image->generate($outfile);
732   } or do {
733    local $_ = $@; chomp; my $error = $_;
734    $::form->error($::locale->text('QR-Image generation failed: ' . $error));
735   };
736
737   $main::lxdebug->leave_sub();
738   return $outfile;
739 }
740
741 sub is_xvfb_running {
742   $main::lxdebug->enter_sub();
743
744   my ($self) = @_;
745
746   local *IN;
747   my $dfname = $self->{"userspath"} . "/xvfb_display";
748   my $display;
749
750   $main::lxdebug->message(LXDebug->DEBUG2(), "    Looking for $dfname\n");
751   if ((-f $dfname) && open(IN, $dfname)) {
752     my $pid = <IN>;
753     chomp($pid);
754     $display = <IN>;
755     chomp($display);
756     my $xauthority = <IN>;
757     chomp($xauthority);
758     close(IN);
759
760     $main::lxdebug->message(LXDebug->DEBUG2(), "      found with $pid and $display\n");
761
762     if ((! -d "/proc/$pid") || !open(IN, "/proc/$pid/cmdline")) {
763       $main::lxdebug->message(LXDebug->DEBUG2(), "  no/wrong process #1\n");
764       unlink($dfname, $xauthority);
765       $main::lxdebug->leave_sub();
766       return undef;
767     }
768     my $line = <IN>;
769     close(IN);
770     if ($line !~ /xvfb/i) {
771       $main::lxdebug->message(LXDebug->DEBUG2(), "      no/wrong process #2\n");
772       unlink($dfname, $xauthority);
773       $main::lxdebug->leave_sub();
774       return undef;
775     }
776
777     $ENV{"XAUTHORITY"} = $xauthority;
778     $ENV{"DISPLAY"} = $display;
779   } else {
780     $main::lxdebug->message(LXDebug->DEBUG2(), "      not found\n");
781   }
782
783   $main::lxdebug->leave_sub();
784
785   return $display;
786 }
787
788 sub spawn_xvfb {
789   $main::lxdebug->enter_sub();
790
791   my ($self) = @_;
792
793   $main::lxdebug->message(LXDebug->DEBUG2, "spawn_xvfb()\n");
794
795   my $display = $self->is_xvfb_running();
796
797   if ($display) {
798     $main::lxdebug->leave_sub();
799     return $display;
800   }
801
802   $display = 99;
803   while ( -f "/tmp/.X${display}-lock") {
804     $display++;
805   }
806   $display = ":${display}";
807   $main::lxdebug->message(LXDebug->DEBUG2(), "  display $display\n");
808
809   my $mcookie = `mcookie`;
810   die("Installation error: mcookie not found.") if ($? != 0);
811   chomp($mcookie);
812
813   $main::lxdebug->message(LXDebug->DEBUG2(), "  mcookie $mcookie\n");
814
815   my $xauthority = "/tmp/.Xauthority-" . $$ . "-" . time() . "-" . int(rand(9999999));
816   $ENV{"XAUTHORITY"} = $xauthority;
817
818   $main::lxdebug->message(LXDebug->DEBUG2(), "  xauthority $xauthority\n");
819
820   if (system("xauth add \"${display}\" . \"${mcookie}\"") == -1) {
821     die "system call to xauth failed: $!";
822   }
823   if ($? != 0) {
824     $self->{"error"} = "Conversion to PDF failed because OpenOffice could not be started (xauth: $!)";
825     $main::lxdebug->leave_sub();
826     return undef;
827   }
828
829   $main::lxdebug->message(LXDebug->DEBUG2(), "  about to fork()\n");
830
831   my $pid = fork();
832   if (0 == $pid) {
833     $main::lxdebug->message(LXDebug->DEBUG2(), "  Child execing\n");
834     exec($::lx_office_conf{applications}->{xvfb}, $display, "-screen", "0", "640x480x8", "-nolisten", "tcp");
835   }
836   sleep(3);
837   $main::lxdebug->message(LXDebug->DEBUG2(), "  parent dont sleeping\n");
838
839   local *OUT;
840   my $dfname = $self->{"userspath"} . "/xvfb_display";
841   if (!open(OUT, ">", $dfname)) {
842     $self->{"error"} = "Conversion to PDF failed because OpenOffice could not be started ($dfname: $!)";
843     unlink($xauthority);
844     kill($pid);
845     $main::lxdebug->leave_sub();
846     return undef;
847   }
848   print(OUT "$pid\n$display\n$xauthority\n");
849   close(OUT);
850
851   $main::lxdebug->message(LXDebug->DEBUG2(), "  parent re-testing\n");
852
853   if (!$self->is_xvfb_running()) {
854     $self->{"error"} = "Conversion to PDF failed because OpenOffice could not be started.";
855     unlink($xauthority, $dfname);
856     kill($pid);
857     $main::lxdebug->leave_sub();
858     return undef;
859   }
860
861   $main::lxdebug->message(LXDebug->DEBUG2(), "  spawn OK\n");
862
863   $main::lxdebug->leave_sub();
864
865   return $display;
866 }
867
868 sub _run_python_uno {
869   my ($self, @args) = @_;
870
871   local $ENV{PYTHONPATH};
872   $ENV{PYTHONPATH} = $::lx_office_conf{environment}->{python_uno_path} . ':' . $ENV{PYTHONPATH} if $::lx_office_conf{environment}->{python_uno_path};
873   my $cmd          = $::lx_office_conf{applications}->{python_uno} . ' ' . join(' ', @args);
874   return `$cmd`;
875 }
876
877 sub is_openoffice_running {
878   my ($self) = @_;
879
880   $main::lxdebug->enter_sub();
881
882   my $output = $self->_run_python_uno('./scripts/oo-uno-test-conn.py', $::lx_office_conf{print_templates}->{openofficeorg_daemon_port}, ' 2> /dev/null');
883   chomp $output;
884
885   my $res = ($? == 0) || $output;
886   $main::lxdebug->message(LXDebug->DEBUG2(), "  is_openoffice_running(): res $res\n");
887
888   $main::lxdebug->leave_sub();
889
890   return $res;
891 }
892
893 sub spawn_openoffice {
894   $main::lxdebug->enter_sub();
895
896   my ($self) = @_;
897
898   $main::lxdebug->message(LXDebug->DEBUG2(), "spawn_openoffice()\n");
899
900   my ($try, $spawned_oo, $res);
901
902   $res = 0;
903   for ($try = 0; $try < 15; $try++) {
904     if ($self->is_openoffice_running()) {
905       $res = 1;
906       last;
907     }
908
909     if ($::dispatcher->interface_type eq 'FastCGI') {
910       $::dispatcher->{request}->Detach;
911     }
912
913     if (!$spawned_oo) {
914       my $pid = fork();
915       if (0 == $pid) {
916         $main::lxdebug->message(LXDebug->DEBUG2(), "  Child daemonizing\n");
917
918         if ($::dispatcher->interface_type eq 'FastCGI') {
919           $::dispatcher->{request}->Finish;
920           $::dispatcher->{request}->LastCall;
921         }
922         chdir('/');
923         open(STDIN, '/dev/null');
924         open(STDOUT, '>/dev/null');
925         my $new_pid = fork();
926         exit if ($new_pid);
927         my $ssres = setsid();
928         $main::lxdebug->message(LXDebug->DEBUG2(), "  Child execing\n");
929         my @cmdline = ($::lx_office_conf{applications}->{openofficeorg_writer},
930                        "-minimized", "-norestore", "-nologo", "-nolockcheck",
931                        "-headless",
932                        "-accept=socket,host=localhost,port=" .
933                        $::lx_office_conf{print_templates}->{openofficeorg_daemon_port} . ";urp;");
934         exec(@cmdline);
935       } else {
936         # parent
937         if ($::dispatcher->interface_type eq 'FastCGI') {
938           $::dispatcher->{request}->Attach;
939         }
940       }
941
942       $main::lxdebug->message(LXDebug->DEBUG2(), "  Parent after fork\n");
943       $spawned_oo = 1;
944       sleep(3);
945     }
946
947     sleep($try >= 5 ? 2 : 1);
948   }
949
950   if (!$res) {
951     $self->{"error"} = "Conversion from OpenDocument to PDF failed because " .
952       "OpenOffice could not be started.";
953   }
954
955   $main::lxdebug->leave_sub();
956
957   return $res;
958 }
959
960 sub convert_to_pdf {
961   $main::lxdebug->enter_sub();
962
963   my ($self) = @_;
964
965   my $form = $self->{"form"};
966
967   my $filename = $form->{"tmpfile"};
968   $filename =~ s/.odt$//;
969   if (substr($filename, 0, 1) ne "/") {
970     $filename = getcwd() . "/${filename}";
971   }
972
973   if (substr($self->{"userspath"}, 0, 1) eq "/") {
974     $ENV{'HOME'} = $self->{"userspath"};
975   } else {
976     $ENV{'HOME'} = getcwd() . "/" . $self->{"userspath"};
977   }
978
979   if (!$self->spawn_xvfb()) {
980     $main::lxdebug->leave_sub();
981     return 0;
982   }
983
984   if (!$::lx_office_conf{print_templates}->{openofficeorg_daemon}) {
985     if (system($::lx_office_conf{applications}->{openofficeorg_writer},
986                "-minimized", "-norestore", "-nologo", "-nolockcheck", "-headless",
987                "file:${filename}.odt",
988                "macro://" . (split('/', $filename))[-1] . "/Standard.Conversion.ConvertSelfToPDF()") == -1) {
989       die "system call to $::lx_office_conf{applications}->{openofficeorg_writer} failed: $!";
990     }
991   } else {
992     if (!$self->spawn_openoffice()) {
993       $main::lxdebug->leave_sub();
994       return 0;
995     }
996
997     $self->_run_python_uno('./scripts/oo-uno-convert-pdf.py', $::lx_office_conf{print_templates}->{openofficeorg_daemon_port}, "${filename}.odt");
998   }
999
1000   my $res = $?;
1001   if ((0 == $?) || (-f "${filename}.pdf" && -s "${filename}.pdf")) {
1002     $form->{"tmpfile"} =~ s/odt$/pdf/;
1003
1004     unlink($filename . ".odt");
1005
1006     $main::lxdebug->leave_sub();
1007     return 1;
1008
1009   }
1010
1011   unlink($filename . ".odt", $filename . ".pdf");
1012   $self->{"error"} = "Conversion from OpenDocument to PDF failed. " .
1013     "Exit code: $res";
1014
1015   $main::lxdebug->leave_sub();
1016   return 0;
1017 }
1018
1019 sub format_string {
1020   my ($self, $content, $variable) = @_;
1021
1022   my $formatter =
1023        $formatters{ $self->{variable_content_types}->{$variable} }
1024     // $formatters{ $self->{default_content_type} }
1025     // $formatters{ text };
1026
1027   return $formatter->($self, $content, variable => $variable);
1028 }
1029
1030 sub get_mime_type() {
1031   my ($self) = @_;
1032
1033   if ($self->{"form"}->{"format"} =~ /pdf/) {
1034     return "application/pdf";
1035   } else {
1036     return "application/vnd.oasis.opendocument.text";
1037   }
1038 }
1039
1040 sub uses_temp_file {
1041   return 1;
1042 }
1043
1044 1;