9de40a54b34697de465ff743cedc6522789a5c4d
[tine20] / tine20 / Tinebase / Export / Richtext / TemplateProcessor.php
1 <?php
2 /**
3  * Tinebase Doc/Docx template processor 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-2017 Metaways Infosystems GmbH (http://www.metaways.de)
10  */
11
12 /**
13  * Tinebase Doc/Docx template processor class
14  *
15  * @package     Tinebase
16  * @subpackage    Export
17  */
18
19
20 class Tinebase_Export_Richtext_TemplateProcessor extends \PhpOffice\PhpWord\TemplateProcessor
21 {
22     const TYPE_STANDARD = 'standard';
23     const TYPE_DATASOURCE = 'datasource';
24     const TYPE_GROUP = 'group';
25     const TYPE_SUBGROUP = 'subgroup';
26     const TYPE_RECORD = 'record';
27     const TYPE_SUBRECORD = 'subrecord';
28
29     /**
30      * Content of document rels (in XML format) of the temporary document.
31      *
32      * @var string
33      */
34     protected $_temporaryDocumentRels = null;
35
36     protected $_tempHeaderRels = array();
37
38     protected $_tempFooterRels = array();
39
40     protected $_type = null;
41
42     protected $_parent = null;
43
44     protected $_config = array();
45
46     /**
47      * @param string $documentTemplate The fully qualified template filename.
48      * @param bool   $inMemory
49      * @param string $type
50      * @param Tinebase_Export_Richtext_TemplateProcessor|null $parent
51      *
52      * @throws \PhpOffice\PhpWord\Exception\CreateTemporaryFileException
53      * @throws \PhpOffice\PhpWord\Exception\CopyFileException
54      */
55     public function __construct($documentTemplate, $inMemory = false, $type = self::TYPE_STANDARD, Tinebase_Export_Richtext_TemplateProcessor $parent = null)
56     {
57         $this->_type = $type;
58         $this->_parent = $parent;
59
60         if (true === $inMemory) {
61             $this->tempDocumentMainPart = $documentTemplate;
62             return;
63         }
64
65         parent::__construct($documentTemplate);
66
67         $index = 1;
68         while (false !== $this->zipClass->locateName($this->getHeaderName($index))) {
69             $fileName = 'word/_rels/header' . $index . '.xml.rels';
70             if (false !== $this->zipClass->locateName($fileName)) {
71                 $this->_tempHeaderRels[$index] = $this->fixBrokenMacros(
72                     $this->zipClass->getFromName($fileName)
73                 );
74             }
75             $index++;
76         }
77         $index = 1;
78         while (false !== $this->zipClass->locateName($this->getFooterName($index))) {
79             $fileName = 'word/_rels/footer' . $index . '.xml.rels';
80             if (false !== $this->zipClass->locateName($fileName)) {
81                 $this->_tempFooterRels[$index] = $this->fixBrokenMacros(
82                     $this->zipClass->getFromName($fileName)
83                 );
84             }
85             $index++;
86         }
87
88         if (false !== $this->zipClass->locateName('word/_rels/document.xml.rels')) {
89             $this->_temporaryDocumentRels = $this->fixBrokenMacros(
90                 $this->zipClass->getFromName('word/_rels/document.xml.rels'));
91         }
92     }
93
94     /**
95      * @return string
96      */
97     public function getType()
98     {
99         return $this->_type;
100     }
101
102     /**
103      * @return Tinebase_Export_Richtext_TemplateProcessor|null
104      */
105     public function getParent()
106     {
107         return $this->_parent;
108     }
109
110     public function replaceTine20ImagePaths()
111     {
112         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
113             Tinebase_Core::getLogger()->debug(__METHOD__ . ' ' . __LINE__ . ' replacing images...');
114
115         if (null !== $this->_temporaryDocumentRels) {
116             $this->_replaceTine20ImagePaths($this->tempDocumentMainPart, $this->_temporaryDocumentRels);
117         }
118         foreach($this->_tempHeaderRels as $index => $data) {
119             $this->_replaceTine20ImagePaths($this->tempDocumentHeaders[$index], $data);
120         }
121         foreach($this->_tempFooterRels as $index => $data) {
122             $this->_replaceTine20ImagePaths($this->tempDocumentFooters[$index], $data);
123         }
124     }
125
126     protected function _replaceTine20ImagePaths($xmlData, $relData)
127     {
128         if (preg_match_all('#<wp:docPr[^>]+"(\w+://[^"]+)".*?r:embed="([^"]+)"#is', $xmlData, $matches, PREG_SET_ORDER)) {
129             foreach($matches as $match) {
130
131                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
132                     Tinebase_Core::getLogger()->debug(__METHOD__ . ' ' . __LINE__ . ' found url: ' . $match[1]);
133
134                 if (!in_array(mb_strtolower(pathinfo($match[1], PATHINFO_EXTENSION)), array('jpg', 'jpeg', 'png', 'gif'))) {
135                     if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
136                         Tinebase_Core::getLogger()->info(__METHOD__ . ' ' . __LINE__ . ' unsupported file extension: ' . $match[1]);
137                     continue;
138                 }
139                 if (preg_match('#Relationship Id="' . $match[2] . '"[^>]+Target="(media/[^"]+)"#', $relData, $relMatch)) {
140                     $fileContent = file_get_contents($match[1]);
141                     if (!empty($fileContent)) {
142                         $this->zipClass->deleteName('word/' . $relMatch[1]);
143                         $this->zipClass->addFromString('word/' . $relMatch[1], $fileContent);
144                     } else {
145                         if (Tinebase_Core::isLogLevel(Zend_Log::WARN))
146                             Tinebase_Core::getLogger()->warn(__METHOD__ . ' ' . __LINE__ . ' could not get file content: ' . $match[1]);
147                     }
148                 } else {
149                     if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
150                         Tinebase_Core::getLogger()->info(__METHOD__ . ' ' . __LINE__ . ' could not find relation matching found url: ' . $match[1]);
151                 }
152             }
153         }
154     }
155
156     /**
157      * Saves the result document.
158      *
159      * @return string
160      *
161      * @throws \PhpOffice\PhpWord\Exception\Exception
162      */
163     public function save()
164     {
165         if (null !== $this->_temporaryDocumentRels) {
166             $this->zipClass->addFromString('word/_rels/document.xml.rels', $this->_temporaryDocumentRels);
167         }
168
169         foreach($this->_tempHeaderRels as $index => $data) {
170             $this->zipClass->addFromString('word/_rels/header' . $index . '.xml.rels', $data);
171         }
172
173         foreach($this->_tempFooterRels as $index => $data) {
174             $this->zipClass->addFromString('word/_rels/footer' . $index . '.xml.rels', $data);
175         }
176
177         return parent::save();
178     }
179
180     /**
181      * @param string $data
182      */
183     public function setMainPart($data)
184     {
185         $this->tempDocumentMainPart = $data;
186     }
187
188     /**
189      * @return string
190      */
191     public function getMainPart()
192     {
193         return $this->tempDocumentMainPart;
194     }
195
196     /**
197      * replace a table row in a template document and return replaced row
198      *
199      * @param string $search
200      * @param string $replacement
201      *
202      * @return string
203      *
204      * @throws \PhpOffice\PhpWord\Exception\Exception
205      */
206     public function replaceRow($search, $replacement)
207     {
208         if ('${' !== substr($search, 0, 2) && '}' !== substr($search, -1)) {
209             $search = '${' . $search . '}';
210         }
211
212         $tagPos = strpos($this->tempDocumentMainPart, $search);
213         if (!$tagPos) {
214             throw new \PhpOffice\PhpWord\Exception\Exception("Can not clone row, template variable not found or variable contains markup.");
215         }
216
217         $rowStart = $this->findRowStart($tagPos);
218         $rowEnd = $this->findRowEnd($tagPos);
219         $xmlRow = $this->getSlice($rowStart, $rowEnd);
220
221         $result = $this->getSlice(0, $rowStart) . $replacement;
222         $result .= $this->getSlice($rowEnd);
223
224         $this->tempDocumentMainPart = $result;
225
226         return $xmlRow;
227     }
228
229     /**
230      * @param $data
231      */
232     public function append($data)
233     {
234         $this->tempDocumentMainPart .= $data;
235     }
236
237     /**
238      * @param string $row
239      * @param int $num
240      * @param string $where
241      *
242     public function insertRow($row, $num, $where)
243     {
244         $row = preg_replace('/\$\{(.*?)\}/', '\${\\1#' . $num . '}', $row);
245
246         $this->setValue($where, $row . $where);
247     }*/
248
249     /**
250      * Clone a block.
251      *
252      * @param string $blockname
253      * @param integer $clones
254      * @param boolean $replace
255      *
256      * @return string|null
257      */
258     public function cloneBlock($blockname, $clones = 1, $replace = true)
259     {
260         $xmlBlock = null;
261         preg_match(
262             '/(<\?xml.*)(<w:p(\s+[^>]*)?>.*?\${' . $blockname . '}.*?<\/w:p>)(.*)(<w:p(\s+[^>]*)?>.*?\${\/' . $blockname . '}.*?<\/w:p>)/is',
263             $this->tempDocumentMainPart,
264             $matches
265         );
266
267         if (isset($matches[4])) {
268             $xmlBlock = $matches[4];
269             $cloned = array();
270             for ($i = 1; $i <= $clones; $i++) {
271                 $cloned[] = $xmlBlock;
272             }
273
274             if ($replace) {
275                 if (($pos = strrpos($matches[2], '<w:p>')) !== 0) {
276                     $matches[2] = substr($matches[2], $pos);
277                 }
278                 $this->tempDocumentMainPart = str_replace(
279                     $matches[2] . $matches[4] . $matches[5],
280                     implode('', $cloned),
281                     $this->tempDocumentMainPart
282                 );
283             }
284         }
285
286         return $xmlBlock;
287     }
288
289     /**
290      * Replace a block.
291      *
292      * @param string $blockname
293      * @param string $replacement
294      *
295      * @return void
296      */
297     public function replaceBlock($blockname, $replacement)
298     {
299         preg_match(
300             '/(<\?xml.*)(<w:p(\s+[^>]*)?>.*?\${' . $blockname . '}.*?<\/w:p>)(.*)(<w:p(\s+[^>]*)?>.*?\${\/' . $blockname . '}.*?<\/w:p>)/is',
301             $this->tempDocumentMainPart,
302             $matches
303         );
304
305         if (isset($matches[4])) {
306             $this->tempDocumentMainPart = str_replace(
307                 $matches[2] . $matches[4] . $matches[5],
308                 $replacement,
309                 $this->tempDocumentMainPart
310             );
311         }
312     }
313
314     /**
315      * @param array $config
316      */
317     public function setConfig(array $config)
318     {
319         $this->_config = $config;
320         if (Tinebase_Export_Richtext_TemplateProcessor::TYPE_RECORD === $this->_type &&
321                 isset($this->_config['recordXml'])) {
322
323         }
324     }
325
326     /**
327      * @param $key
328      * @return bool
329      */
330     public function hasConfig($key)
331     {
332         return isset($this->_config[$key]);
333     }
334
335     /**
336      * @param string|null $key
337      * @return mixed
338      */
339     public function getConfig($key = null)
340     {
341         if (null === $key) {
342             return $this->_config;
343         }
344         return $this->_config[$key];
345     }
346
347     /**
348      * Returns array of all variables in template.
349      *
350      * @return string[]
351      */
352     public function getVariables()
353     {
354         $result = parent::getVariables();
355
356         switch($this->_type) {
357             /** @noinspection PhpMissingBreakStatementInspection */
358             case self::TYPE_DATASOURCE:
359                 if (isset($this->_config['group'])) {
360                     $result = array_merge($result, $this->_config['group']->getVariables());
361                 }
362             case self::TYPE_GROUP:
363                 if (isset($this->_config['record'])) {
364                     $result = array_merge($result, $this->_config['record']->getVariables());
365                 }
366                 if (isset($this->_config['recordRow']) && isset($this->_config['recordRow']['recordRowProcessor'])) {
367                     /** @noinspection PhpUndefinedMethodInspection */
368                     $result = array_merge($result, $this->_config['recordRow']['recordRowProcessor']->getVariables());
369                 }
370                 // DO NOT return variables of sub groups or sub records
371                 break;
372             case self::TYPE_SUBGROUP:
373             case self::TYPE_SUBRECORD:
374                 if (isset($this->_config['recordXml'])) {
375                     $result = array_merge($result, $this->getVariablesForPart($this->_config['recordXml']));
376                 }
377                 break;
378         }
379
380         return array_unique($result);
381     }
382 }