From 6e8d9bb536e502db7eb0707f52798a2c01a3046f Mon Sep 17 00:00:00 2001 From: Herafia Date: Tue, 25 Aug 2026 12:12:57 +0200 Subject: [PATCH 1/5] fix: prevent PDF export crash on tables with nested quotes in inline styles --- inc/simplepdf.class.php | 93 +++++++++++++++++++++++++++++++++-------- 1 file changed, 76 insertions(+), 17 deletions(-) diff --git a/inc/simplepdf.class.php b/inc/simplepdf.class.php index d505647..01d7c6f 100644 --- a/inc/simplepdf.class.php +++ b/inc/simplepdf.class.php @@ -468,30 +468,89 @@ private function cleanTableHtml($html) // Remove colgroup entirely (causes fixed widths) $html = preg_replace('/]*>.*?<\/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 + // 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); + $dom->loadHTML( + '
' . $html . '
', + 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); + 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('/]*)>/i', '', $html, 1); + $kept = []; + foreach (explode(';', $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); } /** From 4f5d8c6834565d619b1acefec0bb2f5113f30f56 Mon Sep 17 00:00:00 2001 From: Herafia Date: Tue, 25 Aug 2026 12:20:34 +0200 Subject: [PATCH 2/5] CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bf37c7..2e79850 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 From fbe63d967fa535c0feca5cf1e9300f14f6e8889a Mon Sep 17 00:00:00 2001 From: Herafia Date: Tue, 25 Aug 2026 15:47:57 +0200 Subject: [PATCH 3/5] add test --- phpunit.xml | 7 ++++ tests/Units/SimplePdfTest.php | 78 +++++++++++++++++++++++++++++++++++ tests/bootstrap.php | 41 ++++++++++++++++++ 3 files changed, 126 insertions(+) create mode 100644 phpunit.xml create mode 100644 tests/Units/SimplePdfTest.php create mode 100644 tests/bootstrap.php diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..3023d8c --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,7 @@ + + + + tests + + + diff --git a/tests/Units/SimplePdfTest.php b/tests/Units/SimplePdfTest.php new file mode 100644 index 0000000..72f41bf --- /dev/null +++ b/tests/Units/SimplePdfTest.php @@ -0,0 +1,78 @@ +. + * + * @author Teclib + * @copyright Copyright (c) 2009-2022 PDF plugin team + * @license AGPL License 3.0 or (at your option) any later version + * @link https://github.com/pluginsGLPI/pdf/ + * @link http://www.glpi-project.org/ + * @package pdf + * @since 2009 + * -------------------------------------------------------------------------- + */ + +declare(strict_types=1); + +namespace GlpiPlugin\Pdf\Tests\Units; + +use Glpi\Tests\GLPITestCase; +use PluginPdfSimplePDF; +use ReflectionMethod; + +class SimplePdfTest extends GLPITestCase +{ + /** + * Test nested quote url in a style attribute + * used to break cleanTableHtml regex and crash TCPDF + */ + public function testCleanTableHtmlWithNestedQuoteUrlStyle(): void + { + $html = << + + + + + +
Content
+ HTML; + + $pdf = new PluginPdfSimplePDF(); + + $method = new ReflectionMethod(PluginPdfSimplePDF::class, 'cleanTableHtml'); + $method->setAccessible(true); + $cleaned = $method->invoke($pdf, $html); + + // no duplicated attribute on + $this->assertMatchesRegularExpression('/^]*>/', $cleaned); + preg_match('/^]*)>/', $cleaned, $matches); + $tableAttributes = $matches[1]; + + $this->assertSame(1, substr_count($tableAttributes, 'border='), 'the
tag must have a single border attribute'); + $this->assertSame(1, substr_count($tableAttributes, 'style='), 'the
tag must have a single style attribute'); + + // width/height stripped, nested url untouched + $this->assertStringNotContainsStringIgnoringCase('width: 614px', $cleaned); + $this->assertStringNotContainsStringIgnoringCase('width: 51.8pt', $cleaned); + $this->assertStringNotContainsStringIgnoringCase('height: 15.75pt', $cleaned); + $this->assertStringContainsString("url('https://example.com/pics/a.jpg')", $cleaned); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..fe0a781 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,41 @@ +. + * + * @author Teclib + * @copyright Copyright (c) 2009-2022 PDF plugin team + * @license AGPL License 3.0 or (at your option) any later version + * @link https://github.com/pluginsGLPI/pdf/ + * @link http://www.glpi-project.org/ + * @package pdf + * @since 2009 + * -------------------------------------------------------------------------- + */ + +use function Safe\realpath; + +$current_plugin_folder = basename(realpath(__DIR__ . '/../')); + +require __DIR__ . '/../../../tests/bootstrap.php'; +require dirname(__DIR__) . '/vendor/autoload.php'; + +if (!Plugin::isPluginActive($current_plugin_folder)) { + throw new RuntimeException(sprintf('Plugin %s is not active in the test database', $current_plugin_folder)); +} From 66b4a20dfba45ec6cece12cf43d2d3fbf26c7abd Mon Sep 17 00:00:00 2001 From: Herafia Date: Tue, 25 Aug 2026 16:29:05 +0200 Subject: [PATCH 4/5] header fix --- tests/Units/SimplePdfTest.php | 3 ++- tests/bootstrap.php | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/Units/SimplePdfTest.php b/tests/Units/SimplePdfTest.php index 72f41bf..89ecdee 100644 --- a/tests/Units/SimplePdfTest.php +++ b/tests/Units/SimplePdfTest.php @@ -19,13 +19,14 @@ * You should have received a copy of the GNU Affero General Public License * along with Reports. If not, see . * - * @author Teclib + * @author Nelly Mahu-Lasson, Remi Collet, Teclib * @copyright Copyright (c) 2009-2022 PDF plugin team * @license AGPL License 3.0 or (at your option) any later version * @link https://github.com/pluginsGLPI/pdf/ * @link http://www.glpi-project.org/ * @package pdf * @since 2009 + * http://www.gnu.org/licenses/agpl-3.0-standalone.html * -------------------------------------------------------------------------- */ diff --git a/tests/bootstrap.php b/tests/bootstrap.php index fe0a781..7c4ce0e 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -19,13 +19,14 @@ * You should have received a copy of the GNU Affero General Public License * along with Reports. If not, see . * - * @author Teclib + * @author Nelly Mahu-Lasson, Remi Collet, Teclib * @copyright Copyright (c) 2009-2022 PDF plugin team * @license AGPL License 3.0 or (at your option) any later version * @link https://github.com/pluginsGLPI/pdf/ * @link http://www.glpi-project.org/ * @package pdf * @since 2009 + * http://www.gnu.org/licenses/agpl-3.0-standalone.html * -------------------------------------------------------------------------- */ From d53519207db3c3f547f1dd33c856cf6d47b9b9d6 Mon Sep 17 00:00:00 2001 From: Herafia Date: Tue, 25 Aug 2026 16:39:07 +0200 Subject: [PATCH 5/5] fix test --- tests/Units/SimplePdfTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Units/SimplePdfTest.php b/tests/Units/SimplePdfTest.php index 89ecdee..b3de60f 100644 --- a/tests/Units/SimplePdfTest.php +++ b/tests/Units/SimplePdfTest.php @@ -59,7 +59,6 @@ public function testCleanTableHtmlWithNestedQuoteUrlStyle(): void $pdf = new PluginPdfSimplePDF(); $method = new ReflectionMethod(PluginPdfSimplePDF::class, 'cleanTableHtml'); - $method->setAccessible(true); $cleaned = $method->invoke($pdf, $html); // no duplicated attribute on