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
This commit is contained in:
Dan Brown
2021-11-25 15:12:32 +00:00
parent 709533c1fb
commit 2c21850da7
3 changed files with 85 additions and 11 deletions

View File

@ -7,21 +7,24 @@ use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page; use BookStack\Entities\Models\Page;
use BookStack\Entities\Tools\Markdown\HtmlToMarkdown; use BookStack\Entities\Tools\Markdown\HtmlToMarkdown;
use BookStack\Uploads\ImageService; use BookStack\Uploads\ImageService;
use DomPDF; use DOMDocument;
use DOMElement;
use DOMXPath;
use Exception; use Exception;
use SnappyPDF;
use Throwable; use Throwable;
class ExportFormatter class ExportFormatter
{ {
protected $imageService; protected $imageService;
protected $pdfGenerator;
/** /**
* ExportService constructor. * ExportService constructor.
*/ */
public function __construct(ImageService $imageService) public function __construct(ImageService $imageService, PdfGenerator $pdfGenerator)
{ {
$this->imageService = $imageService; $this->imageService = $imageService;
$this->pdfGenerator = $pdfGenerator;
} }
/** /**
@ -139,16 +142,40 @@ class ExportFormatter
*/ */
protected function htmlToPdf(string $html): string protected function htmlToPdf(string $html): string
{ {
$containedHtml = $this->containHtml($html); $html = $this->containHtml($html);
$useWKHTML = config('snappy.pdf.binary') !== false && config('app.allow_untrusted_server_fetching') === true; $html = $this->replaceIframesWithLinks($html);
if ($useWKHTML) { return $this->pdfGenerator->fromHtml($html);
$pdf = SnappyPDF::loadHTML($containedHtml); }
$pdf->setOption('print-media-type', true);
} else { /**
$pdf = DomPDF::loadHTML($containedHtml); * 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();
} }
/** /**

View File

@ -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();
}
}

View File

@ -6,6 +6,7 @@ use BookStack\Auth\Role;
use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page; use BookStack\Entities\Models\Page;
use BookStack\Entities\Tools\PdfGenerator;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Tests\TestCase; use Tests\TestCase;
@ -289,6 +290,24 @@ class ExportTest extends TestCase
$resp->assertDontSee('ExportWizardTheFifth'); $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() public function test_page_markdown_export()
{ {
$page = Page::query()->first(); $page = Page::query()->first();