c5890c91a1f5faf0f885370148d6df004aba4f60
[tine20] / tine20 / Tinebase / Export / Doc.php
1 <?php
2 /**
3  * Tinebase Doc/Docx generation class
4  *
5  * @package     Tinebase
6  * @subpackage  Export
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @author      Paul Mehrer <p.mehrer@metaways.de>
9  * @copyright   Copyright (c) 2017 Metaways Infosystems GmbH (http://www.metaways.de)
10  */
11
12 /**
13  * Tinebase Doc/Docx generation class
14  *
15  * @package     Tinebase
16  * @subpackage  Export
17  */
18
19 class Tinebase_Export_Doc extends Tinebase_Export_Abstract implements Tinebase_Record_IteratableInterface
20 {
21
22     /**
23      * the document
24      *
25      * @var \PhpOffice\PhpWord\PhpWord
26      */
27     protected $_docObject;
28
29     /**
30      * the template to work on
31      *
32      * @var Tinebase_Export_Richtext_TemplateProcessor
33      */
34     protected $_docTemplate = null;
35
36     /**
37      * format strings
38      *
39      * @var string
40      */
41     protected $_format = 'docx';
42
43     /**
44      * @var int
45      */
46     protected $_rowCount = 0;
47
48     /**
49      * @var array
50      */
51     protected $_templateVariables = null;
52
53     /**
54      * @var array
55      */
56     protected $_dataSources = array();
57
58     /**
59      * @var string
60      */
61     protected $_currentDataSource = null;
62
63     /**
64      * @var boolean
65      */
66     protected $_skip = false;
67
68     /**
69      * @var Tinebase_Export_Richtext_TemplateProcessor
70      */
71     protected $_currentProcessor = null;
72
73     protected $_subTwigTemplates = array();
74     protected $_subTwigMappings = array();
75
76
77
78     /**
79      * get download content type
80      *
81      * @return string
82      */
83     public function getDownloadContentType()
84     {
85         return 'application/vnd.ms-word';
86     }
87
88
89     /**
90      * return download filename
91      *
92      * @param string $_appName
93      * @param string $_format
94      * @return string
95      */
96     public function getDownloadFilename($_appName, $_format)
97     {
98         return 'letter_' . strtolower($_appName) . '.docx';
99     }
100
101     public static function getDefaultFormat()
102     {
103         return 'docx';
104     }
105
106     /**
107      * generate export
108      */
109     public function generate()
110     {
111         $this->_rowCount = 0;
112         $this->_writeGenericHeader = false;
113         $this->_dumpRecords = false;
114         $this->_createDocument();
115         $this->_exportRecords();
116         if (null !== $this->_docTemplate) {
117             $this->_docTemplate->replaceTine20ImagePaths();
118         }
119     }
120
121     /**
122      * output result
123      */
124     public function write()
125     {
126         $document = $this->getDocument();
127         $tempfile = $document->save();
128         readfile($tempfile);
129         unlink($tempfile);
130     }
131
132     public function save($filename)
133     {
134         $document = $this->getDocument();
135         $tempfile = $document->save();
136
137         copy($tempfile, $filename);
138         unlink($tempfile);
139     }
140
141     /**
142      * @param string $str
143      * @return string
144      */
145     protected function _cutXml($str)
146     {
147         return substr($str, 5);
148     }
149
150     /**
151      * @param $_name
152      */
153     protected function _startDataSource($_name)
154     {
155         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' starting datasource ' . $_name);
156
157         if (!isset($this->_dataSources[$_name])) {
158             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' datasource not found, skipping data!');
159             $this->_skip = true;
160             return;
161         }
162
163         $this->_firstIteration = true;
164         $this->_currentProcessor = $this->_dataSources[$_name];
165         $this->_currentDataSource = $_name;
166         $this->_rowCount = 0;
167     }
168
169     /**
170      * @param $_name
171      */
172     protected function _endDataSource($_name)
173     {
174         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ending datasource ' . $_name);
175
176         $data = '';
177
178         if (false === $this->_skip) {
179
180             $this->_unwrapProcessors();
181
182             /** @var Tinebase_Export_Richtext_TemplateProcessor $processor */
183             $processor = $this->_dataSources[$_name];
184             $data = $this->_cutXml($processor->getMainPart());
185         }
186
187         $this->_lastGroupValue = null;
188         $this->_skip = false;
189         $this->_currentProcessor = $this->_docTemplate;
190         $this->_docTemplate->setValue('DATASOURCE_' . $_name, $data);
191     }
192
193     protected function _unwrapProcessors()
194     {
195         /*if ($this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_SUBRECORD) {
196             $parent = $this->_currentProcessor->getParent();
197             $name = '${R' . $this->_currentProcessor->getConfig('name') . '}';
198             if ($parent->getType()  === Tinebase_Export_Richtext_TemplateProcessor::TYPE_SUBGROUP) {
199                 $this->_currentProcessor = $parent;
200             } elseif ($parent->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_RECORD) {
201                 $parent = $parent->getParent();
202             }
203             $parent->setValue($name, '');
204         }
205         if ($this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_SUBGROUP) {
206             $parent = $this->_currentProcessor->getParent();
207             $name = '${R' . $this->_currentProcessor->getConfig('name') . '}';
208             if ($parent->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_RECORD) {
209                 $parent = $parent->getParent();
210             }
211             $parent->setValue($name, '');
212         }*/
213
214         if ($this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_RECORD) {
215             $this->_currentProcessor = $this->_currentProcessor->getParent();
216             $this->_currentProcessor->setValue('${RECORD_BLOCK}', '');
217         }
218
219         if ($this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_STANDARD) {
220             $this->_currentProcessor->setValue('${RECORD_ROW}', '');
221         }
222
223         if ($this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_GROUP) {
224             $processor = $this->_currentProcessor->getParent();
225             $processor->setValue('${GROUP_BLOCK}', $this->_cutXml($this->_currentProcessor->getMainPart()));
226             $this->_currentProcessor = $processor;
227         }
228     }
229
230     protected function _startGroup()
231     {
232         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' starting group...');
233
234         if (true === $this->_skip) {
235             return;
236         }
237
238         if ($this->_currentProcessor->hasConfig('group')) {
239             $this->_currentProcessor = $this->_currentProcessor->getConfig('group');
240         }
241
242         if ($this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_GROUP ||
243                 $this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_SUBGROUP) {
244             if ($this->_rowCount > 0 && $this->_currentProcessor->hasConfig('groupSeparator')) {
245                 $this->_currentProcessor->append($this->_currentProcessor->getConfig('groupSeparator'));
246             }
247
248             if ($this->_currentProcessor->hasConfig('groupHeader')) {
249                 $this->_currentProcessor->append($this->_currentProcessor->getConfig('groupHeader'));
250             }
251
252             $this->_currentProcessor->append($this->_currentProcessor->getConfig('groupXml'));
253         } elseif ($this->_currentProcessor->hasConfig('recordRow')) {
254             $recordRow = $this->_currentProcessor->getConfig('recordRow');
255
256             if ($this->_rowCount > 0 && isset($recordRow['groupSeparatorRow'])) {
257                 $this->_currentProcessor->setValue('${RECORD_ROW}', $recordRow['groupSeparatorRow'] . '${RECORD_ROW}');
258             }
259
260             if (isset($recordRow['groupHeaderRow'])) {
261                 $this->_currentProcessor->setValue('${RECORD_ROW}', $recordRow['groupHeaderRow'] . '${RECORD_ROW}');
262             }
263         }
264     }
265
266     protected function _endGroup()
267     {
268         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ending group...');
269
270         if (true === $this->_skip) {
271             return;
272         }
273
274         if ($this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_GROUP) {
275             $this->_currentProcessor->setValue('${RECORD_ROW}', '');
276         }
277
278         if ($this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_RECORD) {
279             $this->_currentProcessor = $this->_currentProcessor->getParent();
280             if ($this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_GROUP) {
281                 $this->_currentProcessor->setValue('${RECORD_BLOCK}', '');
282             }
283         }
284
285         if ($this->_currentProcessor->hasConfig('recordRow')) {
286             $recordRow = $this->_currentProcessor->getConfig('recordRow');
287             if (isset($recordRow['groupFooterRow'])) {
288                 $this->_currentProcessor->setValue('${RECORD_ROW}', $recordRow['groupFooterRow'] . '${RECORD_ROW}');
289             }
290         }
291
292         if ($this->_currentProcessor->hasConfig('groupFooter')) {
293             $this->_currentProcessor->append($this->_currentProcessor->getConfig('groupFooter'));
294         }
295     }
296
297     protected function _startRow()
298     {
299         if (true === $this->_skip) {
300             return;
301         }
302
303         $this->_rowCount += 1;
304
305         if ($this->_currentProcessor->hasConfig('recordRow')) {
306             $recordRow = $this->_currentProcessor->getConfig('recordRow');
307             $this->_currentProcessor->setValue('${RECORD_ROW}', $recordRow['recordRow'] . '${RECORD_ROW}');
308         } else {
309             if ($this->_currentProcessor->hasConfig('record')) {
310                 $this->_currentProcessor = $this->_currentProcessor->getConfig('record');
311             }
312
313             if ($this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_RECORD ||
314                     $this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_SUBRECORD) {
315                 $data = '';
316                 if ($this->_rowCount > 1 && $this->_currentProcessor->hasConfig('separator')) {
317                     $data .= $this->_currentProcessor->getConfig('separator');
318                 }
319
320                 if ($this->_currentProcessor->hasConfig('header')) {
321                     $data .= $this->_currentProcessor->getConfig('header');
322                 }
323
324                 if ($this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_RECORD) {
325                     $data .= $this->_currentProcessor->getConfig('recordXml') . '${RECORD_BLOCK}';
326                     $this->_currentProcessor->getParent()->setValue('${RECORD_BLOCK}', $data);
327                 } else {
328                     $name = '${R' . $this->_currentProcessor->getConfig('name') . '}';
329                     $data .= $this->_currentProcessor->getConfig('recordXml') . $name;
330                     if (($parent = $this->_currentProcessor->getParent()) && $parent->getType() ===
331                             Tinebase_Export_Richtext_TemplateProcessor::TYPE_RECORD) {
332                         $parent = $parent->getParent();
333                     }
334                     $parent->setValue($name, $data);
335                 }
336             } else {
337                 throw new Tinebase_Exception_UnexpectedValue('template and definition do not match');
338             }
339         }
340     }
341
342     protected function _endRow()
343     {
344         if (true === $this->_skip) {
345             return;
346         }
347
348         if ($this->_currentProcessor->hasConfig('subgroups')) {
349             foreach ($this->_currentProcessor->getConfig('subgroups') as $property => $group) {
350                 $this->_executeSubTemplate($property, $group);
351             }
352         }
353         if ($this->_currentProcessor->hasConfig('subrecords')) {
354             foreach ($this->_currentProcessor->getConfig('subrecords') as $property => $record) {
355                 $this->_executeSubTemplate($property, $record);
356             }
357         }
358
359         if ($this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_RECORD &&
360                 $this->_currentProcessor->hasConfig('footer')) {
361             $this->_currentProcessor->getParent()->setValue('${RECORD_BLOCK}',
362                 $this->_currentProcessor->getConfig('footer') . '${RECORD_BLOCK}');
363         }
364     }
365
366     /**
367      * @param string $_name
368      * @param Tinebase_Export_Richtext_TemplateProcessor $_processor
369      */
370     protected function _executeSubTemplate($_name, Tinebase_Export_Richtext_TemplateProcessor $_processor)
371     {
372         $recordSet = $this->_currentRecord->{$_name};
373         if (is_array($recordSet)) {
374             if (count($recordSet) === 0) {
375                 return;
376             }
377             if (($record = reset($recordSet)) instanceof Tinebase_Record_Abstract) {
378                 $recordSet = new Tinebase_Record_RecordSet(get_class($record), $recordSet);
379             }
380         } elseif (is_object($recordSet)) {
381             if ($recordSet instanceof Tinebase_Record_Abstract) {
382                 $recordSet = new Tinebase_Record_RecordSet(get_class($recordSet), array($recordSet));
383             } elseif (!$recordSet instanceof Tinebase_Record_RecordSet) {
384                 return;
385             }
386         } else {
387             return;
388         }
389         $oldTemplateVariables = $this->_templateVariables;
390         $oldProcessor = $this->_currentProcessor;
391         $oldDocTemplate = $this->_docTemplate;
392         $oldRowCount = $this->_rowCount;
393         $subTempName = $this->_currentDataSource . '_' . $_name;
394         $oldState = $this->_getCurrentState();
395         foreach (array_keys($oldState) as $key) {
396             $this->{$key} = null;
397         }
398         $this->_templateVariables = null;
399
400
401         $this->_currentProcessor = $_processor;
402         $this->_rowCount = 0;
403         $this->_docTemplate = $_processor;
404
405         if (!isset($this->_subTwigTemplates[$subTempName])) {
406             $this->_twigEnvironment->getLoader()->addLoader(
407                 new Tinebase_Twig_CallBackLoader($this->_templateFileName . $subTempName, $this->_getLastModifiedTimeStamp(),
408                     array($this, '_getTwigSource')));
409
410             $this->_twigTemplate = $this->_twigEnvironment->load($this->_templateFileName . $subTempName);
411             $this->_subTwigTemplates[$subTempName] = $this->_twigTemplate;
412             $this->_subTwigMappings[$subTempName] = $this->_twigMapping;
413         } else {
414             $this->_twigTemplate = $this->_subTwigTemplates[$subTempName];
415             $this->_twigMapping = $this->_subTwigMappings[$subTempName];
416         }
417
418         $this->processIteration($recordSet);
419
420         $result = $this->_cutXml($this->_currentProcessor->getMainPart());
421         $replacementName = ($this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_SUBGROUP ?
422             'RSUBGROUP_' : 'RSUBRECORD_') . $_name;
423
424         $this->_templateVariables = $oldTemplateVariables;
425         $this->_docTemplate = $oldDocTemplate;
426         $this->_currentProcessor = $oldProcessor;
427         if ($this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_RECORD) {
428             $this->_currentProcessor->getParent()->setValue($replacementName, $result);
429         } else {
430             $this->_currentProcessor->setValue($replacementName, $result);
431         }
432         $this->_rowCount = $oldRowCount;
433         $this->_setCurrentState($oldState);
434     }
435
436     /**
437      * get word object
438      *
439      * @return \PhpOffice\PhpWord\PhpWord | \PhpOffice\PhpWord\TemplateProcessor
440      */
441     public function getDocument()
442     {
443         return $this->_docTemplate ? $this->_docTemplate : $this->_docObject;
444     }
445
446
447     /**
448      * create new PhpWord document
449      *
450      * @return void
451      */
452     protected function _createDocument()
453     {
454         \PhpOffice\PhpWord\Settings::setTempDir(Tinebase_Core::getTempDir());
455
456         $templateFile = $this->_getTemplateFilename();
457         $this->_docObject = new \PhpOffice\PhpWord\PhpWord();
458
459         if ($templateFile !== null) {
460             $this->_hasTemplate = true;
461             $this->_docTemplate = new Tinebase_Export_Richtext_TemplateProcessor($templateFile);
462         }
463     }
464
465     /**
466      * @param string $_key
467      * @param string $_value
468      */
469     protected function _setValue($_key, $_value)
470     {
471         if (true === $this->_skip) {
472             return;
473         }
474
475         $this->_currentProcessor->setValue($_key, $_value);
476
477         if ($this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_RECORD) {
478             $this->_currentProcessor->getParent()->setValue($_key, $_value);
479         } elseif ($this->_currentProcessor->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_SUBRECORD) {
480             $this->_currentProcessor->getParent()->setValue($_key, $_value);
481             if ($this->_currentProcessor->getParent()->getType() === Tinebase_Export_Richtext_TemplateProcessor::TYPE_RECORD) {
482                 $this->_currentProcessor->getParent()->getParent()->setValue($_key, $_value);
483             }
484         }
485     }
486
487     /**
488      * @param string $_value
489      * @throws Tinebase_Exception_NotImplemented
490      */
491     protected function _writeValue($_value)
492     {
493         throw new Tinebase_Exception_NotImplemented(__CLASS__ . ' can not provide a meaningful default '
494                 . 'implementation. Subclass needs to provide or avoid it from being called');
495     }
496
497     public function _getTwigSource()
498     {
499         if (null === $this->_currentProcessor) {
500             $this->_onBeforeExportRecords();
501         }
502         $i = 0;
503         $source = '[';
504         foreach ($this->_getTemplateVariables() as $placeholder) {
505             if (strpos($placeholder, 'twig:') === 0) {
506                 $this->_twigMapping[$i] = $placeholder;
507                 $source .= ($i === 0 ? '' : ',') . '{{' . html_entity_decode(substr($placeholder, 5), ENT_QUOTES | ENT_XML1) . '}}';
508                 ++$i;
509             }
510         }
511         $source .= ']';
512
513         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
514             . ' returning twig template source: ' . $source);
515
516         return $source;
517     }
518
519     protected function _onBeforeExportRecords()
520     {
521         if (null !== $this->_docTemplate && null === $this->_currentProcessor) {
522             $this->_currentProcessor = $this->_docTemplate;
523             $this->_findAndReplaceDatasources();
524
525             if (empty($this->_dataSources)) {
526                 $this->_findAndReplaceGroup($this->_docTemplate);
527             }
528         }
529     }
530
531     protected function _findAndReplaceDatasources()
532     {
533         foreach ($this->_getTemplateVariables() as $placeholder) {
534             if (strpos($placeholder, 'DATASOURCE') === 0 && preg_match('/DATASOURCE_(.*)/', $placeholder, $match)) {
535                 if (null === ($dataSource = $this->_docTemplate->findBlock($placeholder, '${' . $match[0] . '}'))) {
536                     throw new Tinebase_Exception_UnexpectedValue('find&replace block for ' . $placeholder . ' failed');
537                 }
538
539                 $dataSource = '<?xml' . $dataSource;
540                 $processor = new Tinebase_Export_Richtext_TemplateProcessor($dataSource, true,
541                     Tinebase_Export_Richtext_TemplateProcessor::TYPE_DATASOURCE);
542                 $this->_dataSources[$match[1]] = $processor;
543
544                 $this->_findAndReplaceGroup($processor);
545             }
546         }
547     }
548
549     /**
550      * @param Tinebase_Export_Richtext_TemplateProcessor $_templateProcessor
551      */
552     protected function _findAndReplaceGroup(Tinebase_Export_Richtext_TemplateProcessor $_templateProcessor)
553     {
554         $config = array();
555
556         if (null !== ($group = $_templateProcessor->findBlock('GROUP_BLOCK', '${GROUP_BLOCK}'))) {
557             $groupProcessor = new Tinebase_Export_Richtext_TemplateProcessor('<?xml' . $group, true,
558                 Tinebase_Export_Richtext_TemplateProcessor::TYPE_GROUP, $_templateProcessor);
559
560             if (null === ($recordProcessor = $this->_findAndReplaceRecord($groupProcessor))) {
561                 $config['recordRow'] = $this->_findAndReplaceRecordRow($groupProcessor);
562             } else {
563                 $config['record'] = $recordProcessor;
564             }
565             if (null !== ($groupHeader = $_templateProcessor->findBlock('GROUP_HEADER', ''))) {
566                 $config['groupHeader'] = $groupHeader;
567             }
568             if (null !== ($groupFooter = $_templateProcessor->findBlock('GROUP_FOOTER', ''))) {
569                 $config['groupFooter'] = $groupFooter;
570             }
571             if (null !== ($groupSeparator = $_templateProcessor->findBlock('GROUP_SEPARATOR', ''))) {
572                 $config['groupSeparator'] = $groupSeparator;
573             }
574             if (isset($config['recordRow']) && (isset($config['groupHeader']) || isset($config['groupFooter']) ||
575                     isset($config['groupSeparator'])) && (isset($config['recordRow']['groupHeaderRow']) ||
576                     isset($config['recordRow']['groupFooterRow']) || isset($config['recordRow']['groupSeparatorRow']))) {
577                 throw new Tinebase_Exception_UnexpectedValue('GROUP with record row must not contain header, footer or separator as table row and group block at the same time');
578             }
579             $config['groupXml'] = $this->_cutXml($groupProcessor->getMainPart());
580             $groupProcessor->setMainPart('<?xml');
581             $groupProcessor->setConfig($config);
582             $config = array('group' => $groupProcessor);
583
584         } elseif (null === ($record = $this->_findAndReplaceRecord($_templateProcessor))) {
585             $config['recordRow'] = $this->_findAndReplaceRecordRow($_templateProcessor);
586         } else {
587             $config['record'] = $record;
588         }
589
590         $_templateProcessor->setConfig($config);
591     }
592
593     /**
594      * @param Tinebase_Export_Richtext_TemplateProcessor $_templateProcessor
595      * @return Tinebase_Export_Richtext_TemplateProcessor|null
596      */
597     protected function _findAndReplaceRecord(Tinebase_Export_Richtext_TemplateProcessor $_templateProcessor)
598     {
599         if (null !== ($recordBlock = $_templateProcessor->findBlock('RECORD_BLOCK', '${RECORD_BLOCK}'))) {
600             $processor = new Tinebase_Export_Richtext_TemplateProcessor('<?xml' . $recordBlock, true,
601                 Tinebase_Export_Richtext_TemplateProcessor::TYPE_RECORD, $_templateProcessor);
602             $this->_findAndReplaceSubGroup($processor);
603             $config = array(
604                 'recordXml'     => $this->_cutXml($processor->getMainPart())
605             );
606             $processor->setMainPart('<?xml');
607
608             if (null !== ($recordHeader = $_templateProcessor->findBlock('RECORD_HEADER', ''))) {
609                 $config['header'] = $recordHeader;
610             }
611
612             if (null !== ($recordFooter = $_templateProcessor->findBlock('RECORD_FOOTER', ''))) {
613                 $config['footer'] = $recordFooter;
614             }
615
616             if (null !== ($recordSeparator = $_templateProcessor->findBlock('RECORD_SEPARATOR', ''))) {
617                 $config['separator'] = $recordSeparator;
618             }
619             $processor->setConfig(array_merge($processor->getConfig(), $config));
620             return $processor;
621         }
622
623         return null;
624     }
625
626     /**
627      * @param Tinebase_Export_Richtext_TemplateProcessor $_templateProcessor
628      * @return array
629      * @throws Tinebase_Exception_UnexpectedValue
630      * @throws \PhpOffice\PhpWord\Exception\Exception
631      */
632     protected function _findAndReplaceRecordRow(Tinebase_Export_Richtext_TemplateProcessor $_templateProcessor)
633     {
634         $result = array();
635
636         if (preg_match('/<w:tbl.*?(\$\{twig:[^}]*record[^}]*})/is', $_templateProcessor->getMainPart(), $matches)) {
637             $result['recordRow'] = $_templateProcessor->replaceRow($matches[1], '${RECORD_ROW}');
638             $processor = new Tinebase_Export_Richtext_TemplateProcessor('<?xml' . $result['recordRow'], true,
639                 Tinebase_Export_Richtext_TemplateProcessor::TYPE_RECORD, $_templateProcessor);
640             $this->_findAndReplaceSubGroup($processor);
641             $processor->setConfig(array(
642                 'recordXml'     => $this->_cutXml($processor->getMainPart())
643             ));
644             $processor->setMainPart('<?xml');
645             $result['recordRowProcessor'] = $processor;
646
647             if (strpos($_templateProcessor->getMainPart(), '${GROUP_HEADER}') !== false) {
648                 $result['groupHeaderRow'] = str_replace('${GROUP_HEADER}', '', $_templateProcessor->replaceRow('${GROUP_HEADER}', ''));
649             }
650
651             if (strpos($_templateProcessor->getMainPart(), '${GROUP_FOOTER}') !== false) {
652                 $result['groupFooterRow'] = str_replace('${GROUP_FOOTER}', '', $_templateProcessor->replaceRow('${GROUP_FOOTER}', ''));
653             }
654
655             if (strpos($_templateProcessor->getMainPart(), '${GROUP_SEPARATOR}') !== false) {
656                 $result['groupSeparatorRow'] = str_replace('${GROUP_SEPARATOR}', '', $_templateProcessor->replaceRow('${GROUP_SEPARATOR}', ''));
657             }
658         } else {
659             throw new Tinebase_Exception_UnexpectedValue('template without RECORD_BLOCK needs to contain a table row with a replacement variable');
660         }
661
662         return $result;
663     }
664
665     /**
666      * @param Tinebase_Export_Richtext_TemplateProcessor $_templateProcessor
667      * @throws Tinebase_Exception
668      */
669     protected function _findAndReplaceSubGroup(Tinebase_Export_Richtext_TemplateProcessor $_templateProcessor)
670     {
671         $parentConfig = array('subgroups' => array(), 'subrecords' => array());
672
673         do {
674             $foundGroup = null;
675             $foundRecord = null;
676             foreach ($_templateProcessor->getVariables() as $var) {
677                 if (strpos($var, 'SUBGROUP') === 0) {
678                     $foundGroup = $var;
679                     break;
680                 }
681                 if (null === $foundRecord && strpos($var, 'SUBRECORD') === 0) {
682                     $foundRecord = $var;
683                 }
684             }
685
686             $config = array();
687             if (null !== $foundGroup) {
688                 if (null !== ($group = $_templateProcessor->findBlock($foundGroup, '${R' . $foundGroup . '}'))) {
689                     list(,$propertyName) = explode('_', $foundGroup);
690                     $groupProcessor = new Tinebase_Export_Richtext_TemplateProcessor('<?xml' . $group, true,
691                         Tinebase_Export_Richtext_TemplateProcessor::TYPE_SUBGROUP, $_templateProcessor);
692
693                     if (null === ($recordProcessor = $this->_findAndReplaceSubRecord($groupProcessor))) {
694                         throw new Tinebase_Exception('subgroup without record block: ' . $foundGroup);
695                     } else {
696                         $config['record'] = $recordProcessor;
697                     }
698                     if (null !== ($groupHeader = $_templateProcessor->findBlock('SUBG_HEADER_' . $propertyName, ''))) {
699                         $config['groupHeader'] = $groupHeader;
700                     }
701                     if (null !== ($groupFooter = $_templateProcessor->findBlock('SUBG_FOOTER_' . $propertyName, ''))) {
702                         $config['groupFooter'] = $groupFooter;
703                     }
704                     if (null !== ($groupSeparator = $_templateProcessor->findBlock('SUBG_SEPARATOR_' . $propertyName, ''))) {
705                         $config['groupSeparator'] = $groupSeparator;
706                     }
707                     $config['groupXml'] = $this->_cutXml($groupProcessor->getMainPart());
708                     $groupProcessor->setMainPart('<?xml');
709                     $groupProcessor->setConfig($config);
710                     $parentConfig['subgroups'][$propertyName] = $groupProcessor;
711                 } else {
712                     throw new Tinebase_Exception('find&replace block failed after subgroup was found: ' . $foundGroup);
713                 }
714             } elseif (null !== $foundRecord) {
715                 if (null === ($recordProcessor = $this->_findAndReplaceSubRecord($_templateProcessor))) {
716                     throw new Tinebase_Exception('subrecord block failed: ' . $foundRecord);
717                 }
718                 list(,$propertyName) = explode('_', $foundRecord);
719                 $parentConfig['subrecords'][$propertyName] = $recordProcessor;
720             } else {
721                 break;
722             }
723         } while (true);
724
725         $config = $_templateProcessor->getConfig();
726         if (count($parentConfig['subgroups']) > 0) {
727             $config['subgroups'] = $parentConfig['subgroups'];
728         }
729         if (count($parentConfig['subrecords']) > 0) {
730             $config['subrecords'] = $parentConfig['subrecords'];
731         }
732         $_templateProcessor->setConfig($config);
733     }
734
735     /**
736      * @param Tinebase_Export_Richtext_TemplateProcessor $_templateProcessor
737      * @return null|Tinebase_Export_Richtext_TemplateProcessor
738      * @throws Tinebase_Exception
739      */
740     protected function _findAndReplaceSubRecord(Tinebase_Export_Richtext_TemplateProcessor $_templateProcessor)
741     {
742         $foundRecord = null;
743         foreach ($_templateProcessor->getVariables() as $var) {
744             if (null === $foundRecord && strpos($var, 'SUBRECORD') === 0) {
745                 $foundRecord = $var;
746                 break;
747             }
748         }
749         if (null === $foundRecord) {
750             return null;
751         }
752
753         if (null !== ($recordBlock = $_templateProcessor->findBlock($foundRecord, '${R' . $foundRecord . '}'))) {
754             list(,$propertyName) = explode('_', $foundRecord);
755             $processor = new Tinebase_Export_Richtext_TemplateProcessor('<?xml', true,
756                 Tinebase_Export_Richtext_TemplateProcessor::TYPE_SUBRECORD, $_templateProcessor);
757             $config = array(
758                 'recordXml'     => $recordBlock,
759                 'name'          => $foundRecord,
760             );
761
762             if (null !== ($recordHeader = $_templateProcessor->findBlock('SUBR_HEADER_' . $propertyName, ''))) {
763                 $config['header'] = $recordHeader;
764             }
765
766             if (null !== ($recordFooter = $_templateProcessor->findBlock('SUBR_FOOTER_' . $propertyName, ''))) {
767                 $config['footer'] = $recordFooter;
768             }
769
770             if (null !== ($recordSeparator = $_templateProcessor->findBlock('SUBR_SEPARATOR_' . $propertyName, ''))) {
771                 $config['separator'] = $recordSeparator;
772             }
773             $processor->setConfig($config);
774             return $processor;
775         } else {
776             throw new Tinebase_Exception('find&replace block failed after subrecord was found: ' . $foundRecord);
777         }
778     }
779
780
781     /**
782      * now simulate processIteration and finish with _onAfterExportRecords
783      *
784      * @param array $_result
785      */
786     protected function _onAfterExportRecords(array $_result)
787     {
788         $this->_unwrapProcessors();
789
790         parent::_onAfterExportRecords($_result);
791     }
792
793     protected function _getTemplateVariables()
794     {
795         if (null === $this->_templateVariables) {
796             $this->_templateVariables = $this->_docTemplate->getVariables();
797         }
798
799         return $this->_templateVariables;
800     }
801 }