c1eee31478f0f142ab842329a922f079197576bf
[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->cloneBlock($placeholder, 1, false))) {
536                     throw new Tinebase_Exception_UnexpectedValue('clone 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                 $this->_docTemplate->replaceBlock($match[0], '${' . $match[0] . '}');
544
545                 $this->_findAndReplaceGroup($processor);
546             }
547         }
548     }
549
550     /**
551      * @param Tinebase_Export_Richtext_TemplateProcessor $_templateProcessor
552      */
553     protected function _findAndReplaceGroup(Tinebase_Export_Richtext_TemplateProcessor $_templateProcessor)
554     {
555         $config = array();
556
557         if (null !== ($group = $_templateProcessor->cloneBlock('GROUP_BLOCK', 1, false))) {
558             $_templateProcessor->replaceBlock('GROUP_BLOCK', '${GROUP_BLOCK}');
559             $groupProcessor = new Tinebase_Export_Richtext_TemplateProcessor('<?xml' . $group, true,
560                 Tinebase_Export_Richtext_TemplateProcessor::TYPE_GROUP, $_templateProcessor);
561
562             if (null === ($recordProcessor = $this->_findAndReplaceRecord($groupProcessor))) {
563                 $config['recordRow'] = $this->_findAndReplaceRecordRow($groupProcessor);
564             } else {
565                 $config['record'] = $recordProcessor;
566             }
567             if (null !== ($groupHeader = $_templateProcessor->cloneBlock('GROUP_HEADER', 1, false))) {
568                 $_templateProcessor->replaceBlock('GROUP_HEADER', '');
569                 $config['groupHeader'] = $groupHeader;
570             }
571             if (null !== ($groupFooter = $_templateProcessor->cloneBlock('GROUP_FOOTER', 1, false))) {
572                 $_templateProcessor->replaceBlock('GROUP_FOOTER', '');
573                 $config['groupFooter'] = $groupFooter;
574             }
575             if (null !== ($groupSeparator = $_templateProcessor->cloneBlock('GROUP_SEPARATOR', 1, false))) {
576                 $_templateProcessor->replaceBlock('GROUP_SEPARATOR', '');
577                 $config['groupSeparator'] = $groupSeparator;
578             }
579             if (isset($config['recordRow']) && (isset($config['groupHeader']) || isset($config['groupFooter']) ||
580                     isset($config['groupSeparator'])) && (isset($config['recordRow']['groupHeaderRow']) ||
581                     isset($config['recordRow']['groupFooterRow']) || isset($config['recordRow']['groupSeparatorRow']))) {
582                 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');
583             }
584             $config['groupXml'] = $this->_cutXml($groupProcessor->getMainPart());
585             $groupProcessor->setMainPart('<?xml');
586             $groupProcessor->setConfig($config);
587             $config = array('group' => $groupProcessor);
588
589         } elseif (null === ($record = $this->_findAndReplaceRecord($_templateProcessor))) {
590             $config['recordRow'] = $this->_findAndReplaceRecordRow($_templateProcessor);
591         } else {
592             $config['record'] = $record;
593         }
594
595         $_templateProcessor->setConfig($config);
596     }
597
598     /**
599      * @param Tinebase_Export_Richtext_TemplateProcessor $_templateProcessor
600      * @return Tinebase_Export_Richtext_TemplateProcessor|null
601      */
602     protected function _findAndReplaceRecord(Tinebase_Export_Richtext_TemplateProcessor $_templateProcessor)
603     {
604         if (null !== ($recordBlock = $_templateProcessor->cloneBlock('RECORD_BLOCK', 1, false))) {
605             $_templateProcessor->replaceBlock('RECORD_BLOCK', '${RECORD_BLOCK}');
606             $processor = new Tinebase_Export_Richtext_TemplateProcessor('<?xml' . $recordBlock, true,
607                 Tinebase_Export_Richtext_TemplateProcessor::TYPE_RECORD, $_templateProcessor);
608             $this->_findAndReplaceSubGroup($processor);
609             $config = array(
610                 'recordXml'     => $this->_cutXml($processor->getMainPart())
611             );
612             $processor->setMainPart('<?xml');
613
614             if (null !== ($recordHeader = $_templateProcessor->cloneBlock('RECORD_HEADER', 1, false))) {
615                 $_templateProcessor->replaceBlock('RECORD_HEADER', '');
616                 $config['header'] = $recordHeader;
617             }
618
619             if (null !== ($recordFooter = $_templateProcessor->cloneBlock('RECORD_FOOTER', 1, false))) {
620                 $_templateProcessor->replaceBlock('RECORD_FOOTER', '');
621                 $config['footer'] = $recordFooter;
622             }
623
624             if (null !== ($recordSeparator = $_templateProcessor->cloneBlock('RECORD_SEPARATOR', 1, false))) {
625                 $_templateProcessor->replaceBlock('RECORD_SEPARATOR', '');
626                 $config['separator'] = $recordSeparator;
627             }
628             $processor->setConfig(array_merge($processor->getConfig(), $config));
629             return $processor;
630         }
631
632         return null;
633     }
634
635     /**
636      * @param Tinebase_Export_Richtext_TemplateProcessor $_templateProcessor
637      * @return array
638      * @throws Tinebase_Exception_UnexpectedValue
639      * @throws \PhpOffice\PhpWord\Exception\Exception
640      */
641     protected function _findAndReplaceRecordRow(Tinebase_Export_Richtext_TemplateProcessor $_templateProcessor)
642     {
643         $result = array();
644
645         if (preg_match('/<w:tbl.*?(\$\{twig:[^}]*record[^}]*})/is', $_templateProcessor->getMainPart(), $matches)) {
646             $result['recordRow'] = $_templateProcessor->replaceRow($matches[1], '${RECORD_ROW}');
647             $processor = new Tinebase_Export_Richtext_TemplateProcessor('<?xml' . $result['recordRow'], true,
648                 Tinebase_Export_Richtext_TemplateProcessor::TYPE_RECORD, $_templateProcessor);
649             $this->_findAndReplaceSubGroup($processor);
650             $processor->setConfig(array(
651                 'recordXml'     => $this->_cutXml($processor->getMainPart())
652             ));
653             $processor->setMainPart('<?xml');
654             $result['recordRowProcessor'] = $processor;
655
656             if (strpos($_templateProcessor->getMainPart(), '${GROUP_HEADER}') !== false) {
657                 $result['groupHeaderRow'] = str_replace('${GROUP_HEADER}', '', $_templateProcessor->replaceRow('${GROUP_HEADER}', ''));
658             }
659
660             if (strpos($_templateProcessor->getMainPart(), '${GROUP_FOOTER}') !== false) {
661                 $result['groupFooterRow'] = str_replace('${GROUP_FOOTER}', '', $_templateProcessor->replaceRow('${GROUP_FOOTER}', ''));
662             }
663
664             if (strpos($_templateProcessor->getMainPart(), '${GROUP_SEPARATOR}') !== false) {
665                 $result['groupSeparatorRow'] = str_replace('${GROUP_SEPARATOR}', '', $_templateProcessor->replaceRow('${GROUP_SEPARATOR}', ''));
666             }
667         } else {
668             throw new Tinebase_Exception_UnexpectedValue('template without RECORD_BLOCK needs to contain a table row with a replacement variable');
669         }
670
671         return $result;
672     }
673
674     /**
675      * @param Tinebase_Export_Richtext_TemplateProcessor $_templateProcessor
676      * @throws Tinebase_Exception
677      */
678     protected function _findAndReplaceSubGroup(Tinebase_Export_Richtext_TemplateProcessor $_templateProcessor)
679     {
680         $parentConfig = array('subgroups' => array(), 'subrecords' => array());
681
682         do {
683             $foundGroup = null;
684             $foundRecord = null;
685             foreach ($_templateProcessor->getVariables() as $var) {
686                 if (strpos($var, 'SUBGROUP') === 0) {
687                     $foundGroup = $var;
688                     break;
689                 }
690                 if (null === $foundRecord && strpos($var, 'SUBRECORD') === 0) {
691                     $foundRecord = $var;
692                 }
693             }
694
695             $config = array();
696             if (null !== $foundGroup) {
697                 if (null !== ($group = $_templateProcessor->cloneBlock($foundGroup, 1, false))) {
698                     list(,$propertyName) = explode('_', $foundGroup);
699                     $_templateProcessor->replaceBlock($foundGroup, '${R' . $foundGroup . '}');
700                     $groupProcessor = new Tinebase_Export_Richtext_TemplateProcessor('<?xml' . $group, true,
701                         Tinebase_Export_Richtext_TemplateProcessor::TYPE_SUBGROUP, $_templateProcessor);
702
703                     if (null === ($recordProcessor = $this->_findAndReplaceSubRecord($groupProcessor))) {
704                         throw new Tinebase_Exception('subgroup without record block: ' . $foundGroup);
705                     } else {
706                         $config['record'] = $recordProcessor;
707                     }
708                     if (null !== ($groupHeader = $_templateProcessor->cloneBlock('SUBG_HEADER_' . $propertyName, 1, false))) {
709                         $_templateProcessor->replaceBlock('SUBG_HEADER_' . $propertyName, '');
710                         $config['groupHeader'] = $groupHeader;
711                     }
712                     if (null !== ($groupFooter = $_templateProcessor->cloneBlock('SUBG_FOOTER_' . $propertyName, 1, false))) {
713                         $_templateProcessor->replaceBlock('SUBG_FOOTER_' . $propertyName, '');
714                         $config['groupFooter'] = $groupFooter;
715                     }
716                     if (null !== ($groupSeparator = $_templateProcessor->cloneBlock('SUBG_SEPARATOR_' . $propertyName, 1, false))) {
717                         $_templateProcessor->replaceBlock('SUBG_SEPARATOR_' . $propertyName, '');
718                         $config['groupSeparator'] = $groupSeparator;
719                     }
720                     $config['groupXml'] = $this->_cutXml($groupProcessor->getMainPart());
721                     $groupProcessor->setMainPart('<?xml');
722                     $groupProcessor->setConfig($config);
723                     $parentConfig['subgroups'][$propertyName] = $groupProcessor;
724                 } else {
725                     throw new Tinebase_Exception('clone block failed after subgroup was found: ' . $foundGroup);
726                 }
727             } elseif (null !== $foundRecord) {
728                 if (null === ($recordProcessor = $this->_findAndReplaceSubRecord($_templateProcessor))) {
729                     throw new Tinebase_Exception('subrecord block failed: ' . $foundRecord);
730                 }
731                 list(,$propertyName) = explode('_', $foundRecord);
732                 $parentConfig['subrecords'][$propertyName] = $recordProcessor;
733             } else {
734                 break;
735             }
736         } while (true);
737
738         $config = $_templateProcessor->getConfig();
739         if (count($parentConfig['subgroups']) > 0) {
740             $config['subgroups'] = $parentConfig['subgroups'];
741         }
742         if (count($parentConfig['subrecords']) > 0) {
743             $config['subrecords'] = $parentConfig['subrecords'];
744         }
745         $_templateProcessor->setConfig($config);
746     }
747
748     /**
749      * @param Tinebase_Export_Richtext_TemplateProcessor $_templateProcessor
750      * @return null|Tinebase_Export_Richtext_TemplateProcessor
751      * @throws Tinebase_Exception
752      */
753     protected function _findAndReplaceSubRecord(Tinebase_Export_Richtext_TemplateProcessor $_templateProcessor)
754     {
755         $foundRecord = null;
756         foreach ($_templateProcessor->getVariables() as $var) {
757             if (null === $foundRecord && strpos($var, 'SUBRECORD') === 0) {
758                 $foundRecord = $var;
759                 break;
760             }
761         }
762         if (null === $foundRecord) {
763             return null;
764         }
765
766         if (null !== ($recordBlock = $_templateProcessor->cloneBlock($foundRecord, 1, false))) {
767             list(,$propertyName) = explode('_', $foundRecord);
768             $_templateProcessor->replaceBlock($foundRecord, '${R' . $foundRecord . '}');
769             $processor = new Tinebase_Export_Richtext_TemplateProcessor('<?xml', true,
770                 Tinebase_Export_Richtext_TemplateProcessor::TYPE_SUBRECORD, $_templateProcessor);
771             $config = array(
772                 'recordXml'     => $recordBlock,
773                 'name'          => $foundRecord,
774             );
775
776             if (null !== ($recordHeader = $_templateProcessor->cloneBlock('SUBR_HEADER_' . $propertyName, 1, false))) {
777                 $_templateProcessor->replaceBlock('SUBR_HEADER_' . $propertyName, '');
778                 $config['header'] = $recordHeader;
779             }
780
781             if (null !== ($recordFooter = $_templateProcessor->cloneBlock('SUBR_FOOTER_' . $propertyName, 1, false))) {
782                 $_templateProcessor->replaceBlock('SUBR_FOOTER_' . $propertyName, '');
783                 $config['footer'] = $recordFooter;
784             }
785
786             if (null !== ($recordSeparator = $_templateProcessor->cloneBlock('SUBR_SEPARATOR_' . $propertyName, 1, false))) {
787                 $_templateProcessor->replaceBlock('SUBR_SEPARATOR_' . $propertyName, '');
788                 $config['separator'] = $recordSeparator;
789             }
790             $processor->setConfig($config);
791             return $processor;
792         } else {
793             throw new Tinebase_Exception('clone block failed after subrecord was found: ' . $foundRecord);
794         }
795     }
796
797
798     /**
799      * now simulate processIteration and finish with _onAfterExportRecords
800      *
801      * @param array $_result
802      */
803     protected function _onAfterExportRecords(array $_result)
804     {
805         $this->_unwrapProcessors();
806
807         parent::_onAfterExportRecords($_result);
808     }
809
810     protected function _getTemplateVariables()
811     {
812         if (null === $this->_templateVariables) {
813             $this->_templateVariables = $this->_docTemplate->getVariables();
814         }
815
816         return $this->_templateVariables;
817     }
818 }