From 2c21850da728dce55cec2e84ec73ed474ba0bd0a Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 25 Nov 2021 15:12:32 +0000 Subject: [PATCH] Added conversion of iframes to anchors on PDF export - Replaced iframe elements with anchor elements wrapped in a paragraph. - Extracted PDF generation action to seperate class for easier mocking within testing. - Added test to cover. For #3077 --- app/Entities/Tools/ExportFormatter.php | 49 ++++++++++++++++++++------ app/Entities/Tools/PdfGenerator.php | 28 +++++++++++++++ tests/Entity/ExportTest.php | 19 ++++++++++ 3 files changed, 85 insertions(+), 11 deletions(-) create mode 100644 app/Entities/Tools/PdfGenerator.php diff --git a/app/Entities/Tools/ExportFormatter.php b/app/Entities/Tools/ExportFormatter.php index 05d0ff13466..ebe0020e75d 100644 --- a/app/Entities/Tools/ExportFormatter.php +++ b/app/Entities/Tools/ExportFormatter.php @@ -7,21 +7,24 @@ use BookStack\Entities\Models\Page; use BookStack\Entities\Tools\Markdown\HtmlToMarkdown; use BookStack\Uploads\ImageService; -use DomPDF; +use DOMDocument; +use DOMElement; +use DOMXPath; use Exception; -use SnappyPDF; use Throwable; class ExportFormatter { protected $imageService; + protected $pdfGenerator; /** * ExportService constructor. */ - public function __construct(ImageService $imageService) + public function __construct(ImageService $imageService, PdfGenerator $pdfGenerator) { $this->imageService = $imageService; + $this->pdfGenerator = $pdfGenerator; } /** @@ -139,16 +142,40 @@ public function bookToPdf(Book $book) */ protected function htmlToPdf(string $html): string { - $containedHtml = $this->containHtml($html); - $useWKHTML = config('snappy.pdf.binary') !== false && config('app.allow_untrusted_server_fetching') === true; - if ($useWKHTML) { - $pdf = SnappyPDF::loadHTML($containedHtml); - $pdf->setOption('print-media-type', true); - } else { - $pdf = DomPDF::loadHTML($containedHtml); + $html = $this->containHtml($html); + $html = $this->replaceIframesWithLinks($html); + return $this->pdfGenerator->fromHtml($html); + } + + /** + * Within the given HTML content, replace any iframe elements + * with anchor links within paragraph blocks. + */ + protected function replaceIframesWithLinks(string $html): string + { + libxml_use_internal_errors(true); + + $doc = new DOMDocument(); + $doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8')); + $xPath = new DOMXPath($doc); + + + $iframes = $xPath->query('//iframe'); + /** @var DOMElement $iframe */ + foreach ($iframes as $iframe) { + $link = $iframe->getAttribute('src'); + if (strpos($link, '//') === 0) { + $link = 'https:' . $link; + } + + $anchor = $doc->createElement('a', $link); + $anchor->setAttribute('href', $link); + $paragraph = $doc->createElement('p'); + $paragraph->appendChild($anchor); + $iframe->replaceWith($paragraph); } - return $pdf->output(); + return $doc->saveHTML(); } /** diff --git a/app/Entities/Tools/PdfGenerator.php b/app/Entities/Tools/PdfGenerator.php new file mode 100644 index 00000000000..d606617a402 --- /dev/null +++ b/app/Entities/Tools/PdfGenerator.php @@ -0,0 +1,28 @@ +setOption('print-media-type', true); + } else { + $pdf = DomPDF::loadHTML($html); + } + + return $pdf->output(); + } + +} \ No newline at end of file diff --git a/tests/Entity/ExportTest.php b/tests/Entity/ExportTest.php index 9ea336db8ff..9a824a3da7f 100644 --- a/tests/Entity/ExportTest.php +++ b/tests/Entity/ExportTest.php @@ -6,6 +6,7 @@ use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Page; +use BookStack\Entities\Tools\PdfGenerator; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; use Tests\TestCase; @@ -289,6 +290,24 @@ public function test_page_export_with_deleted_creator_and_updater() $resp->assertDontSee('ExportWizardTheFifth'); } + public function test_page_pdf_export_converts_iframes_to_links() + { + $page = Page::query()->first()->forceFill([ + 'html' => '', + ]); + $page->save(); + + $pdfHtml = ''; + $mockPdfGenerator = $this->mock(PdfGenerator::class); + $mockPdfGenerator->shouldReceive('fromHtml') + ->with(\Mockery::capture($pdfHtml)) + ->andReturn(''); + + $this->asEditor()->get($page->getUrl('/export/pdf')); + $this->assertStringNotContainsString('iframe>', $pdfHtml); + $this->assertStringContainsString('

https://www.youtube.com/embed/ShqUjt33uOs

', $pdfHtml); + } + public function test_page_markdown_export() { $page = Page::query()->first();