odf.php 39 KB

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