diff --git a/htdocs/includes/odtphp/odf.php b/htdocs/includes/odtphp/odf.php index 0bba0e59345..2f6f0dbea0b 100644 --- a/htdocs/includes/odtphp/odf.php +++ b/htdocs/includes/odtphp/odf.php @@ -23,975 +23,998 @@ class OdfException extends Exception */ class Odf { - protected $config = array( - 'ZIP_PROXY' => 'PclZipProxy', // PclZipProxy, PhpZipProxy - 'DELIMITER_LEFT' => '{', - 'DELIMITER_RIGHT' => '}', - 'PATH_TO_TMP' => '/tmp' - ); - protected $file; - protected $contentXml; // To store content of content.xml file - protected $metaXml; // To store content of meta.xml file - protected $stylesXml; // To store content of styles.xml file - protected $manifestXml; // To store content of META-INF/manifest.xml file - protected $tmpfile; - protected $tmpdir=''; - protected $images = array(); - protected $vars = array(); - protected $segments = array(); + protected $config = array( + 'ZIP_PROXY' => 'PclZipProxy', // PclZipProxy, PhpZipProxy + 'DELIMITER_LEFT' => '{', + 'DELIMITER_RIGHT' => '}', + 'PATH_TO_TMP' => '/tmp' + ); + protected $file; + protected $contentXml; // To store content of content.xml file + protected $metaXml; // To store content of meta.xml file + protected $stylesXml; // To store content of styles.xml file + protected $manifestXml; // To store content of META-INF/manifest.xml file + protected $tmpfile; + protected $tmpdir=''; + protected $images = array(); + protected $vars = array(); + protected $segments = array(); - public $creator; - public $title; - public $subject; - public $userdefined=array(); + public $creator; + public $title; + public $subject; + public $userdefined=array(); - const PIXEL_TO_CM = 0.026458333; - - /** - * Class constructor - * - * @param string $filename The name of the odt file - * @param string $config Array of config data - * @throws OdfException - */ - public function __construct($filename, $config = array()) - { - clearstatcache(); - - if (! is_array($config)) { - throw new OdfException('Configuration data must be provided as array'); - } - foreach ($config as $configKey => $configValue) { - if (array_key_exists($configKey, $this->config)) { - $this->config[$configKey] = $configValue; - } - } - - $md5uniqid = md5(uniqid()); - if ($this->config['PATH_TO_TMP']) $this->tmpdir = preg_replace('|[\/]$|', '', $this->config['PATH_TO_TMP']); // Remove last \ or / - $this->tmpdir .= ($this->tmpdir?'/':'').$md5uniqid; - $this->tmpfile = $this->tmpdir.'/'.$md5uniqid.'.odt'; // We keep .odt extension to allow OpenOffice usage during debug. - - // A working directory is required for some zip proxy like PclZipProxy - if (in_array($this->config['ZIP_PROXY'], array('PclZipProxy')) && ! is_dir($this->config['PATH_TO_TMP'])) { - throw new OdfException('Temporary directory '.$this->config['PATH_TO_TMP'].' must exists'); - } - - // Create tmp direcoty (will be deleted in destructor) - if (!file_exists($this->tmpdir)) { - $result = mkdir($this->tmpdir); - } - - // Load zip proxy - $zipHandler = $this->config['ZIP_PROXY']; - if (!defined('PCLZIP_TEMPORARY_DIR')) define('PCLZIP_TEMPORARY_DIR', $this->tmpdir); - include_once 'zip/'.$zipHandler.'.php'; - if (! class_exists($this->config['ZIP_PROXY'])) { - throw new OdfException($this->config['ZIP_PROXY'] . ' class not found - check your php settings'); - } - $this->file = new $zipHandler($this->tmpdir); + const PIXEL_TO_CM = 0.026458333; + const FIND_TAGS_REGEX = '/<([A-Za-z0-9]+)(?:\s([A-Za-z]+(?:\-[A-Za-z]+)?(?:=(?:".*?")|(?:[0-9]+))))*(?:(?:\s\/>)|(?:>(.*?)<\/\1>))/s'; + const FIND_ENCODED_TAGS_REGEX = '/<([A-Za-z]+)(?:\s([A-Za-z]+(?:\-[A-Za-z]+)?(?:=(?:".*?")|(?:[0-9]+))))*(?:(?:\s\/>)|(?:>(.*)<\/\1>))/'; - if ($this->file->open($filename) !== true) { // This also create the tmpdir directory - throw new OdfException("Error while Opening the file '$filename' - Check your odt filename"); - } - if (($this->contentXml = $this->file->getFromName('content.xml')) === false) { - throw new OdfException("Nothing to parse - Check that the content.xml file is correctly formed in source file '$filename'"); - } - if (($this->manifestXml = $this->file->getFromName('META-INF/manifest.xml')) === false) { - throw new OdfException("Something is wrong with META-INF/manifest.xml in source file '$filename'"); - } - if (($this->metaXml = $this->file->getFromName('meta.xml')) === false) { - throw new OdfException("Nothing to parse - Check that the meta.xml file is correctly formed in source file '$filename'"); - } - if (($this->stylesXml = $this->file->getFromName('styles.xml')) === false) { - throw new OdfException("Nothing to parse - Check that the styles.xml file is correctly formed in source file '$filename'"); - } - $this->file->close(); + /** + * Class constructor + * + * @param string $filename The name of the odt file + * @param string $config Array of config data + * @throws OdfException + */ + public function __construct($filename, $config = array()) + { + clearstatcache(); + + if (! is_array($config)) { + throw new OdfException('Configuration data must be provided as array'); + } + foreach ($config as $configKey => $configValue) { + if (array_key_exists($configKey, $this->config)) { + $this->config[$configKey] = $configValue; + } + } + + $md5uniqid = md5(uniqid()); + if ($this->config['PATH_TO_TMP']) $this->tmpdir = preg_replace('|[\/]$|', '', $this->config['PATH_TO_TMP']); // Remove last \ or / + $this->tmpdir .= ($this->tmpdir?'/':'').$md5uniqid; + $this->tmpfile = $this->tmpdir.'/'.$md5uniqid.'.odt'; // We keep .odt extension to allow OpenOffice usage during debug. + + // A working directory is required for some zip proxy like PclZipProxy + if (in_array($this->config['ZIP_PROXY'], array('PclZipProxy')) && ! is_dir($this->config['PATH_TO_TMP'])) { + throw new OdfException('Temporary directory '.$this->config['PATH_TO_TMP'].' must exists'); + } + + // Create tmp direcoty (will be deleted in destructor) + if (!file_exists($this->tmpdir)) { + $result = mkdir($this->tmpdir); + } + + // Load zip proxy + $zipHandler = $this->config['ZIP_PROXY']; + if (!defined('PCLZIP_TEMPORARY_DIR')) define('PCLZIP_TEMPORARY_DIR', $this->tmpdir); + include_once 'zip/'.$zipHandler.'.php'; + if (! class_exists($this->config['ZIP_PROXY'])) { + throw new OdfException($this->config['ZIP_PROXY'] . ' class not found - check your php settings'); + } + $this->file = new $zipHandler($this->tmpdir); - //print "tmpdir=".$tmpdir; - //print "filename=".$filename; - //print "tmpfile=".$tmpfile; - - copy($filename, $this->tmpfile); - - // Now file has been loaded, we must move the [!-- BEGIN and [!-- END tags outside the - // _moveRowSegments(); - } - - /** - * Assing a template variable into ->vars. - * For example, key is {object_date} and value is '2021-01-01' - * - * @param string $key Name of the variable within the template - * @param string $value Replacement value - * @param bool $encode If true, special XML characters are encoded - * @param string $charset Charset - * @throws OdfException - * @return odf - */ - public function setVars($key, $value, $encode = true, $charset = 'ISO-8859') - { - $tag = $this->config['DELIMITER_LEFT'] . $key . $this->config['DELIMITER_RIGHT']; - - // TODO Warning string may be: - // {aaa} - // instead of {aaa} so we should enhance this function. - //print $key.'-'.$value.'-'.strpos($this->contentXml, $this->config['DELIMITER_LEFT'] . $key . $this->config['DELIMITER_RIGHT']).'
'; - if (strpos($this->contentXml, $tag) === false && strpos($this->stylesXml, $tag) === false) { - // 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). - //throw new OdfException("var $key not found in the document"); - return $this; - } - - $this->vars[$tag] = $this->convertVarToOdf($value, $encode, $charset); - - return $this; - } - - /** - * Replaces html tags found into the $value with ODT compatible tags and return the converted compatible string - * - * @param string $value Replacement value - * @param bool $encode If true, special XML characters are encoded - * @param string $charset Charset - * @return string String in ODTsyntax format - */ - public function convertVarToOdf($value, $encode = true, $charset = 'ISO-8859') - { - $value = $encode ? htmlspecialchars($value) : $value; - $value = ($charset == 'ISO-8859') ? utf8_encode($value) : $value; - $convertedValue = $value; - - // Check if the value includes html tags - if ($this->_hasHtmlTag($value) === true) { - // Default styles for strong/b, i/em, u, s, sub & sup - $automaticStyles = array( - '', - '', - '', - '', - '', - '' - ); - - $customStyles = array(); - $fontDeclarations = array(); - - $convertedValue = $this->_replaceHtmlWithOdtTag($this->_getDataFromHtml($value), $customStyles, $fontDeclarations); - - foreach ($customStyles as $key => $val) { - array_push($automaticStyles, '' . $val . ''); - } - - // Join the styles and add them to the content xml - $styles = ''; - foreach ($automaticStyles as $style) { - if (strpos($this->contentXml, $style) === false) { - $styles .= $style; - } - } - $this->contentXml = str_replace('', $styles . '', $this->contentXml); - - // Join the font declarations and add them to the content xml - $fonts = ''; - foreach ($fontDeclarations as $font) { - if (strpos($this->contentXml, 'style:name="' . $font . '"') === false) { - $fonts .= ''; - } - } - $this->contentXml = str_replace('', $fonts . '', $this->contentXml); - } else { - $convertedValue = preg_replace('/(\r\n|\r|\n)/i', "", $value); - } - - return $convertedValue; - } - - /** - * Replaces html tags in with odt tags and returns an odt string - * - * @param array $tags An array with html tags generated by the getDataFromHtml() function - * @param array $customStyles An array of style defenitions that should be included inside the odt file - * @param array $fontDeclarations An array of font declarations that should be included inside the odt file - * @return string - */ - private function _replaceHtmlWithOdtTag($tags, &$customStyles, &$fontDeclarations) - { - if ($customStyles == null) $customStyles = array(); - if ($fontDeclarations == null) $fontDeclarations = array(); - - $odtResult = ''; - - foreach ((array) $tags as $tag) { - // Check if the current item is a tag or just plain text - if (isset($tag['text'])) { - $odtResult .= $tag['text']; - } elseif (isset($tag['name'])) { - switch ($tag['name']) { - case 'br': - $odtResult .= ''; - break; - case 'strong': - case 'b': - $odtResult .= '' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $tag['innerText']) . ''; - break; - case 'i': - case 'em': - $odtResult .= '' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $tag['innerText']) . ''; - break; - case 'u': - $odtResult .= '' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $tag['innerText']) . ''; - break; - case 's': - $odtResult .= '' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $tag['innerText']) . ''; - break; - case 'sub': - $odtResult .= '' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $tag['innerText']) . ''; - break; - case 'sup': - $odtResult .= '' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $tag['innerText']) . ''; - break; - case 'span': - if (isset($tag['attributes']['style'])) { - $odtStyles = ''; - foreach ($tag['attributes']['style'] as $styleName => $styleValue) { - switch ($styleName) { - case 'font-family': - $fontName = $styleValue; - if (strpos($fontName, ',') !== false) { - $fontName = explode(',', $fontName)[0]; - } - if (!in_array($fontName, $fontDeclarations)) { - array_push($fontDeclarations, $fontName); - } - $odtStyles .= ''; - break; - case 'font-size': - if (preg_match('/([0-9]+)\s?(px|pt)/', $styleValue, $matches)) { - $fontSize = intval($matches[1]); - if ($matches[2] == 'px') { - $fontSize = round($fontSize * 0.75); - } - $odtStyles .= ''; - } - break; - case 'color': - if (preg_match('/#[0-9A-Fa-f]{3}(?:[0-9A-Fa-f]{3})?/', $styleValue)) { - $odtStyles .= ''; - } - break; - } - } - if (strlen($odtStyles) > 0) { - // Generate a unique id for the style (using microtime and random because some CPUs are really fast...) - $key = floatval(str_replace('.', '', microtime(true)))+rand(0, 10); - $customStyles[$key] = $odtStyles; - $odtResult .= '' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $tag['innerText']) . ''; - } - } - break; - default: - $odtResult .= $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations); - break; - } - } - } - return $odtResult; - } - - /** - * Checks if the given text is a html string - * @param string $text The text to check - * @return bool - */ - private function _isHtmlTag($text) - { - return preg_match('/<([A-Za-z]+)(?:\s([A-Za-z]+(?:\-[A-Za-z]+)?(?:=(?:".*?")|(?:[0-9]+))))*(?:(?:\s\/>)|(?:>(.*)<\/\1>))/', $text); - } - - /** - * Checks if the given text includes a html string - * @param string $text The text to check - * @return bool - */ - private function _hasHtmlTag($text) - { - $result = preg_match_all('/<([A-Za-z]+)(?:\s([A-Za-z]+(?:\-[A-Za-z]+)?(?:=(?:".*?")|(?:[0-9]+))))*(?:(?:\s\/>)|(?:>(.*)<\/\1>))/', $text); - return is_numeric($result) && $result > 0; - } - - /** - * Returns an array of html elements - * @param string $html A string with html tags - * @return array - */ - private function _getDataFromHtml($html) - { - $tags = array(); - $tempHtml = $html; - - while (strlen($tempHtml) > 0) { - $matches = array(); - // Check if the string includes a html tag - if (preg_match_all('/<([A-Za-z]+)(?:\s([A-Za-z]+(?:\-[A-Za-z]+)?(?:=(?:".*?")|(?:[0-9]+))))*(?:(?:\s\/>)|(?:>(.*)<\/\1>))/', $tempHtml, $matches)) { - $tagOffset = strpos($tempHtml, $matches[0][0]); - // Check if the string starts with the html tag - if ($tagOffset > 0) { - // Push the text infront of the html tag to the result array - array_push($tags, array( - 'text' => substr($tempHtml, 0, $tagOffset) - )); - // Remove the text from the string - $tempHtml = substr($tempHtml, $tagOffset); - } - // Extract the attribute data from the html tag - $explodedAttributes = array(); - preg_match_all('/([0-9A-Za-z]+(?:="[0-9A-Za-z\:\-\s\,\;\#]*")?)+/', $matches[2][0], $explodedAttributes); - $explodedAttributes = array_filter($explodedAttributes[0]); - $attributes = array(); - // Store each attribute with its name in the $attributes array - $explodedAttributesCount = count($explodedAttributes); - for ($i=0; $i<$explodedAttributesCount; $i++) { - $attribute = trim($explodedAttributes[$i]); - // Check if the attribute has a value (like style="") or has no value (like required) - if (strpos($attribute, '=') !== false) { - $splitAttribute = explode('=', $attribute); - $attrName = trim($splitAttribute[0]); - $attrValue = trim(str_replace('"', '', $splitAttribute[1])); - // check if the current attribute is a style attribute - if (strtolower($attrName) == 'style') { - $attributes[$attrName] = array(); - if (strpos($attrValue, ';') !== false) { - // Split the style properties and store them in an array - $explodedStyles = explode(';', $attrValue); - $explodedStylesCount = count($explodedStyles); - for ($n=0; $n<$explodedStylesCount; $n++) { - $splitStyle = explode(':', $explodedStyles[$n]); - $attributes[$attrName][trim($splitStyle[0])] = trim($splitStyle[1]); - } - } else { - $splitStyle = explode(':', $attrValue); - $attributes[$attrName][trim($splitStyle[0])] = trim($splitStyle[1]); - } - } else { - // Store the value directly in the $attributes array if this is not the style attribute - $attributes[$attrName] = $attrValue; - } - } else { - $attributes[trim($attribute)] = true; - } - } - // Push the html tag data to the result array - array_push($tags, array( - 'name' => $matches[1][0], - 'attributes' => $attributes, - 'innerText' => strip_tags($matches[3][0]), - 'children' => $this->_hasHtmlTag($matches[3][0]) ? $this->_getDataFromHtml($matches[3][0]) : null - )); - // Remove the processed html tag from the html string - $tempHtml = substr($tempHtml, strlen($matches[0][0])); - } else { - array_push($tags, array( - 'text' => $tempHtml - )); - $tempHtml = ''; - } - } - return $tags; - } + if ($this->file->open($filename) !== true) { // This also create the tmpdir directory + throw new OdfException("Error while Opening the file '$filename' - Check your odt filename"); + } + if (($this->contentXml = $this->file->getFromName('content.xml')) === false) { + throw new OdfException("Nothing to parse - Check that the content.xml file is correctly formed in source file '$filename'"); + } + if (($this->manifestXml = $this->file->getFromName('META-INF/manifest.xml')) === false) { + throw new OdfException("Something is wrong with META-INF/manifest.xml in source file '$filename'"); + } + if (($this->metaXml = $this->file->getFromName('meta.xml')) === false) { + throw new OdfException("Nothing to parse - Check that the meta.xml file is correctly formed in source file '$filename'"); + } + if (($this->stylesXml = $this->file->getFromName('styles.xml')) === false) { + throw new OdfException("Nothing to parse - Check that the styles.xml file is correctly formed in source file '$filename'"); + } + $this->file->close(); - /** - * Function to convert a HTML string into an ODT string - * - * @param string $value String to convert - * @return string String converted - */ - public function htmlToUTFAndPreOdf($value) - { - // We decode into utf8, entities - $value=dol_html_entity_decode($value, ENT_QUOTES|ENT_HTML5); + //print "tmpdir=".$tmpdir; + //print "filename=".$filename; + //print "tmpfile=".$tmpfile; - // We convert html tags - $ishtml=dol_textishtml($value); - if ($ishtml) { - // If string is "MYPODUCT - Desc bold with é accent
\n
\nUn texto en español ?" - // Result after clean must be "MYPODUCT - Desc bold with é accent\n\nUn texto en español ?" + copy($filename, $this->tmpfile); - // We want to ignore \n and we want all
to be \n - $value=preg_replace('/(\r\n|\r|\n)/i', '', $value); - $value=preg_replace('/
/i', "\n", $value); - $value=preg_replace('/\/]*>/i', "\n", $value); - $value=preg_replace('/\/]*\/>/i', "\n", $value); + // Now file has been loaded, we must move the [!-- BEGIN and [!-- END tags outside the + // _moveRowSegments(); + } - //$value=preg_replace('//','__lt__text:p text:style-name=__quot__bold__quot____gt__',$value); - //$value=preg_replace('/<\/strong>/','__lt__/text:p__gt__',$value); + /** + * Assing a template variable into ->vars. + * For example, key is {object_date} and value is '2021-01-01' + * + * @param string $key Name of the variable within the template + * @param string $value Replacement value + * @param bool $encode If true, special XML characters are encoded + * @param string $charset Charset + * @throws OdfException + * @return odf + */ + public function setVars($key, $value, $encode = true, $charset = 'ISO-8859') + { + $tag = $this->config['DELIMITER_LEFT'] . $key . $this->config['DELIMITER_RIGHT']; - $value=dol_string_nohtmltag($value, 0); - } + // TODO Warning string may be: + // {aaa} + // instead of {aaa} so we should enhance this function. + //print $key.'-'.$value.'-'.strpos($this->contentXml, $this->config['DELIMITER_LEFT'] . $key . $this->config['DELIMITER_RIGHT']).'
'; + if (strpos($this->contentXml, $tag) === false && strpos($this->stylesXml, $tag) === false) { + // 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). + //throw new OdfException("var $key not found in the document"); + return $this; + } - return $value; - } + $this->vars[$tag] = $this->convertVarToOdf($value, $encode, $charset); + + return $this; + } + + /** + * Replaces html tags found into the $value with ODT compatible tags and return the converted compatible string + * + * @param string $value Replacement value + * @param bool $encode If true, special XML characters are encoded + * @param string $charset Charset + * @return string String in ODTsyntax format + */ + public function convertVarToOdf($value, $encode = true, $charset = 'ISO-8859') + { + $value = html_entity_decode($value, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401); + + // fix breaklines. + $value = preg_replace('//', "
", $value); + $convertedValue = $value; + + // Check if the value includes html tags + if ($this->_hasHtmlTag($value) === true) { + $value = strip_tags($value, '
'); // remove html tags except the one into the list in second parameter + + // Default styles for strong/b, i/em, u, s, sub & sup + $automaticStyles = array( + '', + '', + '', + '', + '', + '' + ); + + $customStyles = array(); + $fontDeclarations = array(); + + $convertedValue = $this->_replaceHtmlWithOdtTag($this->_getDataFromHtml($value), $customStyles, $fontDeclarations, $encode, $charset); + + foreach ($customStyles as $key => $val) { + array_push($automaticStyles, '' . $val . ''); + } + + // Join the styles and add them to the content xml + $styles = ''; + foreach ($automaticStyles as $style) { + if (strpos($this->contentXml, $style) === false) { + $styles .= $style; + } + } + $this->contentXml = str_replace('', $styles . '', $this->contentXml); + + // Join the font declarations and add them to the content xml + $fonts = ''; + foreach ($fontDeclarations as $font) { + if (strpos($this->contentXml, 'style:name="' . $font . '"') === false) { + $fonts .= ''; + } + } + $this->contentXml = str_replace('', $fonts . '', $this->contentXml); + } else { + $convertedValue = $this->encode_chars($convertedValue, $encode, $charset); + $convertedValue = preg_replace('/(\r\n|\r|\n)/i', "", $convertedValue); + } + + return $convertedValue; + } + + /** + * Replaces html tags in with odt tags and returns an odt string. Encodes and converts inner text. + * @param array $tags An array with html tags generated by the getDataFromHtml() function + * @param array $customStyles An array of style defenitions that should be included inside the odt file + * @param array $fontDeclarations An array of font declarations that should be included inside the odt file + * @param bool $encode If true, special XML characters are encoded + * @param string $charset Charset. See encode_chars() + * @return string + */ + private function _replaceHtmlWithOdtTag($tags, &$customStyles, &$fontDeclarations, $encode = false, $charset = '') + { + if ($customStyles == null) $customStyles = array(); + if ($fontDeclarations == null) $fontDeclarations = array(); + + $odtResult = ''; + + foreach ((array) $tags as $tag) { + // Check if the current item is a tag or just plain text + if (isset($tag['text'])) { + $text = $this->encode_chars($tag['text'], $encode, $charset); + $odtResult .= $text; + } elseif (isset($tag['name'])) { + switch ($tag['name']) { + case 'br': + $odtResult .= ''; + break; + case 'strong': + case 'b': + $odtResult .= '' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $this->encode_chars($tag['innerText'], $encode, $charset)) . ''; + break; + case 'i': + case 'em': + $odtResult .= '' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $this->encode_chars($tag['innerText'], $encode, $charset)) . ''; + break; + case 'u': + $odtResult .= '' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $this->encode_chars($tag['innerText'], $encode, $charset)) . ''; + break; + case 's': + $odtResult .= '' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $this->encode_chars($tag['innerText'], $encode, $charset)) . ''; + break; + case 'sub': + $odtResult .= '' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $this->encode_chars($tag['innerText'], $encode, $charset)) . ''; + break; + case 'sup': + $odtResult .= '' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $this->encode_chars($tag['innerText'], $encode, $charset)) . ''; + break; + case 'span': + if (isset($tag['attributes']['style'])) { + $odtStyles = ''; + foreach ($tag['attributes']['style'] as $styleName => $styleValue) { + switch ($styleName) { + case 'font-family': + $fontName = $styleValue; + if (strpos($fontName, ',') !== false) { + $fontName = explode(',', $fontName)[0]; + } + if (!in_array($fontName, $fontDeclarations)) { + array_push($fontDeclarations, $fontName); + } + $odtStyles .= ''; + break; + case 'font-size': + if (preg_match('/([0-9]+)\s?(px|pt)/', $styleValue, $matches)) { + $fontSize = intval($matches[1]); + if ($matches[2] == 'px') { + $fontSize = round($fontSize * 0.75); + } + $odtStyles .= ''; + } + break; + case 'color': + if (preg_match('/#[0-9A-Fa-f]{3}(?:[0-9A-Fa-f]{3})?/', $styleValue)) { + $odtStyles .= ''; + } + break; + } + } + if (strlen($odtStyles) > 0) { + // Generate a unique id for the style (using microtime and random because some CPUs are really fast...) + $key = floatval(str_replace('.', '', microtime(true))) + rand(0, 10); + $customStyles[$key] = $odtStyles; + $odtResult .= '' . ($tag['children'] != null ? $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations) : $this->encode_chars($tag['innerText'], $encode, $charset)) . ''; + } + } + break; + default: + $odtResult .= $this->_replaceHtmlWithOdtTag($tag['children'], $customStyles, $fontDeclarations); + break; + } + } + } + return $odtResult; + } + + /** + * Correctly encode chars + * @param string $text The text to encode or not + * @param bool $encode If true, special XML characters are encoded + * @param string $charset Charset + * @return string The converted text + * @see self::convertVarToOdf() + */ + private function encode_chars($text, $encode = false, $charset = '') + { + $newtext = $encode ? htmlspecialchars($text, ENT_QUOTES | ENT_XML1) : $text; + $newtext = ($charset == 'ISO-8859') ? utf8_encode($newtext) : $newtext; + return $newtext; + } + + /** + * Checks if the given text is a html string + * @param string $text The text to check + * @return bool + */ + private function _isHtmlTag($text) + { + return preg_match(self::FIND_TAGS_REGEX, $text); + } + + /** + * Checks if the given text includes a html string + * @param string $text The text to check + * @return bool + */ + private function _hasHtmlTag($text) + { + $result = preg_match_all(self::FIND_TAGS_REGEX, $text); + return is_numeric($result) && $result > 0; + } + + /** + * Returns an array of html elements + * @param string $html A string with html tags + * @return array + */ + private function _getDataFromHtml($html) + { + $tags = array(); + $tempHtml = $html; + + while (strlen($tempHtml) > 0) { + // Check if the string includes a html tag + if (preg_match_all(self::FIND_TAGS_REGEX, $tempHtml, $matches)) { + $tagOffset = strpos($tempHtml, $matches[0][0]); + // Check if the string starts with the html tag + if ($tagOffset > 0) { + // Push the text infront of the html tag to the result array + array_push($tags, array( + 'text' => substr($tempHtml, 0, $tagOffset) + )); + // Remove the text from the string + $tempHtml = substr($tempHtml, $tagOffset); + } + // Extract the attribute data from the html tag + preg_match_all('/([0-9A-Za-z]+(?:="[0-9A-Za-z\:\-\s\,\;\#]*")?)+/', $matches[2][0], $explodedAttributes); + $explodedAttributes = array_filter($explodedAttributes[0]); + $attributes = array(); + // Store each attribute with its name in the $attributes array + $explodedAttributesCount = count($explodedAttributes); + for ($i = 0; $i < $explodedAttributesCount; $i++) { + $attribute = trim($explodedAttributes[$i]); + // Check if the attribute has a value (like style="") or has no value (like required) + if (strpos($attribute, '=') !== false) { + $splitAttribute = explode('=', $attribute); + $attrName = trim($splitAttribute[0]); + $attrValue = trim(str_replace('"', '', $splitAttribute[1])); + // check if the current attribute is a style attribute + if (strtolower($attrName) == 'style') { + $attributes[$attrName] = array(); + if (strpos($attrValue, ';') !== false) { + // Split the style properties and store them in an array + $explodedStyles = explode(';', $attrValue); + $explodedStylesCount = count($explodedStyles); + for ($n = 0; $n < $explodedStylesCount; $n++) { + $splitStyle = explode(':', $explodedStyles[$n]); + $attributes[$attrName][trim($splitStyle[0])] = trim($splitStyle[1]); + } + } else { + $splitStyle = explode(':', $attrValue); + $attributes[$attrName][trim($splitStyle[0])] = trim($splitStyle[1]); + } + } else { + // Store the value directly in the $attributes array if this is not the style attribute + $attributes[$attrName] = $attrValue; + } + } else { + $attributes[trim($attribute)] = true; + } + } + // Push the html tag data to the result array + array_push($tags, array( + 'name' => $matches[1][0], + 'attributes' => $attributes, + 'innerText' => strip_tags($matches[3][0]), + 'children' => $this->_hasHtmlTag($matches[3][0]) ? $this->_getDataFromHtml($matches[3][0]) : null + )); + // Remove the processed html tag from the html string + $tempHtml = substr($tempHtml, strlen($matches[0][0])); + } else { + array_push($tags, array( + 'text' => $tempHtml + )); + $tempHtml = ''; + } + } + return $tags; + } - /** - * Function to convert a HTML string into an ODT string - * - * @param string $value String to convert - * @return string String converted - */ - public function preOdfToOdf($value) - { - $value = str_replace("\n", "", $value); + /** + * Function to convert a HTML string into an ODT string + * + * @param string $value String to convert + * @return string String converted + */ + public function htmlToUTFAndPreOdf($value) + { + // We decode into utf8, entities + $value=dol_html_entity_decode($value, ENT_QUOTES|ENT_HTML5); - //$value = str_replace("__lt__", "<", $value); - //$value = str_replace("__gt__", ">", $value); - //$value = str_replace("__quot__", '"', $value); + // We convert html tags + $ishtml=dol_textishtml($value); + if ($ishtml) { + // If string is "MYPODUCT - Desc bold with é accent
\n
\nUn texto en español ?" + // Result after clean must be "MYPODUCT - Desc bold with é accent\n\nUn texto en español ?" - return $value; - } + // We want to ignore \n and we want all
to be \n + $value=preg_replace('/(\r\n|\r|\n)/i', '', $value); + $value=preg_replace('/
/i', "\n", $value); + $value=preg_replace('/\/]*>/i', "\n", $value); + $value=preg_replace('/\/]*\/>/i', "\n", $value); - /** - * Assign a template variable as a picture - * - * @param string $key name of the variable within the template - * @param string $value path to the picture - * @throws OdfException - * @return odf - */ - public function setImage($key, $value) - { - $filename = strtok(strrchr($value, '/'), '/.'); - $file = substr(strrchr($value, '/'), 1); - $size = @getimagesize($value); - if ($size === false) { - throw new OdfException("Invalid image"); - } - list ($width, $height) = $size; - $width *= self::PIXEL_TO_CM; - $height *= self::PIXEL_TO_CM; - $xml = <</','__lt__text:p text:style-name=__quot__bold__quot____gt__',$value); + //$value=preg_replace('/<\/strong>/','__lt__/text:p__gt__',$value); + + $value=dol_string_nohtmltag($value, 0); + } + + return $value; + } + + + /** + * Function to convert a HTML string into an ODT string + * + * @param string $value String to convert + * @return string String converted + */ + public function preOdfToOdf($value) + { + $value = str_replace("\n", "", $value); + + //$value = str_replace("__lt__", "<", $value); + //$value = str_replace("__gt__", ">", $value); + //$value = str_replace("__quot__", '"', $value); + + return $value; + } + + /** + * Assign a template variable as a picture + * + * @param string $key name of the variable within the template + * @param string $value path to the picture + * @throws OdfException + * @return odf + */ + public function setImage($key, $value) + { + $filename = strtok(strrchr($value, '/'), '/.'); + $file = substr(strrchr($value, '/'), 1); + $size = @getimagesize($value); + if ($size === false) { + throw new OdfException("Invalid image"); + } + list ($width, $height) = $size; + $width *= self::PIXEL_TO_CM; + $height *= self::PIXEL_TO_CM; + $xml = << IMG; - $this->images[$value] = $file; - $this->setVars($key, $xml, false); - return $this; - } + $this->images[$value] = $file; + $this->setVars($key, $xml, false); + return $this; + } - /** - * Move segment tags for lines of tables - * This function is called automatically within the constructor, so this->contentXml is clean before any other thing - * - * @return void - */ - private function _moveRowSegments() - { - // Replace BEGINxxx into BEGIN xxx - $this->contentXml = preg_replace('/\[!--\sBEGIN]>(row.[\S]*)\s--\]/sm', '[!-- BEGIN \\1 --]', $this->contentXml); - // Replace ENDxxx into END xxx - $this->contentXml = preg_replace('/\[!--\sEND]>(row.[\S]*)\s--\]/sm', '[!-- END \\1 --]', $this->contentXml); + /** + * Move segment tags for lines of tables + * This function is called automatically within the constructor, so this->contentXml is clean before any other thing + * + * @return void + */ + private function _moveRowSegments() + { + // Replace BEGINxxx into BEGIN xxx + $this->contentXml = preg_replace('/\[!--\sBEGIN]>(row.[\S]*)\s--\]/sm', '[!-- BEGIN \\1 --]', $this->contentXml); + // Replace ENDxxx into END xxx + $this->contentXml = preg_replace('/\[!--\sEND]>(row.[\S]*)\s--\]/sm', '[!-- END \\1 --]', $this->contentXml); - // Search all possible rows in the document - $reg1 = "#]*>(.*)
#smU"; - $matches = array(); - preg_match_all($reg1, $this->contentXml, $matches); - for ($i = 0, $size = count($matches[0]); $i < $size; $i++) { - // Check if the current row contains a segment row.* - $reg2 = '#\[!--\sBEGIN\s(row.[\S]*)\s--\](.*)\[!--\sEND\s\\1\s--\]#sm'; - $matches2 = array(); - if (preg_match($reg2, $matches[0][$i], $matches2)) { - $balise = str_replace('row.', '', $matches2[1]); - // Move segment tags around the row - $replace = array( - '[!-- BEGIN ' . $matches2[1] . ' --]' => '', - '[!-- END ' . $matches2[1] . ' --]' => '', - ' '[!-- BEGIN ' . $balise . ' --]' => '
[!-- END ' . $balise . ' --]' - ); - $replacedXML = str_replace(array_keys($replace), array_values($replace), $matches[0][$i]); - $this->contentXml = str_replace($matches[0][$i], $replacedXML, $this->contentXml); - } - } - } + // Search all possible rows in the document + $reg1 = "#]*>(.*)#smU"; + $matches = array(); + preg_match_all($reg1, $this->contentXml, $matches); + for ($i = 0, $size = count($matches[0]); $i < $size; $i++) { + // Check if the current row contains a segment row.* + $reg2 = '#\[!--\sBEGIN\s(row.[\S]*)\s--\](.*)\[!--\sEND\s\\1\s--\]#sm'; + $matches2 = array(); + if (preg_match($reg2, $matches[0][$i], $matches2)) { + $balise = str_replace('row.', '', $matches2[1]); + // Move segment tags around the row + $replace = array( + '[!-- BEGIN ' . $matches2[1] . ' --]' => '', + '[!-- END ' . $matches2[1] . ' --]' => '', + ' '[!-- BEGIN ' . $balise . ' --]' => '[!-- END ' . $balise . ' --]' + ); + $replacedXML = str_replace(array_keys($replace), array_values($replace), $matches[0][$i]); + $this->contentXml = str_replace($matches[0][$i], $replacedXML, $this->contentXml); + } + } + } - /** - * Merge template variables - * Called at the beginning of the _save function - * - * @param string $type 'content', 'styles' or 'meta' - * @return void - */ - private function _parse($type = 'content') - { - // Search all tags found into condition to complete $this->vars, so we will proceed all tests even if not defined - $reg='@\[!--\sIF\s([{}a-zA-Z0-9\.\,_]+)\s--\]@smU'; - $matches = array(); - preg_match_all($reg, $this->contentXml, $matches, PREG_SET_ORDER); + /** + * Merge template variables + * Called at the beginning of the _save function + * + * @param string $type 'content', 'styles' or 'meta' + * @return void + */ + private function _parse($type = 'content') + { + // Search all tags found into condition to complete $this->vars, so we will proceed all tests even if not defined + $reg='@\[!--\sIF\s([{}a-zA-Z0-9\.\,_]+)\s--\]@smU'; + $matches = array(); + preg_match_all($reg, $this->contentXml, $matches, PREG_SET_ORDER); - //var_dump($this->vars);exit; - foreach ($matches as $match) { // For each match, if there is no entry into this->vars, we add it - if (! empty($match[1]) && ! isset($this->vars[$match[1]])) { - $this->vars[$match[1]] = ''; // Not defined, so we set it to '', we just need entry into this->vars for next loop - } - } - //var_dump($this->vars);exit; + //var_dump($this->vars);exit; + foreach ($matches as $match) { // For each match, if there is no entry into this->vars, we add it + if (! empty($match[1]) && ! isset($this->vars[$match[1]])) { + $this->vars[$match[1]] = ''; // Not defined, so we set it to '', we just need entry into this->vars for next loop + } + } + //var_dump($this->vars);exit; - // Conditionals substitution - // Note: must be done before static substitution, else the variable will be replaced by its value and the conditional won't work anymore - foreach ($this->vars as $key => $value) { - // If value is true (not 0 nor false nor null nor empty string) - if ($value) { - //dol_syslog("Var ".$key." is defined, we remove the IF, ELSE and ENDIF "); - //$sav=$this->contentXml; - // Remove the IF tag - $this->contentXml = str_replace('[!-- IF '.$key.' --]', '', $this->contentXml); - // Remove everything between the ELSE tag (if it exists) and the ENDIF tag - $reg = '@(\[!--\sELSE\s' . $key . '\s--\](.*))?\[!--\sENDIF\s' . $key . '\s--\]@smU'; // U modifier = all quantifiers are non-greedy - $this->contentXml = preg_replace($reg, '', $this->contentXml); - /*if ($sav != $this->contentXml) - { - dol_syslog("We found a IF and it was processed"); - //var_dump($sav);exit; - }*/ - } else { - // 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 + // Conditionals substitution + // Note: must be done before static substitution, else the variable will be replaced by its value and the conditional won't work anymore + foreach ($this->vars as $key => $value) { + // If value is true (not 0 nor false nor null nor empty string) + if ($value) { + //dol_syslog("Var ".$key." is defined, we remove the IF, ELSE and ENDIF "); + //$sav=$this->contentXml; + // Remove the IF tag + $this->contentXml = str_replace('[!-- IF '.$key.' --]', '', $this->contentXml); + // Remove everything between the ELSE tag (if it exists) and the ENDIF tag + $reg = '@(\[!--\sELSE\s' . $key . '\s--\](.*))?\[!--\sENDIF\s' . $key . '\s--\]@smU'; // U modifier = all quantifiers are non-greedy + $this->contentXml = preg_replace($reg, '', $this->contentXml); + /*if ($sav != $this->contentXml) + { + dol_syslog("We found a IF and it was processed"); + //var_dump($sav);exit; + }*/ + } else { + // 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 - //dol_syslog("Var ".$key." is not defined, we remove the IF, ELSE and ENDIF "); - //$sav=$this->contentXml; - // Find all conditional blocks for this variable: from IF to ELSE and to ENDIF - $reg = '@\[!--\sIF\s' . $key . '\s--\](.*)(\[!--\sELSE\s' . $key . '\s--\](.*))?\[!--\sENDIF\s' . $key . '\s--\]@smU'; // U modifier = all quantifiers are non-greedy - preg_match_all($reg, $this->contentXml, $matches, PREG_SET_ORDER); - 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 - if (!empty($match[3])) $this->contentXml = str_replace($match[0], $match[3], $this->contentXml); - } - // Cleanup the other conditional blocks (all the others where there were no ELSE clause, we can just remove them altogether) - $this->contentXml = preg_replace($reg, '', $this->contentXml); - /*if ($sav != $this->contentXml) - { - dol_syslog("We found a IF and it was processed"); - //var_dump($sav);exit; - }*/ - } - } + //dol_syslog("Var ".$key." is not defined, we remove the IF, ELSE and ENDIF "); + //$sav=$this->contentXml; + // Find all conditional blocks for this variable: from IF to ELSE and to ENDIF + $reg = '@\[!--\sIF\s' . $key . '\s--\](.*)(\[!--\sELSE\s' . $key . '\s--\](.*))?\[!--\sENDIF\s' . $key . '\s--\]@smU'; // U modifier = all quantifiers are non-greedy + preg_match_all($reg, $this->contentXml, $matches, PREG_SET_ORDER); + 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 + if (!empty($match[3])) $this->contentXml = str_replace($match[0], $match[3], $this->contentXml); + } + // Cleanup the other conditional blocks (all the others where there were no ELSE clause, we can just remove them altogether) + $this->contentXml = preg_replace($reg, '', $this->contentXml); + /*if ($sav != $this->contentXml) + { + dol_syslog("We found a IF and it was processed"); + //var_dump($sav);exit; + }*/ + } + } - // Static substitution - if ($type == 'content') $this->contentXml = str_replace(array_keys($this->vars), array_values($this->vars), $this->contentXml); - if ($type == 'styles') $this->stylesXml = str_replace(array_keys($this->vars), array_values($this->vars), $this->stylesXml); - if ($type == 'meta') $this->metaXml = str_replace(array_keys($this->vars), array_values($this->vars), $this->metaXml); - } + // Static substitution + if ($type == 'content') $this->contentXml = str_replace(array_keys($this->vars), array_values($this->vars), $this->contentXml); + if ($type == 'styles') $this->stylesXml = str_replace(array_keys($this->vars), array_values($this->vars), $this->stylesXml); + if ($type == 'meta') $this->metaXml = str_replace(array_keys($this->vars), array_values($this->vars), $this->metaXml); + } - /** - * Add the merged segment to the document - * - * @param Segment $segment Segment - * @throws OdfException - * @return odf - */ - public function mergeSegment(Segment $segment) - { - if (! array_key_exists($segment->getName(), $this->segments)) { - throw new OdfException($segment->getName() . 'cannot be parsed, has it been set yet ?'); - } - $string = $segment->getName(); - // $reg = '@]*>\[!--\sBEGIN\s' . $string . '\s--\](.*)\[!--.+END\s' . $string . '\s--\]<\/text:p>@smU'; - $reg = '@\[!--\sBEGIN\s' . $string . '\s--\](.*)\[!--.+END\s' . $string . '\s--\]@smU'; - $this->contentXml = preg_replace($reg, $segment->getXmlParsed(), $this->contentXml); - return $this; - } + /** + * Add the merged segment to the document + * + * @param Segment $segment Segment + * @throws OdfException + * @return odf + */ + public function mergeSegment(Segment $segment) + { + if (! array_key_exists($segment->getName(), $this->segments)) { + throw new OdfException($segment->getName() . 'cannot be parsed, has it been set yet ?'); + } + $string = $segment->getName(); + // $reg = '@]*>\[!--\sBEGIN\s' . $string . '\s--\](.*)\[!--.+END\s' . $string . '\s--\]<\/text:p>@smU'; + $reg = '@\[!--\sBEGIN\s' . $string . '\s--\](.*)\[!--.+END\s' . $string . '\s--\]@smU'; + $this->contentXml = preg_replace($reg, $segment->getXmlParsed(), $this->contentXml); + return $this; + } - /** - * Display all the current template variables - * - * @return string - */ - public function printVars() - { - return print_r('
' . print_r($this->vars, true) . '
', true); - } + /** + * Display all the current template variables + * + * @return string + */ + public function printVars() + { + return print_r('
' . print_r($this->vars, true) . '
', true); + } - /** - * Display the XML content of the file from odt document - * as it is at the moment - * - * @return string - */ - public function __toString() - { - return $this->contentXml; - } + /** + * Display the XML content of the file from odt document + * as it is at the moment + * + * @return string + */ + public function __toString() + { + return $this->contentXml; + } - /** - * Display loop segments declared with setSegment() - * - * @return string - */ - public function printDeclaredSegments() - { - return '
' . print_r(implode(' ', array_keys($this->segments)), true) . '
'; - } + /** + * Display loop segments declared with setSegment() + * + * @return string + */ + public function printDeclaredSegments() + { + return '
' . print_r(implode(' ', array_keys($this->segments)), true) . '
'; + } - /** - * Declare a segment in order to use it in a loop. - * Extract the segment and store it into $this->segments[]. Return it for next call. - * - * @param string $segment Segment - * @throws OdfException - * @return Segment - */ - public function setSegment($segment) - { - if (array_key_exists($segment, $this->segments)) { - return $this->segments[$segment]; - } - // $reg = "#\[!--\sBEGIN\s$segment\s--\]<\/text:p>(.*)\[!--\sEND\s$segment\s--\]#sm"; - $reg = "#\[!--\sBEGIN\s$segment\s--\](.*)\[!--\sEND\s$segment\s--\]#sm"; - $m = array(); - if (preg_match($reg, html_entity_decode($this->contentXml), $m) == 0) { - throw new OdfException("'".$segment."' segment not found in the document. The tag [!-- BEGIN xxx --] or [!-- END xxx --] is not present into content file."); - } - $this->segments[$segment] = new Segment($segment, $m[1], $this); - return $this->segments[$segment]; - } - /** - * Save the odt file on the disk - * - * @param string $file name of the desired file - * @throws OdfException - * @return void - */ - public function saveToDisk($file = null) - { - if ($file !== null && is_string($file)) { - if (file_exists($file) && !(is_file($file) && is_writable($file))) { - throw new OdfException('Permission denied : can\'t create ' . $file); - } - $this->_save(); - copy($this->tmpfile, $file); - } else { - $this->_save(); - } - } + /** + * Declare a segment in order to use it in a loop. + * Extract the segment and store it into $this->segments[]. Return it for next call. + * + * @param string $segment Segment + * @throws OdfException + * @return Segment + */ + public function setSegment($segment) + { + if (array_key_exists($segment, $this->segments)) { + return $this->segments[$segment]; + } + // $reg = "#\[!--\sBEGIN\s$segment\s--\]<\/text:p>(.*)\[!--\sEND\s$segment\s--\]#sm"; + $reg = "#\[!--\sBEGIN\s$segment\s--\](.*)\[!--\sEND\s$segment\s--\]#sm"; + $m = array(); + if (preg_match($reg, html_entity_decode($this->contentXml), $m) == 0) { + throw new OdfException("'".$segment."' segment not found in the document. The tag [!-- BEGIN xxx --] or [!-- END xxx --] is not present into content file."); + } + $this->segments[$segment] = new Segment($segment, $m[1], $this); + return $this->segments[$segment]; + } + /** + * Save the odt file on the disk + * + * @param string $file name of the desired file + * @throws OdfException + * @return void + */ + public function saveToDisk($file = null) + { + if ($file !== null && is_string($file)) { + if (file_exists($file) && !(is_file($file) && is_writable($file))) { + throw new OdfException('Permission denied : can\'t create ' . $file); + } + $this->_save(); + copy($this->tmpfile, $file); + } else { + $this->_save(); + } + } - /** - * Write output file onto disk - * - * @throws OdfException - * @return void - */ - private function _save() - { - $res=$this->file->open($this->tmpfile); // tmpfile is odt template - $this->_parse('content'); - $this->_parse('styles'); - $this->_parse('meta'); + /** + * Write output file onto disk + * + * @throws OdfException + * @return void + */ + private function _save() + { + $res=$this->file->open($this->tmpfile); // tmpfile is odt template + $this->_parse('content'); + $this->_parse('styles'); + $this->_parse('meta'); - $this->setMetaData(); - //print $this->metaXml;exit; + $this->setMetaData(); + //print $this->metaXml;exit; - if (! $this->file->addFromString('content.xml', $this->contentXml)) { - throw new OdfException('Error during file export addFromString content'); - } - if (! $this->file->addFromString('meta.xml', $this->metaXml)) { - throw new OdfException('Error during file export addFromString meta'); - } - if (! $this->file->addFromString('styles.xml', $this->stylesXml)) { - throw new OdfException('Error during file export addFromString styles'); - } + if (! $this->file->addFromString('content.xml', $this->contentXml)) { + throw new OdfException('Error during file export addFromString content'); + } + if (! $this->file->addFromString('meta.xml', $this->metaXml)) { + throw new OdfException('Error during file export addFromString meta'); + } + if (! $this->file->addFromString('styles.xml', $this->stylesXml)) { + throw new OdfException('Error during file export addFromString styles'); + } - foreach ($this->images as $imageKey => $imageValue) { - // Add the image inside the ODT document - $this->file->addFile($imageKey, 'Pictures/' . $imageValue); - // 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) - $this->addImageToManifest($imageValue); - } - if (! $this->file->addFromString('./META-INF/manifest.xml', $this->manifestXml)) { - throw new OdfException('Error during file export: manifest.xml'); - } - $this->file->close(); - } + foreach ($this->images as $imageKey => $imageValue) { + // Add the image inside the ODT document + $this->file->addFile($imageKey, 'Pictures/' . $imageValue); + // 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) + $this->addImageToManifest($imageValue); + } + if (! $this->file->addFromString('./META-INF/manifest.xml', $this->manifestXml)) { + throw new OdfException('Error during file export: manifest.xml'); + } + $this->file->close(); + } - /** - * Update Meta information - * 2013-03-16T14:06:25 - * - * @return void - */ - public function setMetaData() - { - if (empty($this->creator)) $this->creator=''; + /** + * Update Meta information + * 2013-03-16T14:06:25 + * + * @return void + */ + public function setMetaData() + { + if (empty($this->creator)) $this->creator=''; - $this->metaXml = preg_replace('/.*<\/dc:date>/', ''.gmdate("Y-m-d\TH:i:s").'', $this->metaXml); - $this->metaXml = preg_replace('/.*<\/dc:creator>/', ''.htmlspecialchars($this->creator).'', $this->metaXml); - $this->metaXml = preg_replace('/.*<\/dc:title>/', ''.htmlspecialchars($this->title).'', $this->metaXml); - $this->metaXml = preg_replace('/.*<\/dc:subject>/', ''.htmlspecialchars($this->subject).'', $this->metaXml); + $this->metaXml = preg_replace('/.*<\/dc:date>/', ''.gmdate("Y-m-d\TH:i:s").'', $this->metaXml); + $this->metaXml = preg_replace('/.*<\/dc:creator>/', ''.htmlspecialchars($this->creator).'', $this->metaXml); + $this->metaXml = preg_replace('/.*<\/dc:title>/', ''.htmlspecialchars($this->title).'', $this->metaXml); + $this->metaXml = preg_replace('/.*<\/dc:subject>/', ''.htmlspecialchars($this->subject).'', $this->metaXml); - if (count($this->userdefined)) { - foreach ($this->userdefined as $key => $val) { - $this->metaXml = preg_replace('', '', $this->metaXml); - $this->metaXml = preg_replace('/.*<\/meta:user-defined>/', '', $this->metaXml); - $this->metaXml = str_replace('', ''.htmlspecialchars($val).'', $this->metaXml); - } - } - } + if (count($this->userdefined)) { + foreach ($this->userdefined as $key => $val) { + $this->metaXml = preg_replace('', '', $this->metaXml); + $this->metaXml = preg_replace('/.*<\/meta:user-defined>/', '', $this->metaXml); + $this->metaXml = str_replace('', ''.htmlspecialchars($val).'', $this->metaXml); + } + } + } - /** - * Update Manifest file according to added image files - * - * @param string $file Image file to add into manifest content - * @return void - */ - public function addImageToManifest($file) - { - // Get the file extension - $ext = substr(strrchr($file, '.'), 1); - // 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) - $add = ' '."\n"; - // Append the image to the manifest - $this->manifestXml = str_replace('', $add.'', $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) - } + /** + * Update Manifest file according to added image files + * + * @param string $file Image file to add into manifest content + * @return void + */ + public function addImageToManifest($file) + { + // Get the file extension + $ext = substr(strrchr($file, '.'), 1); + // 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) + $add = ' '."\n"; + // Append the image to the manifest + $this->manifestXml = str_replace('', $add.'', $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) + } - /** - * Export the file as attached file by HTTP - * - * @param string $name (optional) - * @throws OdfException - * @return void - */ - public function exportAsAttachedFile($name = "") - { - $this->_save(); - if (headers_sent($filename, $linenum)) { - throw new OdfException("headers already sent ($filename at $linenum)"); - } + /** + * Export the file as attached file by HTTP + * + * @param string $name (optional) + * @throws OdfException + * @return void + */ + public function exportAsAttachedFile($name = "") + { + $this->_save(); + if (headers_sent($filename, $linenum)) { + throw new OdfException("headers already sent ($filename at $linenum)"); + } - if ( $name == "" ) { - $name = md5(uniqid()) . ".odt"; - } + if ( $name == "" ) { + $name = md5(uniqid()) . ".odt"; + } - header('Content-type: application/vnd.oasis.opendocument.text'); - header('Content-Disposition: attachment; filename="'.$name.'"'); - header('Content-Length: '.filesize($this->tmpfile)); - readfile($this->tmpfile); - } + header('Content-type: application/vnd.oasis.opendocument.text'); + header('Content-Disposition: attachment; filename="'.$name.'"'); + header('Content-Length: '.filesize($this->tmpfile)); + readfile($this->tmpfile); + } - /** - * Convert the ODT file to PDF and export the file as attached file by HTTP - * 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 - * - * @param string $name Name of ODT file to generate before generating PDF - * @throws OdfException - * @return void - */ - public function exportAsAttachedPDF($name = "") - { - global $conf; + /** + * Convert the ODT file to PDF and export the file as attached file by HTTP + * 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 + * + * @param string $name Name of ODT file to generate before generating PDF + * @throws OdfException + * @return void + */ + public function exportAsAttachedPDF($name = "") + { + global $conf; - if ( $name == "" ) $name = "temp".md5(uniqid()); + if ( $name == "" ) $name = "temp".md5(uniqid()); - dol_syslog(get_class($this).'::exportAsAttachedPDF $name='.$name, LOG_DEBUG); - $this->saveToDisk($name); + dol_syslog(get_class($this).'::exportAsAttachedPDF $name='.$name, LOG_DEBUG); + $this->saveToDisk($name); - $execmethod=(empty($conf->global->MAIN_EXEC_USE_POPEN)?1:2); // 1 or 2 - // Method 1 sometimes hang the server. + $execmethod=(empty($conf->global->MAIN_EXEC_USE_POPEN)?1:2); // 1 or 2 + // Method 1 sometimes hang the server. - // Export to PDF using LibreOffice - if (getDolGlobalString('MAIN_ODT_AS_PDF') == 'libreoffice') { - dol_mkdir($conf->user->dir_temp); // We must be sure the directory exists and is writable + // Export to PDF using LibreOffice + if (getDolGlobalString('MAIN_ODT_AS_PDF') == 'libreoffice') { + dol_mkdir($conf->user->dir_temp); // We must be sure the directory exists and is writable - // We delete and recreate a subdir because the soffice may have change pemrissions on it - dol_delete_dir_recursive($conf->user->dir_temp.'/odtaspdf'); - dol_mkdir($conf->user->dir_temp.'/odtaspdf'); + // We delete and recreate a subdir because the soffice may have change pemrissions on it + dol_delete_dir_recursive($conf->user->dir_temp.'/odtaspdf'); + dol_mkdir($conf->user->dir_temp.'/odtaspdf'); - // Install prerequisites: apt install soffice libreoffice-common libreoffice-writer - // using windows libreoffice that must be in path - // using linux/mac libreoffice that must be in path - // Note PHP Config "fastcgi.impersonate=0" must set to 0 - Default is 1 - $command ='soffice --headless -env:UserInstallation=file:\''.$conf->user->dir_temp.'/odtaspdf\' --convert-to pdf --outdir '. escapeshellarg(dirname($name)). " ".escapeshellarg($name); - } elseif (preg_match('/unoconv/', getDolGlobalString('MAIN_ODT_AS_PDF'))) { - // If issue with unoconv, see https://github.com/dagwieers/unoconv/issues/87 + // Install prerequisites: apt install soffice libreoffice-common libreoffice-writer + // using windows libreoffice that must be in path + // using linux/mac libreoffice that must be in path + // Note PHP Config "fastcgi.impersonate=0" must set to 0 - Default is 1 + $command ='soffice --headless -env:UserInstallation=file:\''.$conf->user->dir_temp.'/odtaspdf\' --convert-to pdf --outdir '. escapeshellarg(dirname($name)). " ".escapeshellarg($name); + } elseif (preg_match('/unoconv/', getDolGlobalString('MAIN_ODT_AS_PDF'))) { + // If issue with unoconv, see https://github.com/dagwieers/unoconv/issues/87 - // 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 . + // 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 . - // Try this with www-data user: /usr/bin/unoconv -vvvv -f pdf /tmp/document-example.odt - // It must return: - //Verbosity set to level 4 - //Using office base path: /usr/lib/libreoffice - //Using office binary path: /usr/lib/libreoffice/program - //DEBUG: Connection type: socket,host=127.0.0.1,port=2002;urp;StarOffice.ComponentContext - //DEBUG: Existing listener not found. - //DEBUG: Launching our own listener using /usr/lib/libreoffice/program/soffice.bin. - //LibreOffice listener successfully started. (pid=9287) - //Input file: /tmp/document-example.odt - //unoconv: file `/tmp/document-example.odt' does not exist. - //unoconv: RuntimeException during import phase: - //Office probably died. Unsupported URL : "type detection failed" - //DEBUG: Terminating LibreOffice instance. - //DEBUG: Waiting for LibreOffice instance to exit + // Try this with www-data user: /usr/bin/unoconv -vvvv -f pdf /tmp/document-example.odt + // It must return: + //Verbosity set to level 4 + //Using office base path: /usr/lib/libreoffice + //Using office binary path: /usr/lib/libreoffice/program + //DEBUG: Connection type: socket,host=127.0.0.1,port=2002;urp;StarOffice.ComponentContext + //DEBUG: Existing listener not found. + //DEBUG: Launching our own listener using /usr/lib/libreoffice/program/soffice.bin. + //LibreOffice listener successfully started. (pid=9287) + //Input file: /tmp/document-example.odt + //unoconv: file `/tmp/document-example.odt' does not exist. + //unoconv: RuntimeException during import phase: + //Office probably died. Unsupported URL : "type detection failed" + //DEBUG: Terminating LibreOffice instance. + //DEBUG: Waiting for LibreOffice instance to exit - // If it fails: - // - set shell of user to bash instead of nologin. - // - 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 + // If it fails: + // - set shell of user to bash instead of nologin. + // - 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 - $command = getDolGlobalString('MAIN_ODT_AS_PDF').' '.escapeshellcmd($name); - //$command = '/usr/bin/unoconv -vvv '.escapeshellcmd($name); - } else { - // deprecated old method using odt2pdf.sh (native, jodconverter, ...) - $tmpname=preg_replace('/\.odt/i', '', $name); + $command = getDolGlobalString('MAIN_ODT_AS_PDF').' '.escapeshellcmd($name); + //$command = '/usr/bin/unoconv -vvv '.escapeshellcmd($name); + } else { + // deprecated old method using odt2pdf.sh (native, jodconverter, ...) + $tmpname=preg_replace('/\.odt/i', '', $name); - if (getDolGlobalString('MAIN_DOL_SCRIPTS_ROOT')) { - $command = getDolGlobalString('MAIN_DOL_SCRIPTS_ROOT').'/scripts/odt2pdf/odt2pdf.sh '.escapeshellcmd($tmpname).' '.(is_numeric(getDolGlobalString('MAIN_ODT_AS_PDF'))?'jodconverter':getDolGlobalString('MAIN_ODT_AS_PDF')); - } else { - 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); - $command = '../../scripts/odt2pdf/odt2pdf.sh '.escapeshellcmd($tmpname).' '.(is_numeric(getDolGlobalString('MAIN_ODT_AS_PDF'))?'jodconverter':getDolGlobalString('MAIN_ODT_AS_PDF')); - } - } + if (getDolGlobalString('MAIN_DOL_SCRIPTS_ROOT')) { + $command = getDolGlobalString('MAIN_DOL_SCRIPTS_ROOT').'/scripts/odt2pdf/odt2pdf.sh '.escapeshellcmd($tmpname).' '.(is_numeric(getDolGlobalString('MAIN_ODT_AS_PDF'))?'jodconverter':getDolGlobalString('MAIN_ODT_AS_PDF')); + } else { + 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); + $command = '../../scripts/odt2pdf/odt2pdf.sh '.escapeshellcmd($tmpname).' '.(is_numeric(getDolGlobalString('MAIN_ODT_AS_PDF'))?'jodconverter':getDolGlobalString('MAIN_ODT_AS_PDF')); + } + } - //$dirname=dirname($name); - //$command = DOL_DOCUMENT_ROOT.'/includes/odtphp/odt2pdf.sh '.$name.' '.$dirname; + //$dirname=dirname($name); + //$command = DOL_DOCUMENT_ROOT.'/includes/odtphp/odt2pdf.sh '.$name.' '.$dirname; - dol_syslog(get_class($this).'::exportAsAttachedPDF $execmethod='.$execmethod.' Run command='.$command, LOG_DEBUG); - // TODO Use: - // $outputfile = DOL_DATA_ROOT.'/odt2pdf.log'; - // $result = $utils->executeCLI($command, $outputfile); and replace test on $execmethod. - // $retval will be $result['result'] - // $errorstring will be $result['output'] - $retval=0; $output_arr=array(); - if ($execmethod == 1) { - exec($command, $output_arr, $retval); - } - if ($execmethod == 2) { - $outputfile = DOL_DATA_ROOT.'/odt2pdf.log'; + dol_syslog(get_class($this).'::exportAsAttachedPDF $execmethod='.$execmethod.' Run command='.$command, LOG_DEBUG); + // TODO Use: + // $outputfile = DOL_DATA_ROOT.'/odt2pdf.log'; + // $result = $utils->executeCLI($command, $outputfile); and replace test on $execmethod. + // $retval will be $result['result'] + // $errorstring will be $result['output'] + $retval=0; $output_arr=array(); + if ($execmethod == 1) { + exec($command, $output_arr, $retval); + } + if ($execmethod == 2) { + $outputfile = DOL_DATA_ROOT.'/odt2pdf.log'; - $ok=0; - $handle = fopen($outputfile, 'w'); - if ($handle) { - dol_syslog(get_class($this)."Run command ".$command, LOG_DEBUG); - fwrite($handle, $command."\n"); - $handlein = popen($command, 'r'); - while (!feof($handlein)) { - $read = fgets($handlein); - fwrite($handle, $read); - $output_arr[]=$read; - } - pclose($handlein); - fclose($handle); - } - if (! empty($conf->global->MAIN_UMASK)) @chmod($outputfile, octdec($conf->global->MAIN_UMASK)); - } + $ok=0; + $handle = fopen($outputfile, 'w'); + if ($handle) { + dol_syslog(get_class($this)."Run command ".$command, LOG_DEBUG); + fwrite($handle, $command."\n"); + $handlein = popen($command, 'r'); + while (!feof($handlein)) { + $read = fgets($handlein); + fwrite($handle, $read); + $output_arr[]=$read; + } + pclose($handlein); + fclose($handle); + } + if (! empty($conf->global->MAIN_UMASK)) @chmod($outputfile, octdec($conf->global->MAIN_UMASK)); + } - if ($retval == 0) { - dol_syslog(get_class($this).'::exportAsAttachedPDF $ret_val='.$retval, LOG_DEBUG); - $filename=''; $linenum=0; + if ($retval == 0) { + dol_syslog(get_class($this).'::exportAsAttachedPDF $ret_val='.$retval, LOG_DEBUG); + $filename=''; $linenum=0; - if (php_sapi_name() != 'cli') { // If we are in a web context (not into CLI context) - if (headers_sent($filename, $linenum)) { - throw new OdfException("headers already sent ($filename at $linenum)"); - } + if (php_sapi_name() != 'cli') { // If we are in a web context (not into CLI context) + if (headers_sent($filename, $linenum)) { + throw new OdfException("headers already sent ($filename at $linenum)"); + } - if (!empty($conf->global->MAIN_DISABLE_PDF_AUTOUPDATE)) { - $name=preg_replace('/\.od(x|t)/i', '', $name); - header('Content-type: application/pdf'); - header('Content-Disposition: attachment; filename="'.$name.'.pdf"'); - readfile($name.".pdf"); - } - } + if (!empty($conf->global->MAIN_DISABLE_PDF_AUTOUPDATE)) { + $name=preg_replace('/\.od(x|t)/i', '', $name); + header('Content-type: application/pdf'); + header('Content-Disposition: attachment; filename="'.$name.'.pdf"'); + readfile($name.".pdf"); + } + } - if (!empty($conf->global->MAIN_ODT_AS_PDF_DEL_SOURCE)) { - unlink($name); - } - } else { - dol_syslog(get_class($this).'::exportAsAttachedPDF $ret_val='.$retval, LOG_DEBUG); - dol_syslog(get_class($this).'::exportAsAttachedPDF $output_arr='.var_export($output_arr, true), LOG_DEBUG); + if (!empty($conf->global->MAIN_ODT_AS_PDF_DEL_SOURCE)) { + unlink($name); + } + } else { + dol_syslog(get_class($this).'::exportAsAttachedPDF $ret_val='.$retval, LOG_DEBUG); + dol_syslog(get_class($this).'::exportAsAttachedPDF $output_arr='.var_export($output_arr, true), LOG_DEBUG); - if ($retval == 126) { - throw new OdfException('Permission execute convert script : ' . $command); - } else { - $errorstring=''; - foreach ($output_arr as $line) { - $errorstring.= $line."
"; - } - 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); - } - } - } + if ($retval == 126) { + throw new OdfException('Permission execute convert script : ' . $command); + } else { + $errorstring=''; + foreach ($output_arr as $line) { + $errorstring.= $line."
"; + } + 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); + } + } + } - /** - * Returns a variable of configuration - * - * @param string $configKey Config key - * @return string The requested variable of configuration - */ - public function getConfig($configKey) - { - if (array_key_exists($configKey, $this->config)) { - return $this->config[$configKey]; - } - return false; - } - /** - * Returns the temporary working file - * - * @return string le chemin vers le fichier temporaire de travail - */ - public function getTmpfile() - { - return $this->tmpfile; - } + /** + * Returns a variable of configuration + * + * @param string $configKey Config key + * @return string The requested variable of configuration + */ + public function getConfig($configKey) + { + if (array_key_exists($configKey, $this->config)) { + return $this->config[$configKey]; + } + return false; + } + /** + * Returns the temporary working file + * + * @return string le chemin vers le fichier temporaire de travail + */ + public function getTmpfile() + { + return $this->tmpfile; + } - /** - * Delete the temporary file when the object is destroyed - */ - public function __destruct() - { - if (file_exists($this->tmpfile)) { - unlink($this->tmpfile); - } + /** + * Delete the temporary file when the object is destroyed + */ + public function __destruct() + { + if (file_exists($this->tmpfile)) { + unlink($this->tmpfile); + } - if (file_exists($this->tmpdir)) { - $this->_rrmdir($this->tmpdir); - rmdir($this->tmpdir); - } - } + if (file_exists($this->tmpdir)) { + $this->_rrmdir($this->tmpdir); + rmdir($this->tmpdir); + } + } - /** - * Empty the temporary working directory recursively - * - * @param string $dir The temporary working directory - * @return void - */ - private function _rrmdir($dir) - { - if ($handle = opendir($dir)) { - while (($file = readdir($handle)) !== false) { - if ($file != '.' && $file != '..') { - if (is_dir($dir . '/' . $file)) { - $this->_rrmdir($dir . '/' . $file); - rmdir($dir . '/' . $file); - } else { - unlink($dir . '/' . $file); - } - } - } - closedir($handle); - } - } + /** + * Empty the temporary working directory recursively + * + * @param string $dir The temporary working directory + * @return void + */ + private function _rrmdir($dir) + { + if ($handle = opendir($dir)) { + while (($file = readdir($handle)) !== false) { + if ($file != '.' && $file != '..') { + if (is_dir($dir . '/' . $file)) { + $this->_rrmdir($dir . '/' . $file); + rmdir($dir . '/' . $file); + } else { + unlink($dir . '/' . $file); + } + } + } + closedir($handle); + } + } - /** - * return the value present on odt in [valuename][/valuename] - * - * @param string $valuename Balise in the template - * @return string The value inside the balise - */ - public function getvalue($valuename) - { - $searchreg="/\\[".$valuename."\\](.*)\\[\\/".$valuename."\\]/"; - $matches = array(); - preg_match($searchreg, $this->contentXml, $matches); - $this->contentXml = preg_replace($searchreg, "", $this->contentXml); - return $matches[1]; - } -} + /** + * return the value present on odt in [valuename][/valuename] + * + * @param string $valuename Balise in the template + * @return string The value inside the balise + */ + public function getvalue($valuename) + { + $searchreg="/\\[".$valuename."\\](.*)\\[\\/".$valuename."\\]/"; + $matches = array(); + preg_match($searchreg, $this->contentXml, $matches); + $this->contentXml = preg_replace($searchreg, "", $this->contentXml); + return $matches[1]; + } +} \ No newline at end of file