Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Fixed

- Fix PDF export of tabs renamed in GLPI 11
- Fix PDF export crash on tickets with tables containing nested-quote styles

## [4.1.4] - 2026-07-30

Expand Down
93 changes: 76 additions & 17 deletions inc/simplepdf.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -468,30 +468,89 @@ private function cleanTableHtml($html)
// Remove colgroup entirely (causes fixed widths)
$html = preg_replace('/<colgroup\b[^>]*>.*?<\/colgroup>/is', '', $html);

// Remove table-layout:fixed style (prevents auto-sizing)
$html = preg_replace('/table-layout\s*:\s*fixed\s*;?/i', '', $html);
// Parse with DOMDocument rather than regexes: style/attribute values coming

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a unit test for cleanTableHtml() with a nested-quote url('...') style value (case from !45825)? Verified locally that the old regex code produced a <table> with duplicate border/style attributes on this input, the actual crash mechanism, and that the new code doesn't; a test would pin the regression.

// from pasted web content can contain nested quotes (e.g. style="...url('...')..."),
// which regex-based quote matching cannot handle reliably and ends up corrupting
// the markup fed to TCPDF (causing crashes on malformed HTML).
$dom = new DOMDocument();
libxml_use_internal_errors(true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

libxml_use_internal_errors(true) is never restored, so libxml error reporting stays silenced for the rest of the request.

Suggested change
libxml_use_internal_errors(true);
$libxml_previous_state = libxml_use_internal_errors(true);

and after libxml_clear_errors(); (line 481):

        libxml_use_internal_errors($libxml_previous_state);

$dom->loadHTML(
'<?xml encoding="utf-8" ?><div>' . $html . '</div>',
LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD,
);
libxml_clear_errors();

$wrapper = $dom->getElementsByTagName('div')->item(0);
if ($wrapper === null) {
// Fallback: parsing failed unexpectedly, keep original content rather than losing it
return $html;
}

foreach (iterator_to_array($dom->getElementsByTagName('table')) as $table) {
// Remove width/height (attributes and styles) on the table and its rows/cells
$xpath = new DOMXPath($dom);
Comment on lines +489 to +491

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DOMXPath is rebuilt on every table; hoist it out of the loop.

Suggested change
foreach (iterator_to_array($dom->getElementsByTagName('table')) as $table) {
// Remove width/height (attributes and styles) on the table and its rows/cells
$xpath = new DOMXPath($dom);
$xpath = new DOMXPath($dom);
foreach (iterator_to_array($dom->getElementsByTagName('table')) as $table) {
// Remove width/height (attributes and styles) on the table and its rows/cells

foreach ($xpath->query('.//td | .//th | .//tr | .', $table) as $node) {
if (!($node instanceof DOMElement)) {
continue;
}

$node->removeAttribute('width');
$node->removeAttribute('height');

$style = $this->removeStyleProperties($node->getAttribute('style'), ['width', 'height', 'table-layout']);
if ($style === '') {
$node->removeAttribute('style');
} else {
$node->setAttribute('style', $style);
}
}

// Add border to table if missing (for visibility)
if (!$table->hasAttribute('border') || (int) $table->getAttribute('border') < 1) {
$table->setAttribute('border', '1');
}

// Remove width/height styles only from table elements (table, td, th, tr)
$html = preg_replace('/(<(?:table|td|th|tr)\b[^>]*)\s+style\s*=\s*["\']([^"\']*)\bwidth\s*:\s*[^;"\'>]+;?([^"\']*)["\']/', '$1 style="$2$3"', $html);
$html = preg_replace('/(<(?:table|td|th|tr)\b[^>]*)\s+style\s*=\s*["\']([^"\']*)\bheight\s*:\s*[^;"\'>]+;?([^"\']*)["\']/', '$1 style="$2$3"', $html);
// Force table to 100% width for PDF (do this LAST)
$style = trim($table->getAttribute('style') . ';width:100%;', ';');
$table->setAttribute('style', $style);
}

// Remove width/height attributes only from table elements (table, td, th, tr)
$html = preg_replace('/(<(?:table|td|th|tr)\b[^>]+)\s+width\s*=\s*["\']?[^"\'\s>]+["\']?/i', '$1', $html);
$html = preg_replace('/(<(?:table|td|th|tr)\b[^>]+)\s+height\s*=\s*["\']?[^"\'\s>]+["\']?/i', '$1', $html);
$output = '';
foreach (iterator_to_array($wrapper->childNodes) as $child) {
$output .= $dom->saveHTML($child);
}

// Clean up empty style attributes and double spaces
$html = preg_replace('/\s+style\s*=\s*["\'][\s]*["\']/', '', $html);
$html = preg_replace('/\s+/', ' ', $html);
return $output;
}

// Add border to table if missing (for visibility)
if (!preg_match('/border\s*=\s*["\']?[1-9]/i', $html)) {
$html = preg_replace('/<table/i', '<table border="1"', $html, 1);
/**
* Remove the given CSS properties from an inline style declaration.
*
* @param $style string inline style attribute content
* @param $properties array lowercase property names to strip
*
* @return string
**/
private function removeStyleProperties($style, array $properties)
{
if (trim($style) === '') {
return '';
}

// Force table to 100% width for PDF (do this LAST)
$html = preg_replace('/<table([^>]*)>/i', '<table$1 style="width:100%">', $html, 1);
$kept = [];
foreach (explode(';', $style) as $declaration) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

explode(';', ...) also splits inside url(...), so a data URI is rebuilt with an extra space and becomes invalid: url(data:image/png;base64,X) comes out as url(data:image/png; base64,X).

Suggested change
foreach (explode(';', $style) as $declaration) {
foreach (preg_split('/;(?![^(]*\))/', $style) as $declaration) {

$declaration = trim($declaration);
if ($declaration === '') {
continue;
}
$property = strtolower(trim(explode(':', $declaration, 2)[0]));
if (in_array($property, $properties, true)) {
continue;
}
$kept[] = $declaration;
}

return $html;
return implode('; ', $kept);
}

/**
Expand Down