From c10e955e67732c0381978d4884535c86d1de0856 Mon Sep 17 00:00:00 2001 From: atm-nicolasV Date: Wed, 8 Apr 2026 14:04:04 +0200 Subject: [PATCH 1/2] FIX: rebuild ZIP in odf.php to prevent corrupt ODT and failed PDF conversion ZipArchive::addFromString on existing entries leaves ghost data causing overlapped components. Rebuild ZIP from scratch to produce clean ODT files that soffice can convert to PDF. Also add missing require for product.lib.php in commondocgenerator. Co-Authored-By: Claude Opus 4.6 (1M context) --- Changelog_qaleo.md | 44 +++++ .../core/class/commondocgenerator.class.php | 3 + htdocs/includes/odtphp/odf.php | 173 ++++++++++-------- 3 files changed, 141 insertions(+), 79 deletions(-) create mode 100644 Changelog_qaleo.md diff --git a/Changelog_qaleo.md b/Changelog_qaleo.md new file mode 100644 index 0000000000000..9149325cd4937 --- /dev/null +++ b/Changelog_qaleo.md @@ -0,0 +1,44 @@ +------------------V21.0_Qaleo------------------------ +- ajout des clés pour les doc .odt suivantes : **08/04/2026** +--- Clés ligne (dans segment row.lines) --- +{line_product_weight} +{line_product_weight_raw} +{line_product_weight_units} +{line_product_weight_total} +{line_product_weight_total_units} +{line_product_length} +{line_product_length_raw} +{line_product_width} +{line_product_width_raw} +{line_product_height} +{line_product_height_raw} +{line_product_surface} +{line_product_surface_raw} +{line_product_volume} +{line_product_volume_raw} +{line_product_net_measure} +{line_product_customcode} +{line_product_country_code} +{line_product_country_id} +{line_product_country} +{line_product_finished} +{line_product_finished_raw} +{line_product_url} +{line_product_note_public} +{line_product_note_private} +{line_product_stock_reel} +{line_product_pmp} +{line_product_cost_price} +{line_product_accountancy_code_sell} +{line_product_accountancy_code_buy} +{line_product_packaging} +{line_product_status_batch} +{line_product_duration} +{line_product_fk_default_warehouse} +{line_product_default_warehouse} +{line_product_fk_default_bom} +{line_product_default_bom} + +--- Clés objet (hors segment row.lines) --- +{object_total_weight} +{object_total_weight_raw} diff --git a/htdocs/core/class/commondocgenerator.class.php b/htdocs/core/class/commondocgenerator.class.php index e5b9bdfa5ff0f..cfca5e9c964f5 100644 --- a/htdocs/core/class/commondocgenerator.class.php +++ b/htdocs/core/class/commondocgenerator.class.php @@ -756,6 +756,7 @@ public function get_substitutionarray_object($object, $outputlangs, $array_key = // Calculate total weight from all lines if (!empty($object->lines) && is_array($object->lines)) { require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php'; + include_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php'; $total_weight = 0; $weight_unit = null; foreach ($object->lines as $tmpline) { @@ -946,6 +947,8 @@ public function get_substitutionarray_lines($line, $outputlangs, $linenumber = 0 // Load full product data to get weight, dimensions, customs code, etc. if (!empty($line->fk_product) && $line->fk_product > 0) { + require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php'; + include_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php'; $product = new Product($this->db); if ($product->fetch($line->fk_product) > 0) { // Dimensions & weight diff --git a/htdocs/includes/odtphp/odf.php b/htdocs/includes/odtphp/odf.php index aa19feac431b2..7d812326ef55b 100644 --- a/htdocs/includes/odtphp/odf.php +++ b/htdocs/includes/odtphp/odf.php @@ -716,34 +716,66 @@ public function saveToDisk($file = null) */ 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; - if (! $this->file->addFromString('content.xml', $this->contentXml)) { - throw new OdfException('Error during file export addFromString content'); + // Rebuild the ZIP from scratch to avoid overlapped components + // PHP ZipArchive::addFromString on existing entries leaves ghost data, corrupting the file + $newfile = $this->tmpfile . '.new'; + $sourceZip = new \ZipArchive(); + $destZip = new \ZipArchive(); + + if ($sourceZip->open($this->tmpfile) !== true) { + throw new OdfException('Error opening source template: ' . $this->tmpfile); } - if (! $this->file->addFromString('meta.xml', $this->metaXml)) { - throw new OdfException('Error during file export addFromString meta'); + if ($destZip->open($newfile, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) { + $sourceZip->close(); + throw new OdfException('Error creating new file: ' . $newfile); } - if (! $this->file->addFromString('styles.xml', $this->stylesXml)) { - throw new OdfException('Error during file export addFromString styles'); + + // Map of files we will replace with modified content + $replacements = array( + 'content.xml' => $this->contentXml, + 'meta.xml' => $this->metaXml, + 'styles.xml' => $this->stylesXml, + ); + + // Copy all entries from source, replacing modified ones + for ($i = 0; $i < $sourceZip->numFiles; $i++) { + $entryName = $sourceZip->getNameIndex($i); + if ($entryName === 'META-INF/manifest.xml') { + continue; // Will be added after images + } + if (isset($replacements[$entryName])) { + $destZip->addFromString($entryName, $replacements[$entryName]); + } elseif ($entryName === 'mimetype') { + // mimetype must be stored uncompressed + $destZip->addFromString($entryName, $sourceZip->getFromIndex($i)); + $destZip->setCompressionName($entryName, \ZipArchive::CM_STORE); + } else { + $destZip->addFromString($entryName, $sourceZip->getFromIndex($i)); + } } + // Add new images 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) + $destZip->addFile($imageKey, 'Pictures/' . $imageValue); $this->addImageToManifest($imageValue); } - if (! $this->file->addFromString('META-INF/manifest.xml', $this->manifestXml)) { - throw new OdfException('Error during file export: manifest.xml'); + + // Add manifest last (after images have been registered) + $destZip->addFromString('META-INF/manifest.xml', $this->manifestXml); + + $sourceZip->close(); + $destZip->close(); + + // Replace original with clean version + if (!rename($newfile, $this->tmpfile)) { + throw new OdfException('Error replacing file: ' . $this->tmpfile); } - $this->file->close(); } /** @@ -844,80 +876,63 @@ public function exportAsAttachedPDF($name = "") // 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:'.escapeshellarg((getDolGlobalString('MAIN_ODT_ADD_SLASH_FOR_WINDOWS') ? '///' : '').dol_sanitizePathName($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 . - - // 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 - - $command = getDolGlobalString('MAIN_ODT_AS_PDF').' '.escapeshellarg($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 = dol_sanitizePathName(getDolGlobalString('MAIN_DOL_SCRIPTS_ROOT')).'/scripts/odt2pdf/odt2pdf.sh '.escapeshellarg($tmpname).' '.escapeshellarg(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); - $paramodt2pdf = (is_numeric(getDolGlobalString('MAIN_ODT_AS_PDF')) ? 'jodconverter' : getDolGlobalString('MAIN_ODT_AS_PDF')); - $paramodt2pdf = dol_sanitizePathName($paramodt2pdf); - $command = '../../scripts/odt2pdf/odt2pdf.sh '.escapeshellarg($tmpname).' '.escapeshellarg($paramodt2pdf); + $sofficebin = getDolGlobalString('MAIN_ODT_BINARY', 'soffice'); + $userInstallationDir = '/tmp/odtaspdf_'.md5($conf->user->id.'_'.$conf->user->login); + if (!is_dir($userInstallationDir)) { + dol_mkdir($userInstallationDir); } - } - - //$dirname=dirname($name); - //$command = DOL_DOCUMENT_ROOT.'/includes/odtphp/odt2pdf.sh '.$name.' '.$dirname; + $userInstallationUrl = 'file://'.$userInstallationDir; - dol_syslog(get_class($this).'::exportAsAttachedPDF $execmethod='.$execmethod.' Run command='.$command, LOG_DEBUG); + $command = $sofficebin.' --headless --nologo --nodefault --nofirststartwizard '.escapeshellarg('-env:UserInstallation='.$userInstallationUrl).' --convert-to pdf --outdir '. escapeshellarg(dol_sanitizePathName(dirname($name))). " ".escapeshellarg($name); - // 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(escapeshellcmd($command), $output_arr, $retval); - } - if ($execmethod == 2) { $outputfile = DOL_DATA_ROOT.'/odt2pdf.log'; - $handle = fopen($outputfile, 'w'); if ($handle) { - dol_syslog(get_class($this)."Run command ".$command, LOG_DEBUG); - dol_syslog(get_class($this)."escapeshellcmd(command) = ".escapeshellcmd($command), LOG_DEBUG); - fwrite($handle, $command."\n"); - $handlein = popen(escapeshellcmd($command), 'r'); - while (!feof($handlein)) { - $read = fgets($handlein); - fwrite($handle, $read); - $output_arr[]=$read; + if (!file_exists($name)) { + fwrite($handle, "Error: Input file ".$name." does not exist before conversion!\n"); + } + fwrite($handle, "Command: ".$command."\n"); + } + + if ($execmethod == 1) { + exec($command, $output_arr, $retval); + if ($handle) { + foreach ($output_arr as $line) { + fwrite($handle, $line."\n"); + } + } + } + if ($execmethod == 2) { + if ($handle) { + $handlein = popen($command, 'r'); + while (!feof($handlein)) { + $read = fgets($handlein); + fwrite($handle, $read); + $output_arr[]=$read; + } + $res_pclose = pclose($handlein); + if ($res_pclose != -1) { + $retval = ($res_pclose >> 8) & 0xFF; // Get the exit code + } else { + $retval = 127; // Process could not be started + } + } + } + + if ($handle) { + fwrite($handle, "Return value: ".$retval."\n"); + $pdfname = preg_replace('/\.od(t|s|x)/i', '.pdf', $name); + if ($retval == 0 && !file_exists($pdfname)) { + fwrite($handle, "Warning: Conversion reported success but ".$pdfname." was not found.\n"); + fwrite($handle, "Directory listing of ".dirname($name).":\n"); + $files = scandir(dirname($name)); + foreach ($files as $f) { + fwrite($handle, "- ".$f."\n"); + } } - pclose($handlein); fclose($handle); + dolChmod($outputfile); } - dolChmod($outputfile); } if ($retval == 0) { From 45da12acd6762c248b49c8cd11b8fc9c6f5c342f Mon Sep 17 00:00:00 2001 From: atm-nicolasV Date: Wed, 8 Apr 2026 15:31:30 +0200 Subject: [PATCH 2/2] fix odt to pdf convertion --- Changelog_qaleo.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Changelog_qaleo.md b/Changelog_qaleo.md index 9149325cd4937..5ef77478f47e8 100644 --- a/Changelog_qaleo.md +++ b/Changelog_qaleo.md @@ -1,5 +1,6 @@ ------------------V21.0_Qaleo------------------------ -- ajout des clés pour les doc .odt suivantes : **08/04/2026** +- Modifications de /htdocs/includes/odtphp/odf.php, provoquer une erreur de génération du pdf depuis un odt **08/04/2026** +- ajout des clés pour les doc .odt suivantes : --- Clés ligne (dans segment row.lines) --- {line_product_weight} {line_product_weight_raw}