]> BookStack Code Mirror - bookstack/commitdiff
Added conversion of iframes to anchors on PDF export
authorDan Brown <redacted>
Thu, 25 Nov 2021 15:12:32 +0000 (15:12 +0000)
committerDan Brown <redacted>
Thu, 25 Nov 2021 15:12:32 +0000 (15:12 +0000)
- 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
app/Entities/Tools/PdfGenerator.php [new file with mode: 0644]
tests/Entity/ExportTest.php

index 05d0ff13466ad81c9de1da4ee60b2373429e6dac..ebe0020e75d2302bfa05768e56b231887e418b21 100644 (file)
@@ -7,21 +7,24 @@ use BookStack\Entities\Models\Chapter;
 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 @@ class ExportFormatter
      */
     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 (file)
index 0000000..d606617
--- /dev/null
@@ -0,0 +1,28 @@
+<?php
+
+namespace BookStack\Entities\Tools;
+
+use Barryvdh\Snappy\Facades\SnappyPdf;
+use Barryvdh\DomPDF\Facade as DomPDF;
+
+class PdfGenerator
+{
+
+    /**
+     * Generate PDF content from the given HTML content.
+     */
+    public function fromHtml(string $html): string
+    {
+        $useWKHTML = config('snappy.pdf.binary') !== false && config('app.allow_untrusted_server_fetching') === true;
+
+        if ($useWKHTML) {
+            $pdf = SnappyPDF::loadHTML($html);
+            $pdf->setOption('print-media-type', true);
+        } else {
+            $pdf = DomPDF::loadHTML($html);
+        }
+
+        return $pdf->output();
+    }
+
+}
\ No newline at end of file
index 9ea336db8ff191592515e8ff94b9a7bf16962b0c..9a824a3da7f2ea6d4d901caa1aa671bc2487861a 100644 (file)
@@ -6,6 +6,7 @@ use BookStack\Auth\Role;
 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 @@ class ExportTest extends TestCase
         $resp->assertDontSee('ExportWizardTheFifth');
     }
 
+    public function test_page_pdf_export_converts_iframes_to_links()
+    {
+        $page = Page::query()->first()->forceFill([
+            'html'     => '<iframe width="560" height="315" src="//www.youtube.com/embed/ShqUjt33uOs"></iframe>',
+        ]);
+        $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('<p><a href="https://www.youtube.com/embed/ShqUjt33uOs">https://www.youtube.com/embed/ShqUjt33uOs</a></p>', $pdfHtml);
+    }
+
     public function test_page_markdown_export()
     {
         $page = Page::query()->first();