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 é accent<br />\n<br />\nUn texto en español ?" 410 // Result after clean must be "MYPODUCT - Desc bold with é accent\n\nUn texto en españ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