1<?php
2
3require 'Segment.php';
4
5class OdfException extends Exception
6{
7}
8
9/**
10 * Templating class for odt file
11 * You need PHP 5.2 at least
12 * You need Zip Extension or PclZip library
13 *
14 * @copyright  2008 - Julien Pauli - Cyril PIERRE de GEYER - Anaska (http://www.anaska.com)
15 * @copyright  2010-2015 - Laurent Destailleur - eldy@users.sourceforge.net
16 * @copyright  2010 - Vikas Mahajan - http://vikasmahajan.wordpress.com
17 * @copyright  2012 - Stephen Larroque - lrq3000@gmail.com
18 * @license    https://www.gnu.org/copyleft/gpl.html  GPL License
19 * @version 1.5.0
20 */
21class Odf
22{
23	protected $config = array(
24	'ZIP_PROXY' => 'PclZipProxy',	// PclZipProxy, PhpZipProxy
25	'DELIMITER_LEFT' => '{',
26	'DELIMITER_RIGHT' => '}',
27	'PATH_TO_TMP' => '/tmp'
28	);
29	protected $file;
30	protected $contentXml;			// To store content of content.xml file
31	protected $metaXml;			    // To store content of meta.xml file
32	protected $stylesXml;			// To store content of styles.xml file
33	protected $manifestXml;			// To store content of META-INF/manifest.xml file
34	protected $tmpfile;
35	protected $tmpdir='';
36	protected $images = array();
37	protected $vars = array();
38	protected $segments = array();
39
40	public $creator;
41	public $title;
42	public $subject;
43	public $userdefined=array();
44
45	const PIXEL_TO_CM = 0.026458333;
46
47	/**
48	 * Class constructor
49	 *
50	 * @param string $filename     The name of the odt file
51	 * @param string $config       Array of config data
52	 * @throws OdfException
53	 */
54	public function __construct($filename, $config = array())
55	{
56		clearstatcache();
57
58		if (! is_array($config)) {
59			throw new OdfException('Configuration data must be provided as array');
60		}
61		foreach ($config as $configKey => $configValue) {
62			if (array_key_exists($configKey, $this->config)) {
63				$this->config[$configKey] = $configValue;
64			}
65		}
66
67		$md5uniqid = md5(uniqid());
68		if ($this->config['PATH_TO_TMP']) $this->tmpdir = preg_replace('|[\/]$|','',$this->config['PATH_TO_TMP']);	// Remove last \ or /
69		$this->tmpdir .= ($this->tmpdir?'/':'').$md5uniqid;
70		$this->tmpfile = $this->tmpdir.'/'.$md5uniqid.'.odt';	// We keep .odt extension to allow OpenOffice usage during debug.
71
72		// A working directory is required for some zip proxy like PclZipProxy
73		if (in_array($this->config['ZIP_PROXY'],array('PclZipProxy')) && ! is_dir($this->config['PATH_TO_TMP'])) {
74			throw new OdfException('Temporary directory '.$this->config['PATH_TO_TMP'].' must exists');
75		}
76
77		// Create tmp direcoty (will be deleted in destructor)
78		if (!file_exists($this->tmpdir)) {
79			$result=mkdir($this->tmpdir);
80		}
81
82		// Load zip proxy
83		$zipHandler = $this->config['ZIP_PROXY'];
84		if (!defined('PCLZIP_TEMPORARY_DIR')) define('PCLZIP_TEMPORARY_DIR',$this->tmpdir);
85		include_once('zip/'.$zipHandler.'.php');
86		if (! class_exists($this->config['ZIP_PROXY'])) {
87			throw new OdfException($this->config['ZIP_PROXY'] . ' class not found - check your php settings');
88		}
89		$this->file = new $zipHandler($this->tmpdir);
90
91
92		if ($this->file->open($filename) !== true) {	// This also create the tmpdir directory
93			throw new OdfException("Error while Opening the file '$filename' - Check your odt filename");
94		}
95		if (($this->contentXml = $this->file->getFromName('content.xml')) === false) {
96			throw new OdfException("Nothing to parse - Check that the content.xml file is correctly formed in source file '$filename'");
97		}
98		if (($this->manifestXml = $this->file->getFromName('META-INF/manifest.xml')) === false) {
99			throw new OdfException("Something is wrong with META-INF/manifest.xml in source file '$filename'");
100		}
101		if (($this->metaXml = $this->file->getFromName('meta.xml')) === false) {
102			throw new OdfException("Nothing to parse - Check that the meta.xml file is correctly formed in source file '$filename'");
103		}
104		if (($this->stylesXml = $this->file->getFromName('styles.xml')) === false) {
105			throw new OdfException("Nothing to parse - Check that the styles.xml file is correctly formed in source file '$filename'");
106		}
107		$this->file->close();
108
109
110		//print "tmpdir=".$tmpdir;
111		//print "filename=".$filename;
112		//print "tmpfile=".$tmpfile;
113
114		copy($filename, $this->tmpfile);
115
116		// Now file has been loaded, we must move the [!-- BEGIN and [!-- END tags outside the
117		// <table:table-row tag and clean bad lines tags.
118		$this->_moveRowSegments();
119	}
120
121	/**
122	 * Assing a template variable
123	 *
124	 * @param string   $key        Name of the variable within the template
125	 * @param string   $value      Replacement value
126	 * @param bool     $encode     If true, special XML characters are encoded
127	 * @param string   $charset    Charset
128	 * @throws OdfException
129	 * @return odf
130	 */
131	public function setVars($key, $value, $encode = true, $charset = 'ISO-8859')
132	{
133		$tag = $this->config['DELIMITER_LEFT'] . $key . $this->config['DELIMITER_RIGHT'];
134		// TODO Warning string may be:
135		// <text:span text:style-name="T13">{</text:span><text:span text:style-name="T12">aaa</text:span><text:span text:style-name="T13">}</text:span>
136		// instead of {aaa} so we should enhance this function.
137		//print $key.'-'.$value.'-'.strpos($this->contentXml, $this->config['DELIMITER_LEFT'] . $key . $this->config['DELIMITER_RIGHT']).'<br>';
138		if (strpos($this->contentXml, $tag) === false && strpos($this->stylesXml, $tag) === false) {
139			// Add the throw only for development. In most cases, it is normal to not having the key into the document (only few keys are presents).
140			//throw new OdfException("var $key not found in the document");
141			return $this;
142		}
143
144		$this->vars[$tag] = $this->convertVarToOdf($value, $encode, $charset);
145
146		return $this;
147	}
148
149	/**
150     * Replaces html tags in odt tags and returns a compatible string
151     * @param string   $key        Name of the variable within the template
152	 * @param string   $value      Replacement value
153	 * @param bool     $encode     If true, special XML characters are encoded
154	 * @param string   $charset    Charset
155     * @return string
156     */
157	public function convertVarToOdf($value, $encode = true, $charset = 'ISO-8859')
158	{
159		$value = $encode ? htmlspecialchars($value) : $value;
160		$value = ($charset == 'ISO-8859') ? utf8_encode($value) : $value;
161		$convertedValue = $value;
162
163		// Check if the value includes html tags
164		if ($this->_hasHtmlTag($value) === true) {
165			// Default styles for strong/b, i/em, u, s, sub & sup
166			$automaticStyles = array(
167				'<style:style style:name="boldText" style:family="text"><style:text-properties fo:font-weight="bold" style:font-weight-asian="bold" style:font-weight-complex="bold" /></style:style>',
168				'<style:style style:name="italicText" style:family="text"><style:text-properties fo:font-style="italic" style:font-style-asian="italic" style:font-style-complex="italic" /></style:style>',
169				'<style:style style:name="underlineText" style:family="text"><style:text-properties style:text-underline-style="solid" style:text-underline-width="auto" style:text-underline-color="font-color" /></style:style>',
170				'<style:style style:name="strikethroughText" style:family="text"><style:text-properties style:text-line-through-style="solid" style:text-line-through-type="single" /></style:style>',
171				'<style:style style:name="subText" style:family="text"><style:text-properties style:text-position="sub 58%" /></style:style>',
172				'<style:style style:name="supText" style:family="text"><style:text-properties style:text-position="super 58%" /></style:style>'
173			);
174
175			$convertedValue = $this->_replaceHtmlWithOdtTag($this->_getDataFromHtml($value), $customStyles, $fontDeclarations);
176
177			foreach ($customStyles as $key => $val) {
178				array_push($automaticStyles, '<style:style style:name="customStyle' . $key . '" style:family="text">' . $val . '</style:style>');
179			}
180
181			// Join the styles and add them to the content xml
182			$styles = '';
183			foreach ($automaticStyles as $style) {
184				if (strpos($this->contentXml, $style) === false) {
185					$styles .= $style;
186				}
187			}
188			$this->contentXml = str_replace('</office:automatic-styles>', $styles . '</office:automatic-styles>', $this->contentXml);
189
190			// Join the font declarations and add them to the content xml
191			$fonts = '';
192			foreach ($fontDeclarations as $font) {
193				if (strpos($this->contentXml, 'style:name="' . $font . '"') === false) {
194					$fonts .= '<style:font-face style:name="' . $font . '" svg:font-family="\'' . $font . '\'" />';
195				}
196			}
197			$this->contentXml = str_replace('</office:font-face-decls>', $fonts . '</office:font-face-decls>', $this->contentXml);
198		}
199		else $convertedValue = preg_replace('/(\r\n|\r|\n)/i', "<text:line-break/>", $value);
200
201		return $convertedValue;
202	}
203
204	/**
205     * Replaces html tags in with odt tags and returns an odt string
206     * @param array $tags   An array with html tags generated by the getDataFromHtml() function
207     * @param array $customStyles   An array of style defenitions that should be included inside the odt file
208     * @param array $fontDeclarations   An array of font declarations that should be included inside the odt file
209     * @return string
210     */
211    private function _replaceHtmlWithOdtTag($tags, &$customStyles, &$fontDeclarations)
212	{
213        if ($customStyles == null) $customStyles = array();
214        if ($fontDeclarations == null) $fontDeclarations = array();
215
216        $odtResult = '';
217
218        foreach ((array) $tags as $tag) {
219            // Check if the current item is a tag or just plain text
220            if (isset($tag['text'])) {
221                $odtResult .= $tag['text'];
222            } elseif (isset($tag['name'])) {
223                switch ($tag['name']) {
224                    case 'br':
225                        $odtResult .= '<text:line-break/>';
226                        break;
227                    case 'strong':
228                    case 'b':
229                        $odtResult .= '<text:span text:style-name="boldText">' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $tag['innerText']) . '</text:span>';
230                        break;
231                    case 'i':
232                    case 'em':
233                        $odtResult .= '<text:span text:style-name="italicText">' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $tag['innerText']) . '</text:span>';
234                        break;
235                    case 'u':
236                        $odtResult .= '<text:span text:style-name="underlineText">' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $tag['innerText']) . '</text:span>';
237                        break;
238                    case 's':
239                        $odtResult .= '<text:span text:style-name="strikethroughText">' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $tag['innerText']) . '</text:span>';
240                        break;
241                    case 'sub':
242                        $odtResult .= '<text:span text:style-name="subText">' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $tag['innerText']) . '</text:span>';
243                        break;
244                    case 'sup':
245                        $odtResult .= '<text:span text:style-name="supText">' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $tag['innerText']) . '</text:span>';
246                        break;
247                    case 'span':
248                        if (isset($tag['attributes']['style'])) {
249                            $odtStyles = '';
250                            foreach ($tag['attributes']['style'] as $styleName => $styleValue) {
251                                switch ($styleName) {
252                                    case 'font-family':
253                                        $fontName = $styleValue;
254                                        if (strpos($fontName, ',') !== false) {
255                                            $fontName = explode(',', $fontName)[0];
256                                        }
257                                        if (!in_array($fontName, $fontDeclarations)) {
258                                            array_push($fontDeclarations, $fontName);
259                                        }
260                                        $odtStyles .= '<style:text-properties style:font-name="' . $fontName . '" />';
261                                        break;
262                                    case 'font-size':
263                                        if (preg_match('/([0-9]+)\s?(px|pt)/', $styleValue, $matches)) {
264                                            $fontSize = intval($matches[1]);
265                                            if ($matches[2] == 'px') {
266                                                $fontSize = round($fontSize * 0.75);
267                                            }
268                                            $odtStyles .= '<style:text-properties fo:font-size="' . $fontSize . 'pt" style:font-size-asian="' . $fontSize . 'pt" style:font-size-complex="' . $fontSize . 'pt" />';
269                                        }
270                                        break;
271                                    case 'color':
272                                        if (preg_match('/#[0-9A-Fa-f]{3}(?:[0-9A-Fa-f]{3})?/', $styleValue)) {
273                                            $odtStyles .= '<style:text-properties fo:color="' . $styleValue . '" />';
274                                        }
275                                        break;
276                                }
277                            }
278                            if (strlen($odtStyles) > 0) {
279								// Generate a unique id for the style (using microtime and random because some CPUs are really fast...)
280                                $key = floatval(str_replace('.', '', microtime(true)))+rand(0, 10);
281                                $customStyles[$key] = $odtStyles;
282                                $odtResult .= '<text:span text:style-name="customStyle' . $key . '">' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $tag['innerText']) . '</text:span>';
283                            }
284                        }
285                        break;
286                    default:
287                        $odtResult .= $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations);
288                        break;
289                }
290            }
291        }
292        return $odtResult;
293    }
294
295    /**
296     * Checks if the given text is a html string
297     * @param string    $text   The text to check
298     * @return bool
299     */
300    private function _isHtmlTag($text)
301	{
302        return preg_match('/<([A-Za-z]+)(?:\s([A-Za-z]+(?:\-[A-Za-z]+)?(?:=(?:".*?")|(?:[0-9]+))))*(?:(?:\s\/>)|(?:>(.*)<\/\1>))/', $text);
303    }
304
305    /**
306     * Checks if the given text includes a html string
307     * @param string    $text   The text to check
308     * @return bool
309     */
310    private function _hasHtmlTag($text)
311	{
312        $result = preg_match_all('/<([A-Za-z]+)(?:\s([A-Za-z]+(?:\-[A-Za-z]+)?(?:=(?:".*?")|(?:[0-9]+))))*(?:(?:\s\/>)|(?:>(.*)<\/\1>))/', $text);
313        return is_numeric($result) && $result > 0;
314    }
315
316    /**
317     * Returns an array of html elements
318     * @param string    $html   A string with html tags
319     * @return array
320     */
321    private function _getDataFromHtml($html)
322	{
323        $tags = array();
324        $tempHtml = $html;
325
326        while (strlen($tempHtml) > 0) {
327            // Check if the string includes a html tag
328            if (preg_match_all('/<([A-Za-z]+)(?:\s([A-Za-z]+(?:\-[A-Za-z]+)?(?:=(?:".*?")|(?:[0-9]+))))*(?:(?:\s\/>)|(?:>(.*)<\/\1>))/', $tempHtml, $matches)) {
329                $tagOffset = strpos($tempHtml, $matches[0][0]);
330                // Check if the string starts with the html tag
331                if ($tagOffset > 0) {
332                    // Push the text infront of the html tag to the result array
333                    array_push($tags, array(
334                        'text' => substr($tempHtml, 0, $tagOffset)
335                    ));
336                    // Remove the text from the string
337                    $tempHtml = substr($tempHtml, $tagOffset);
338                }
339                // Extract the attribute data from the html tag
340                preg_match_all('/([0-9A-Za-z]+(?:="[0-9A-Za-z\:\-\s\,\;\#]*")?)+/', $matches[2][0], $explodedAttributes);
341                $explodedAttributes = array_filter($explodedAttributes[0]);
342                $attributes = array();
343                // Store each attribute with its name in the $attributes array
344                $explodedAttributesCount = count($explodedAttributes);
345                for ($i=0; $i<$explodedAttributesCount; $i++) {
346                    $attribute = trim($explodedAttributes[$i]);
347                    // Check if the attribute has a value (like style="") or has no value (like required)
348                    if (strpos($attribute, '=') !== false) {
349                        $splitAttribute = explode('=', $attribute);
350                        $attrName = trim($splitAttribute[0]);
351                        $attrValue = trim(str_replace('"', '', $splitAttribute[1]));
352                        // check if the current attribute is a style attribute
353                        if (strtolower($attrName) == 'style') {
354                            $attributes[$attrName] = array();
355                            if (strpos($attrValue, ';') !== false) {
356                                // Split the style properties and store them in an array
357                                $explodedStyles = explode(';', $attrValue);
358                                $explodedStylesCount = count($explodedStyles);
359                                for ($n=0; $n<$explodedStylesCount; $n++) {
360                                    $splitStyle = explode(':', $explodedStyles[$n]);
361                                    $attributes[$attrName][trim($splitStyle[0])] = trim($splitStyle[1]);
362                                }
363                            } else {
364                                $splitStyle = explode(':', $attrValue);
365                                $attributes[$attrName][trim($splitStyle[0])] = trim($splitStyle[1]);
366                            }
367                        } else {
368                            // Store the value directly in the $attributes array if this is not the style attribute
369                            $attributes[$attrName] = $attrValue;
370                        }
371                    } else {
372                        $attributes[trim($attribute)] = true;
373                    }
374                }
375                // Push the html tag data to the result array
376                array_push($tags, array(
377                    'name' => $matches[1][0],
378                    'attributes' => $attributes,
379                    'innerText' => strip_tags($matches[3][0]),
380                    'children' => $this->_hasHtmlTag($matches[3][0]) ? $this->_getDataFromHtml($matches[3][0]) : null
381                ));
382                // Remove the processed html tag from the html string
383                $tempHtml = substr($tempHtml, strlen($matches[0][0]));
384            } else {
385                array_push($tags, array(
386                    'text' => $tempHtml
387                ));
388                $tempHtml = '';
389            }
390        }
391        return $tags;
392    }
393
394
395	/**
396	 * Function to convert a HTML string into an ODT string
397	 *
398	 * @param	string	$value	String to convert
399	 */
400	public function htmlToUTFAndPreOdf($value)
401	{
402		// We decode into utf8, entities
403		$value=dol_html_entity_decode($value, ENT_QUOTES|ENT_HTML5);
404
405		// We convert html tags
406		$ishtml=dol_textishtml($value);
407		if ($ishtml)
408		{
409	        // If string is "MYPODUCT - Desc <strong>bold</strong> with &eacute; accent<br />\n<br />\nUn texto en espa&ntilde;ol ?"
410    	    // Result after clean must be "MYPODUCT - Desc bold with é accent\n\nUn texto en espa&ntilde;ol ?"
411
412			// We want to ignore \n and we want all <br> to be \n
413			$value=preg_replace('/(\r\n|\r|\n)/i','',$value);
414			$value=preg_replace('/<br>/i',"\n",$value);
415			$value=preg_replace('/<br\s+[^<>\/]*>/i',"\n",$value);
416			$value=preg_replace('/<br\s+[^<>\/]*\/>/i',"\n",$value);
417
418			//$value=preg_replace('/<strong>/','__lt__text:p text:style-name=__quot__bold__quot____gt__',$value);
419			//$value=preg_replace('/<\/strong>/','__lt__/text:p__gt__',$value);
420
421			$value=dol_string_nohtmltag($value, 0);
422		}
423
424		return $value;
425	}
426
427
428	/**
429	 * Function to convert a HTML string into an ODT string
430	 *
431	 * @param	string	$value	String to convert
432	 */
433	public function preOdfToOdf($value)
434	{
435		$value = str_replace("\n", "<text:line-break/>", $value);
436
437		//$value = str_replace("__lt__", "<", $value);
438		//$value = str_replace("__gt__", ">", $value);
439		//$value = str_replace("__quot__", '"', $value);
440
441		return $value;
442	}
443
444	/**
445	 * Evaluating php codes inside the ODT and output the buffer (print, echo) inplace of the code
446	 *
447	 * @return int             0
448	 */
449	public function phpEval()
450	{
451		preg_match_all('/[\{\<]\?(php)?\s+(?P<content>.+)\?[\}\>]/iU',$this->contentXml, $matches); // detecting all {?php code ?} or <?php code ? >
452		$nbfound=count($matches['content']);
453		for ($i=0; $i < $nbfound; $i++)
454		{
455			try {
456				$ob_output = ''; // flush the output for each code. This var will be filled in by the eval($code) and output buffering : any print or echo or output will be redirected into this variable
457				$code = $matches['content'][$i];
458				ob_start();
459				eval ($code);
460				$ob_output = ob_get_contents(); // send the content of the buffer into $ob_output
461				$this->contentXml = str_replace($matches[0][$i], $ob_output, $this->contentXml);
462				ob_end_clean();
463			} catch (Exception $e) {
464				ob_end_clean();
465				$this->contentXml = str_replace($matches[0][$i], 'ERROR: there was a problem while evaluating this portion of code, please fix it: '.$e, $this->contentXml);
466			}
467		}
468		return 0;
469	}
470
471	/**
472	 * Assign a template variable as a picture
473	 *
474	 * @param string $key name of the variable within the template
475	 * @param string $value path to the picture
476	 * @throws OdfException
477	 * @return odf
478	 */
479	public function setImage($key, $value)
480	{
481		$filename = strtok(strrchr($value, '/'), '/.');
482		$file = substr(strrchr($value, '/'), 1);
483		$size = @getimagesize($value);
484		if ($size === false) {
485			throw new OdfException("Invalid image");
486		}
487		list ($width, $height) = $size;
488		$width *= self::PIXEL_TO_CM;
489		$height *= self::PIXEL_TO_CM;
490		$xml = <<<IMG
491			<draw:frame draw:style-name="fr1" draw:name="$filename" text:anchor-type="aschar" svg:width="{$width}cm" svg:height="{$height}cm" draw:z-index="3"><draw:image xlink:href="Pictures/$file" xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad"/></draw:frame>
492IMG;
493		$this->images[$value] = $file;
494		$this->setVars($key, $xml, false);
495		return $this;
496	}
497
498	/**
499	 * Move segment tags for lines of tables
500	 * This function is called automatically within the constructor, so this->contentXml is clean before any other thing
501	 *
502	 * @return void
503	 */
504	private function _moveRowSegments()
505	{
506	    // Replace BEGIN<text:s/>xxx into BEGIN xxx
507	    $this->contentXml = preg_replace('/\[!--\sBEGIN<text:s[^>]>(row.[\S]*)\s--\]/sm', '[!-- BEGIN \\1 --]', $this->contentXml);
508	    // Replace END<text:s/>xxx into END xxx
509	    $this->contentXml = preg_replace('/\[!--\sEND<text:s[^>]>(row.[\S]*)\s--\]/sm', '[!-- END \\1 --]', $this->contentXml);
510
511	    // Search all possible rows in the document
512		$reg1 = "#<table:table-row[^>]*>(.*)</table:table-row>#smU";
513		preg_match_all($reg1, $this->contentXml, $matches);
514		for ($i = 0, $size = count($matches[0]); $i < $size; $i++) {
515			// Check if the current row contains a segment row.*
516			$reg2 = '#\[!--\sBEGIN\s(row.[\S]*)\s--\](.*)\[!--\sEND\s\\1\s--\]#sm';
517			if (preg_match($reg2, $matches[0][$i], $matches2)) {
518				$balise = str_replace('row.', '', $matches2[1]);
519				// Move segment tags around the row
520				$replace = array(
521				'[!-- BEGIN ' . $matches2[1] . ' --]'	=> '',
522				'[!-- END ' . $matches2[1] . ' --]'		=> '',
523				'<table:table-row'							=> '[!-- BEGIN ' . $balise . ' --]<table:table-row',
524				'</table:table-row>'						=> '</table:table-row>[!-- END ' . $balise . ' --]'
525				);
526				$replacedXML = str_replace(array_keys($replace), array_values($replace), $matches[0][$i]);
527				$this->contentXml = str_replace($matches[0][$i], $replacedXML, $this->contentXml);
528			}
529		}
530	}
531
532	/**
533	 * Merge template variables
534	 * Called at the beginning of the _save function
535	 *
536	 * @param  string	$type		'content', 'styles' or 'meta'
537	 * @return void
538	 */
539	private function _parse($type='content')
540	{
541	    // Search all tags fou into condition to complete $this->vars, so we will proceed all tests even if not defined
542	    $reg='@\[!--\sIF\s([{}a-zA-Z0-9\.\,_]+)\s--\]@smU';
543	    preg_match_all($reg, $this->contentXml, $matches, PREG_SET_ORDER);
544
545	    //var_dump($this->vars);exit;
546	    foreach($matches as $match)   // For each match, if there is no entry into this->vars, we add it
547		{
548		    if (! empty($match[1]) && ! isset($this->vars[$match[1]]))
549			{
550			    $this->vars[$match[1]] = '';     // Not defined, so we set it to '', we just need entry into this->vars for next loop
551			}
552	    }
553	    //var_dump($this->vars);exit;
554
555		// Conditionals substitution
556		// Note: must be done before static substitution, else the variable will be replaced by its value and the conditional won't work anymore
557	    foreach($this->vars as $key => $value)
558		{
559			// If value is true (not 0 nor false nor null nor empty string)
560			if ($value)
561			{
562			    //dol_syslog("Var ".$key." is defined, we remove the IF, ELSE and ENDIF ");
563			    //$sav=$this->contentXml;
564				// Remove the IF tag
565				$this->contentXml = str_replace('[!-- IF '.$key.' --]', '', $this->contentXml);
566				// Remove everything between the ELSE tag (if it exists) and the ENDIF tag
567				$reg = '@(\[!--\sELSE\s' . $key . '\s--\](.*))?\[!--\sENDIF\s' . $key . '\s--\]@smU'; // U modifier = all quantifiers are non-greedy
568				$this->contentXml = preg_replace($reg, '', $this->contentXml);
569				/*if ($sav != $this->contentXml)
570				{
571				    dol_syslog("We found a IF and it was processed");
572				    //var_dump($sav);exit;
573				}*/
574			}
575			// Else the value is false, then two cases: no ELSE and we're done, or there is at least one place where there is an ELSE clause, then we replace it
576			else
577			{
578			    //dol_syslog("Var ".$key." is not defined, we remove the IF, ELSE and ENDIF ");
579			    //$sav=$this->contentXml;
580				// Find all conditional blocks for this variable: from IF to ELSE and to ENDIF
581				$reg = '@\[!--\sIF\s' . $key . '\s--\](.*)(\[!--\sELSE\s' . $key . '\s--\](.*))?\[!--\sENDIF\s' . $key . '\s--\]@smU'; // U modifier = all quantifiers are non-greedy
582				preg_match_all($reg, $this->contentXml, $matches, PREG_SET_ORDER);
583				foreach($matches as $match) { // For each match, if there is an ELSE clause, we replace the whole block by the value in the ELSE clause
584					if (!empty($match[3])) $this->contentXml = str_replace($match[0], $match[3], $this->contentXml);
585				}
586				// Cleanup the other conditional blocks (all the others where there were no ELSE clause, we can just remove them altogether)
587				$this->contentXml = preg_replace($reg, '', $this->contentXml);
588				/*if ($sav != $this->contentXml)
589				{
590				    dol_syslog("We found a IF and it was processed");
591				    //var_dump($sav);exit;
592				}*/
593			}
594		}
595
596		// Static substitution
597		if ($type == 'content')	$this->contentXml = str_replace(array_keys($this->vars), array_values($this->vars), $this->contentXml);
598		if ($type == 'styles')	$this->stylesXml = str_replace(array_keys($this->vars), array_values($this->vars), $this->stylesXml);
599		if ($type == 'meta')	$this->metaXml = str_replace(array_keys($this->vars), array_values($this->vars), $this->metaXml);
600
601	}
602
603	/**
604	 * Add the merged segment to the document
605	 *
606	 * @param Segment $segment     Segment
607	 * @throws OdfException
608	 * @return odf
609	 */
610	public function mergeSegment(Segment $segment)
611	{
612		if (! array_key_exists($segment->getName(), $this->segments)) {
613			throw new OdfException($segment->getName() . 'cannot be parsed, has it been set yet ?');
614		}
615		$string = $segment->getName();
616		// $reg = '@<text:p[^>]*>\[!--\sBEGIN\s' . $string . '\s--\](.*)\[!--.+END\s' . $string . '\s--\]<\/text:p>@smU';
617		$reg = '@\[!--\sBEGIN\s' . $string . '\s--\](.*)\[!--.+END\s' . $string . '\s--\]@smU';
618		$this->contentXml = preg_replace($reg, $segment->getXmlParsed(), $this->contentXml);
619		return $this;
620	}
621
622	/**
623	 * Display all the current template variables
624	 *
625	 * @return string
626	 */
627	public function printVars()
628	{
629		return print_r('<pre>' . print_r($this->vars, true) . '</pre>', true);
630	}
631
632	/**
633	 * Display the XML content of the file from odt document
634	 * as it is at the moment
635	 *
636	 * @return string
637	 */
638	public function __toString()
639	{
640		return $this->contentXml;
641	}
642
643	/**
644	 * Display loop segments declared with setSegment()
645	 *
646	 * @return string
647	 */
648	public function printDeclaredSegments()
649	{
650		return '<pre>' . print_r(implode(' ', array_keys($this->segments)), true) . '</pre>';
651	}
652
653	/**
654	 * Declare a segment in order to use it in a loop.
655	 * Extract the segment and store it into $this->segments[]. Return it for next call.
656	 *
657	 * @param  string      $segment        Segment
658	 * @throws OdfException
659	 * @return Segment
660	 */
661	public function setSegment($segment)
662	{
663		if (array_key_exists($segment, $this->segments)) {
664			return $this->segments[$segment];
665		}
666		// $reg = "#\[!--\sBEGIN\s$segment\s--\]<\/text:p>(.*)<text:p\s.*>\[!--\sEND\s$segment\s--\]#sm";
667		$reg = "#\[!--\sBEGIN\s$segment\s--\](.*)\[!--\sEND\s$segment\s--\]#sm";
668		if (preg_match($reg, html_entity_decode($this->contentXml), $m) == 0) {
669			throw new OdfException("'".$segment."' segment not found in the document. The tag [!-- BEGIN xxx --] or [!-- END xxx --] is not present into content file.");
670		}
671		$this->segments[$segment] = new Segment($segment, $m[1], $this);
672		return $this->segments[$segment];
673	}
674	/**
675	 * Save the odt file on the disk
676	 *
677	 * @param string $file name of the desired file
678	 * @throws OdfException
679	 * @return void
680	 */
681	public function saveToDisk($file = null)
682	{
683		if ($file !== null && is_string($file)) {
684			if (file_exists($file) && !(is_file($file) && is_writable($file))) {
685				throw new OdfException('Permission denied : can\'t create ' . $file);
686			}
687			$this->_save();
688			copy($this->tmpfile, $file);
689		} else {
690			$this->_save();
691		}
692	}
693
694	/**
695	 * Write output file onto disk
696	 *
697	 * @throws OdfException
698	 * @return void
699	 */
700	private function _save()
701	{
702		$res=$this->file->open($this->tmpfile);    // tmpfile is odt template
703		$this->_parse('content');
704		$this->_parse('styles');
705		$this->_parse('meta');
706
707		$this->setMetaData();
708		//print $this->metaXml;exit;
709
710		if (! $this->file->addFromString('content.xml', $this->contentXml)) {
711			throw new OdfException('Error during file export addFromString content');
712		}
713		if (! $this->file->addFromString('meta.xml', $this->metaXml)) {
714			throw new OdfException('Error during file export addFromString meta');
715		}
716		if (! $this->file->addFromString('styles.xml', $this->stylesXml)) {
717			throw new OdfException('Error during file export addFromString styles');
718		}
719
720		foreach ($this->images as $imageKey => $imageValue) {
721			// Add the image inside the ODT document
722			$this->file->addFile($imageKey, 'Pictures/' . $imageValue);
723			// Add the image to the Manifest (which maintains a list of images, necessary to avoid "Corrupt ODT file. Repair?" when opening the file with LibreOffice)
724			$this->addImageToManifest($imageValue);
725		}
726		if (! $this->file->addFromString('./META-INF/manifest.xml', $this->manifestXml)) {
727			throw new OdfException('Error during file export: manifest.xml');
728		}
729		$this->file->close();
730	}
731
732	/**
733	 * Update Meta information
734	 * <dc:date>2013-03-16T14:06:25</dc:date>
735	 *
736	 * @return void
737	 */
738	public function setMetaData()
739	{
740	    if (empty($this->creator)) $this->creator='';
741
742		$this->metaXml = preg_replace('/<dc:date>.*<\/dc:date>/', '<dc:date>'.gmdate("Y-m-d\TH:i:s").'</dc:date>', $this->metaXml);
743		$this->metaXml = preg_replace('/<dc:creator>.*<\/dc:creator>/', '<dc:creator>'.htmlspecialchars($this->creator).'</dc:creator>', $this->metaXml);
744		$this->metaXml = preg_replace('/<dc:title>.*<\/dc:title>/', '<dc:title>'.htmlspecialchars($this->title).'</dc:title>', $this->metaXml);
745		$this->metaXml = preg_replace('/<dc:subject>.*<\/dc:subject>/', '<dc:subject>'.htmlspecialchars($this->subject).'</dc:subject>', $this->metaXml);
746
747		if (count($this->userdefined))
748		{
749		    foreach($this->userdefined as $key => $val)
750		    {
751		      $this->metaXml = preg_replace('<meta:user-defined meta:name="'.$key.'"/>', '', $this->metaXml);
752		      $this->metaXml = preg_replace('/<meta:user-defined meta:name="'.$key.'">.*<\/meta:user-defined>/', '', $this->metaXml);
753		      $this->metaXml = str_replace('</office:meta>', '<meta:user-defined meta:name="'.$key.'">'.htmlspecialchars($val).'</meta:user-defined></office:meta>', $this->metaXml);
754		    }
755		}
756	}
757
758	/**
759	 * Update Manifest file according to added image files
760	 *
761	 * @param string	$file		Image file to add into manifest content
762	 * @return void
763	 */
764	public function addImageToManifest($file)
765	{
766		// Get the file extension
767		$ext = substr(strrchr($file, '.'), 1);
768		// Create the correct image XML entry to add to the manifest (this is necessary because ODT format requires that we keep a list of the images in the manifest.xml)
769		$add = ' <manifest:file-entry manifest:media-type="image/'.$ext.'" manifest:full-path="Pictures/'.$file.'"/>'."\n";
770		// Append the image to the manifest
771		$this->manifestXml = str_replace('</manifest:manifest>', $add.'</manifest:manifest>', $this->manifestXml); // we replace the manifest closing tag by the image XML entry + manifest closing tag (this results in appending the data, we do not overwrite anything)
772	}
773
774	/**
775	 * Export the file as attached file by HTTP
776	 *
777	 * @param string $name (optional)
778	 * @throws OdfException
779	 * @return void
780	 */
781	public function exportAsAttachedFile($name = "")
782	{
783		$this->_save();
784		if (headers_sent($filename, $linenum)) {
785			throw new OdfException("headers already sent ($filename at $linenum)");
786		}
787
788		if( $name == "" )
789		{
790			$name = md5(uniqid()) . ".odt";
791		}
792
793		header('Content-type: application/vnd.oasis.opendocument.text');
794		header('Content-Disposition: attachment; filename="'.$name.'"');
795		header('Content-Length: '.filesize($this->tmpfile));
796		readfile($this->tmpfile);
797	}
798
799	/**
800	 * Convert the ODT file to PDF and export the file as attached file by HTTP
801	 * Note: you need to have JODConverter and OpenOffice or LibreOffice installed and executable on the same system as where this php script will be executed. You also need to chmod +x odt2pdf.sh
802	 *
803	 * @param 	string 	$name 	Name of ODT file to generate before generating PDF
804	 * @throws OdfException
805	 * @return void
806	 */
807	public function exportAsAttachedPDF($name="")
808	{
809		global $conf;
810
811		if( $name == "" ) $name = "temp".md5(uniqid());
812
813		dol_syslog(get_class($this).'::exportAsAttachedPDF $name='.$name, LOG_DEBUG);
814		$this->saveToDisk($name);
815
816		$execmethod=(empty($conf->global->MAIN_EXEC_USE_POPEN)?1:2);	// 1 or 2
817		// Method 1 sometimes hang the server.
818
819
820		// Export to PDF using LibreOffice
821		if ($conf->global->MAIN_ODT_AS_PDF == 'libreoffice')
822		{
823			// Install prerequisites: apt install soffice libreoffice-common libreoffice-writer
824			// using windows libreoffice that must be in path
825			// using linux/mac libreoffice that must be in path
826			// Note PHP Config "fastcgi.impersonate=0" must set to 0 - Default is 1
827			$command ='soffice --headless -env:UserInstallation=file:"//'.$conf->user->dir_temp.'" --convert-to pdf --outdir '. escapeshellarg(dirname($name)). " ".escapeshellarg($name);
828		}
829		elseif (preg_match('/unoconv/', $conf->global->MAIN_ODT_AS_PDF))
830		{
831			// If issue with unoconv, see https://github.com/dagwieers/unoconv/issues/87
832
833			// MAIN_ODT_AS_PDF should be   "sudo -u unoconv /usr/bin/unoconv" and userunoconv must have sudo to be root by adding file /etc/sudoers.d/unoconv with content  www-data ALL=(unoconv) NOPASSWD: /usr/bin/unoconv .
834
835			// Try this with www-data user:    /usr/bin/unoconv -vvvv -f pdf /tmp/document-example.odt
836			// It must return:
837			//Verbosity set to level 4
838			//Using office base path: /usr/lib/libreoffice
839			//Using office binary path: /usr/lib/libreoffice/program
840			//DEBUG: Connection type: socket,host=127.0.0.1,port=2002;urp;StarOffice.ComponentContext
841			//DEBUG: Existing listener not found.
842			//DEBUG: Launching our own listener using /usr/lib/libreoffice/program/soffice.bin.
843			//LibreOffice listener successfully started. (pid=9287)
844			//Input file: /tmp/document-example.odt
845			//unoconv: file `/tmp/document-example.odt' does not exist.
846			//unoconv: RuntimeException during import phase:
847			//Office probably died. Unsupported URL <file:///tmp/document-example.odt>: "type detection failed"
848			//DEBUG: Terminating LibreOffice instance.
849			//DEBUG: Waiting for LibreOffice instance to exit
850
851			// If it fails:
852			// - set shell of user to bash instead of nologin.
853			// - set permission to read/write to user on home directory /var/www so user can create the libreoffice , dconf and .cache dir and files then set permission back
854
855			$command = $conf->global->MAIN_ODT_AS_PDF.' '.escapeshellcmd($name);
856			//$command = '/usr/bin/unoconv -vvv '.escapeshellcmd($name);
857		}
858		else
859		{
860			// deprecated old method using odt2pdf.sh (native, jodconverter, ...)
861			$tmpname=preg_replace('/\.odt/i', '', $name);
862
863			if (!empty($conf->global->MAIN_DOL_SCRIPTS_ROOT))
864			{
865				$command = $conf->global->MAIN_DOL_SCRIPTS_ROOT.'/scripts/odt2pdf/odt2pdf.sh '.escapeshellcmd($tmpname).' '.(is_numeric($conf->global->MAIN_ODT_AS_PDF)?'jodconverter':$conf->global->MAIN_ODT_AS_PDF);
866			}
867			else
868			{
869			    dol_syslog(get_class($this).'::exportAsAttachedPDF is used but the constant MAIN_DOL_SCRIPTS_ROOT with path to script directory was not defined.', LOG_WARNING);
870				$command = '../../scripts/odt2pdf/odt2pdf.sh '.escapeshellcmd($tmpname).' '.(is_numeric($conf->global->MAIN_ODT_AS_PDF)?'jodconverter':$conf->global->MAIN_ODT_AS_PDF);
871			}
872		}
873
874		//$dirname=dirname($name);
875		//$command = DOL_DOCUMENT_ROOT.'/includes/odtphp/odt2pdf.sh '.$name.' '.$dirname;
876
877		dol_syslog(get_class($this).'::exportAsAttachedPDF $execmethod='.$execmethod.' Run command='.$command,LOG_DEBUG);
878		$retval=0; $output_arr=array();
879		if ($execmethod == 1)
880		{
881			exec($command, $output_arr, $retval);
882		}
883		if ($execmethod == 2)
884		{
885			$outputfile = DOL_DATA_ROOT.'/odt2pdf.log';
886
887			$ok=0;
888			$handle = fopen($outputfile, 'w');
889			if ($handle)
890			{
891				dol_syslog(get_class($this)."Run command ".$command,LOG_DEBUG);
892				fwrite($handle, $command."\n");
893				$handlein = popen($command, 'r');
894				while (!feof($handlein))
895				{
896					$read = fgets($handlein);
897					fwrite($handle, $read);
898					$output_arr[]=$read;
899				}
900				pclose($handlein);
901				fclose($handle);
902			}
903			if (! empty($conf->global->MAIN_UMASK)) @chmod($outputfile, octdec($conf->global->MAIN_UMASK));
904		}
905
906		if ($retval == 0)
907		{
908			dol_syslog(get_class($this).'::exportAsAttachedPDF $ret_val='.$retval, LOG_DEBUG);
909			$filename=''; $linenum=0;
910			if (headers_sent($filename, $linenum)) {
911				throw new OdfException("headers already sent ($filename at $linenum)");
912			}
913
914			if (!empty($conf->global->MAIN_DISABLE_PDF_AUTOUPDATE)) {
915				$name=preg_replace('/\.od(x|t)/i', '', $name);
916				header('Content-type: application/pdf');
917				header('Content-Disposition: attachment; filename="'.$name.'.pdf"');
918				readfile($name.".pdf");
919			}
920			if (!empty($conf->global->MAIN_ODT_AS_PDF_DEL_SOURCE))
921			{
922				unlink($name);
923			}
924		} else {
925			dol_syslog(get_class($this).'::exportAsAttachedPDF $ret_val='.$retval, LOG_DEBUG);
926			dol_syslog(get_class($this).'::exportAsAttachedPDF $output_arr='.var_export($output_arr, true), LOG_DEBUG);
927
928			if ($retval==126) {
929				throw new OdfException('Permission execute convert script : ' . $command);
930			}
931			else {
932			    $errorstring='';
933				foreach($output_arr as $line) {
934				    $errorstring.= $line."<br>";
935				}
936				throw new OdfException('ODT to PDF convert fail (option MAIN_ODT_AS_PDF is '.$conf->global->MAIN_ODT_AS_PDF.', command was '.$command.', retval='.$retval.') : ' . $errorstring);
937			}
938		}
939	}
940
941	/**
942	 * Returns a variable of configuration
943	 *
944	 * @param  string  $configKey  Config key
945	 * @return string              The requested variable of configuration
946	 */
947	public function getConfig($configKey)
948	{
949		if (array_key_exists($configKey, $this->config)) {
950			return $this->config[$configKey];
951		}
952		return false;
953	}
954	/**
955	 * Returns the temporary working file
956	 *
957	 * @return string le chemin vers le fichier temporaire de travail
958	 */
959	public function getTmpfile()
960	{
961		return $this->tmpfile;
962	}
963
964	/**
965	 * Delete the temporary file when the object is destroyed
966	 */
967	public function __destruct()
968	{
969		if (file_exists($this->tmpfile)) {
970			unlink($this->tmpfile);
971		}
972
973		if (file_exists($this->tmpdir)) {
974			$this->_rrmdir($this->tmpdir);
975			rmdir($this->tmpdir);
976		}
977	}
978
979	/**
980	 * Empty the temporary working directory recursively
981	 *
982	 * @param  string  $dir    The temporary working directory
983	 * @return void
984	 */
985	private function _rrmdir($dir)
986	{
987		if ($handle = opendir($dir)) {
988			while (($file = readdir($handle)) !== false) {
989				if ($file != '.' && $file != '..') {
990					if (is_dir($dir . '/' . $file)) {
991						$this->_rrmdir($dir . '/' . $file);
992						rmdir($dir . '/' . $file);
993					} else {
994						unlink($dir . '/' . $file);
995					}
996				}
997			}
998			closedir($handle);
999		}
1000	}
1001
1002	/**
1003	 * return the value present on odt in [valuename][/valuename]
1004	 *
1005	 * @param  string $valuename   Balise in the template
1006	 * @return string              The value inside the balise
1007	 */
1008	public function getvalue($valuename)
1009	{
1010		$searchreg="/\\[".$valuename."\\](.*)\\[\\/".$valuename."\\]/";
1011		preg_match($searchreg, $this->contentXml, $matches);
1012		$this->contentXml = preg_replace($searchreg, "", $this->contentXml);
1013		return  $matches[1];
1014	}
1015}
1016