X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/ee24635e06a8c01d751f80caba47c57f76e8989d..refs/pull/5280/head:/app/Entities/Tools/PdfGenerator.php diff --git a/app/Entities/Tools/PdfGenerator.php b/app/Entities/Tools/PdfGenerator.php index d0c9158a9..79cd1b02f 100644 --- a/app/Entities/Tools/PdfGenerator.php +++ b/app/Entities/Tools/PdfGenerator.php @@ -2,27 +2,29 @@ namespace BookStack\Entities\Tools; -use Barryvdh\DomPDF\Facade\Pdf as DomPDF; -use Barryvdh\Snappy\Facades\SnappyPdf; +use BookStack\Exceptions\PdfExportException; +use Knp\Snappy\Pdf as SnappyPdf; +use Dompdf\Dompdf; +use Symfony\Component\Process\Exception\ProcessTimedOutException; +use Symfony\Component\Process\Process; class PdfGenerator { const ENGINE_DOMPDF = 'dompdf'; const ENGINE_WKHTML = 'wkhtml'; + const ENGINE_COMMAND = 'command'; /** * Generate PDF content from the given HTML content. + * @throws PdfExportException */ public function fromHtml(string $html): string { - if ($this->getActiveEngine() === self::ENGINE_WKHTML) { - $pdf = SnappyPDF::loadHTML($html); - $pdf->setOption('print-media-type', true); - } else { - $pdf = DomPDF::loadHTML($html); - } - - return $pdf->output(); + return match ($this->getActiveEngine()) { + self::ENGINE_COMMAND => $this->renderUsingCommand($html), + self::ENGINE_WKHTML => $this->renderUsingWkhtml($html), + default => $this->renderUsingDomPdf($html) + }; } /** @@ -31,8 +33,107 @@ class PdfGenerator */ public function getActiveEngine(): string { - $useWKHTML = config('snappy.pdf.binary') !== false && config('app.allow_untrusted_server_fetching') === true; + if (config('exports.pdf_command')) { + return self::ENGINE_COMMAND; + } + + if ($this->getWkhtmlBinaryPath() && config('app.allow_untrusted_server_fetching') === true) { + return self::ENGINE_WKHTML; + } + + return self::ENGINE_DOMPDF; + } + + protected function getWkhtmlBinaryPath(): string + { + $wkhtmlBinaryPath = config('exports.snappy.pdf_binary'); + if (file_exists(base_path('wkhtmltopdf'))) { + $wkhtmlBinaryPath = base_path('wkhtmltopdf'); + } + + return $wkhtmlBinaryPath ?: ''; + } + + protected function renderUsingDomPdf(string $html): string + { + $options = config('exports.dompdf'); + $domPdf = new Dompdf($options); + $domPdf->setBasePath(base_path('public')); + + $domPdf->loadHTML($this->convertEntities($html)); + $domPdf->render(); + + return (string) $domPdf->output(); + } + + /** + * @throws PdfExportException + */ + protected function renderUsingCommand(string $html): string + { + $command = config('exports.pdf_command'); + $inputHtml = tempnam(sys_get_temp_dir(), 'bs-pdfgen-html-'); + $outputPdf = tempnam(sys_get_temp_dir(), 'bs-pdfgen-output-'); + + $replacementsByPlaceholder = [ + '{input_html_path}' => $inputHtml, + '{output_pdf_path}' => $outputPdf, + ]; - return $useWKHTML ? self::ENGINE_WKHTML : self::ENGINE_DOMPDF; + foreach ($replacementsByPlaceholder as $placeholder => $replacement) { + $command = str_replace($placeholder, escapeshellarg($replacement), $command); + } + + file_put_contents($inputHtml, $html); + + $timeout = intval(config('exports.pdf_command_timeout')); + $process = Process::fromShellCommandline($command); + $process->setTimeout($timeout); + + try { + $process->run(); + } catch (ProcessTimedOutException $e) { + throw new PdfExportException("PDF Export via command failed due to timeout at {$timeout} second(s)"); + } + + if (!$process->isSuccessful()) { + throw new PdfExportException("PDF Export via command failed with exit code {$process->getExitCode()}, stdout: {$process->getOutput()}, stderr: {$process->getErrorOutput()}"); + } + + $pdfContents = file_get_contents($outputPdf); + unlink($outputPdf); + + if ($pdfContents === false) { + throw new PdfExportException("PDF Export via command failed, unable to read PDF output file"); + } else if (empty($pdfContents)) { + throw new PdfExportException("PDF Export via command failed, PDF output file is empty"); + } + + return $pdfContents; + } + + protected function renderUsingWkhtml(string $html): string + { + $snappy = new SnappyPdf($this->getWkhtmlBinaryPath()); + $options = config('exports.snappy.options'); + return $snappy->getOutputFromHtml($html, $options); + } + + /** + * Taken from https://github.com/barryvdh/laravel-dompdf/blob/v2.1.1/src/PDF.php + * Copyright (c) 2021 barryvdh, MIT License + * https://github.com/barryvdh/laravel-dompdf/blob/v2.1.1/LICENSE + */ + protected function convertEntities(string $subject): string + { + $entities = [ + '€' => '€', + '£' => '£', + ]; + + foreach ($entities as $search => $replace) { + $subject = str_replace($search, $replace, $subject); + } + return $subject; } }