diff --git a/app/Services/Pdf/PdfService.php b/app/Services/Pdf/PdfService.php index a6aa9ec295..093677f084 100644 --- a/app/Services/Pdf/PdfService.php +++ b/app/Services/Pdf/PdfService.php @@ -121,7 +121,7 @@ class PdfService public function getHtml(): string { - $html = \App\Services\Pdf\Purify::purify($this->builder->document); + $html = str_replace('%24', '$', \App\Services\Pdf\Purify::clean($this->builder->document->saveHTML())); if (config('ninja.log_pdf_html')) { nlog($html); diff --git a/app/Services/Pdf/Purify.php b/app/Services/Pdf/Purify.php index cbd0af1efc..e5d6842f9b 100644 --- a/app/Services/Pdf/Purify.php +++ b/app/Services/Pdf/Purify.php @@ -85,7 +85,14 @@ class Purify // Template specific 'hidden' => ['*'], 'zoom' => ['*'], - 'size' => ['*'] + 'size' => ['*'], + + // Meta tag attributes + 'charset' => ['*'], + 'name' => ['*'], + 'content' => ['*'], + 'http-equiv' => ['*'], + 'viewport' => ['*'], ]; private static array $dangerous_css_patterns = [ @@ -142,53 +149,66 @@ class Purify 'childElementCount', 'style', 'hidden', - 'display' + 'display', + 'innerHTML', // Add innerHTML to allowed properties + 'innerText' // Add innerText since it's used in the script ]; - private static function isAllowedScript(string $script): bool + private static function isAllowedScript(string $script): bool { - // Allow both entry points with comments and whitespace - if (!preg_match('/^\s*(?:\/\/[^\n]*\n\s*)*(?:document\.(?:addEventListener\s*\(\s*[\'"]DOMContentLoaded[\'"]\s*,|querySelectorAll\s*\())/', $script)) { - return false; + // Allow the specific encoded-html script + $encodedHtmlScript = "document.addEventListener(\"DOMContentLoaded\",function(){document.querySelectorAll(`[data-state=\"encoded-html\"]`).forEach(e=>e.innerHTML=e.innerText)},!1);"; + if (trim($script) === $encodedHtmlScript) { + return true; } - $tokens = token_get_all('documentElement) { - return ''; - } + $document = new \DOMDocument(); + @$document->loadHTML(htmlspecialchars_decode(htmlspecialchars($html, ENT_QUOTES, 'UTF-8'))); // Function to recursively check nodes - $cleanNodes = function ($node) use (&$cleanNodes, &$allowed_elements, &$allowed_attributes) { + $cleanNodes = function ($node) use (&$cleanNodes) { $allowed_elements = self::$allowed_elements; $allowed_attributes = self::$allowed_attributes; @@ -386,6 +405,7 @@ class Purify }; try { + $cleanNodes($document->documentElement); return $document->saveHTML(); @@ -393,7 +413,7 @@ class Purify } catch (\Exception $e) { nlog('Error cleaning HTML: ' . $e->getMessage()); - + throw new \RuntimeException('HTML sanitization failed'); } diff --git a/app/Services/PdfMaker/PdfMaker.php b/app/Services/PdfMaker/PdfMaker.php index eb65653766..ee6434762a 100644 --- a/app/Services/PdfMaker/PdfMaker.php +++ b/app/Services/PdfMaker/PdfMaker.php @@ -29,6 +29,8 @@ class PdfMaker private $options; + public $xpath; + /** @var CommonMarkConverter */ protected $commonmark; @@ -130,7 +132,7 @@ class PdfMaker */ public function getCompiledHTML($final = false) { - $html = \App\Services\Pdf\Purify::purify($this->document); + $html = \App\Services\Pdf\Purify::clean($this->document->saveHTML()); return str_replace('%24', '$', $html); } diff --git a/app/Services/PdfMaker/PdfMakerUtilities.php b/app/Services/PdfMaker/PdfMakerUtilities.php index 0109a31b41..ed1abba533 100644 --- a/app/Services/PdfMaker/PdfMakerUtilities.php +++ b/app/Services/PdfMaker/PdfMakerUtilities.php @@ -22,7 +22,8 @@ trait PdfMakerUtilities $document = new DOMDocument(); $document->validateOnParse = true; - @$document->loadHTML(mb_convert_encoding($this->design->html(), 'HTML-ENTITIES', 'UTF-8')); + // @$document->loadHTML(mb_convert_encoding($this->design->html(), 'HTML-ENTITIES', 'UTF-8')); + @$document->loadHTML(htmlspecialchars_decode(htmlspecialchars($this->design->html(), ENT_QUOTES, 'UTF-8'))); $this->document = $document; $this->xpath = new DOMXPath($document); @@ -33,9 +34,9 @@ trait PdfMakerUtilities $element = $this->document->getElementById($selector); if ($section) { - return $element->getAttribute($section); + return $element->getAttribute($section); } - + return $element->nodeValue; } @@ -138,7 +139,7 @@ trait PdfMakerUtilities public function updateVariables(array $variables) { - $html = strtr(str_replace('%24', '$', $this->document->saveHTML()), $variables['labels']); + $html = strtr($this->getCompiledHTML(), $variables['labels']); $html = strtr($html, $variables['values']); diff --git a/app/Services/Template/TemplateService.php b/app/Services/Template/TemplateService.php index 51cda3b1a7..8130ae56f6 100644 --- a/app/Services/Template/TemplateService.php +++ b/app/Services/Template/TemplateService.php @@ -406,10 +406,8 @@ class TemplateService */ public function save(): self { - nlog("Template Service"); - $html = \App\Services\Pdf\Purify::purify($this->document); - $this->compiled_html = str_replace('%24', '$', $html); + $this->compiled_html = str_replace('%24', '$', \App\Services\Pdf\Purify::clean($this->document->saveHTML())); return $this; } diff --git a/app/Utils/Traits/Pdf/PdfMaker.php b/app/Utils/Traits/Pdf/PdfMaker.php index 52d3efa9ee..301ab49f09 100644 --- a/app/Utils/Traits/Pdf/PdfMaker.php +++ b/app/Utils/Traits/Pdf/PdfMaker.php @@ -90,7 +90,7 @@ trait PdfMaker } $html = str_ireplace(['file:/', 'iframe', 'setHtml($html) ->generate(); diff --git a/tests/Feature/PdfMaker/PdfMakerTest.php b/tests/Feature/PdfMaker/PdfMakerTest.php index 76a4515783..fa0372e247 100644 --- a/tests/Feature/PdfMaker/PdfMakerTest.php +++ b/tests/Feature/PdfMaker/PdfMakerTest.php @@ -70,7 +70,7 @@ class PdfMakerTest extends TestCase 'properties' => [ 'class' => 'my-awesome-class', 'style' => 'margin-top: 10px;', - 'script' => 'console.log(1)', + 'script' => '', ], ], ], @@ -88,8 +88,8 @@ class PdfMakerTest extends TestCase ->build(); $this->assertStringContainsString('my-awesome-class', $maker->getSection('product-table', 'class')); - $this->assertStringContainsString('margin-top: 10px;', $maker->getSection('product-table', 'style')); - $this->assertStringContainsString('console.log(1)', $maker->getSection('product-table', 'script')); + $this->assertStringContainsString('margin-top: 10px', $maker->getSection('product-table', 'style')); + // $this->assertStringContainsString('console.log(1)', $maker->getSection('product-table', 'script')); } public function testVariablesAreReplaced() @@ -115,6 +115,10 @@ class PdfMakerTest extends TestCase ->design($design) ->build(); +nlog("1".$maker->getCompiledHTML()); +nlog("2 NEXT"); +nlog("2".$maker->getSection('header')); + $this->assertStringContainsString('Invoice Ninja', $maker->getCompiledHTML()); $this->assertStringContainsString('Invoice Ninja', $maker->getSection('header')); }