From bb6670d395180f8a81bcbfd92da1896fcfb18d34 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 22 Apr 2024 16:40:42 +0100 Subject: [PATCH 1/4] PDF: Started new command option, merged options, simplified dompdf - Updated DOMPDF to direcly use library instead of depending on barry wrapper. - Merged existing export options file into single exports file. - Defined option for new command option. Related to #4732 --- .env.example.complete | 8 ++ app/Config/app.php | 1 - app/Config/{dompdf.php => exports.php} | 53 +++++++++---- app/Config/snappy.php | 34 -------- app/Entities/Tools/PdfGenerator.php | 56 +++++++++++-- composer.json | 2 +- composer.lock | 105 ++++--------------------- readme.md | 1 - 8 files changed, 110 insertions(+), 150 deletions(-) rename app/Config/{dompdf.php => exports.php} (88%) delete mode 100644 app/Config/snappy.php diff --git a/.env.example.complete b/.env.example.complete index 1242968182a..b4beb60cc0e 100644 --- a/.env.example.complete +++ b/.env.example.complete @@ -325,6 +325,14 @@ FILE_UPLOAD_SIZE_LIMIT=50 # Can be 'a4' or 'letter'. EXPORT_PAGE_SIZE=a4 +# Export PDF Command +# Set a command which can be used to convert a HTML file into a PDF file. +# When false this will not be used. +# String values represent the command to be called for conversion. +# Supports '{input_html_path}' and '{output_pdf_path}' placeholder values. +# Example: EXPORT_PDF_COMMAND="/scripts/convert.sh {input_html_path} {output_pdf_path}" +EXPORT_PDF_COMMAND=false + # Set path to wkhtmltopdf binary for PDF generation. # Can be 'false' or a path path like: '/home/bins/wkhtmltopdf' # When false, BookStack will attempt to find a wkhtmltopdf in the application diff --git a/app/Config/app.php b/app/Config/app.php index dda787f3f99..67f31159f60 100644 --- a/app/Config/app.php +++ b/app/Config/app.php @@ -116,7 +116,6 @@ // Application Service Providers 'providers' => ServiceProvider::defaultProviders()->merge([ // Third party service providers - Barryvdh\DomPDF\ServiceProvider::class, Barryvdh\Snappy\ServiceProvider::class, SocialiteProviders\Manager\ServiceProvider::class, diff --git a/app/Config/dompdf.php b/app/Config/exports.php similarity index 88% rename from app/Config/dompdf.php rename to app/Config/exports.php index 09dd91bcc3c..63cc2419d3b 100644 --- a/app/Config/dompdf.php +++ b/app/Config/exports.php @@ -1,23 +1,56 @@ 'A4', + 'letter' => 'Letter', +]; + $dompdfPaperSizeMap = [ 'a4' => 'a4', 'letter' => 'letter', ]; +$exportPageSize = env('EXPORT_PAGE_SIZE', 'a4'); + return [ - 'show_warnings' => false, // Throw an Exception on warnings from dompdf + // Set a command which can be used to convert a HTML file into a PDF file. + // When false this will not be used. + // String values represent the command to be called for conversion. + // Supports '{input_html_path}' and '{output_pdf_path}' placeholder values. + // Example: EXPORT_PDF_COMMAND="/scripts/convert.sh {input_html_path} {output_pdf_path}" + 'pdf_command' => env('EXPORT_PDF_COMMAND', false), + + // 2024-04: Snappy/WKHTMLtoPDF now considered deprecated in regard to BookStack support. + 'snappy' => [ + 'pdf' => [ + 'enabled' => true, + 'binary' => file_exists(base_path('wkhtmltopdf')) ? base_path('wkhtmltopdf') : env('WKHTMLTOPDF', false), + 'timeout' => false, + 'options' => [ + 'outline' => true, + 'page-size' => $snappyPaperSizeMap[$exportPageSize] ?? 'A4', + ], + 'env' => [], + ], + 'image' => [ + 'enabled' => false, + 'binary' => '/usr/local/bin/wkhtmltoimage', + 'timeout' => false, + 'options' => [], + 'env' => [], + ], + ], - 'options' => [ + 'dompdf' => [ /** * The location of the DOMPDF font directory. * @@ -101,7 +134,7 @@ /** * Whether to enable font subsetting or not. */ - 'enable_fontsubsetting' => false, + 'enable_font_subsetting' => false, /** * The PDF rendering backend to use. @@ -165,7 +198,7 @@ * * @see CPDF_Adapter::PAPER_SIZES for valid sizes ('letter', 'legal', 'A4', etc.) */ - 'default_paper_size' => $dompdfPaperSizeMap[env('EXPORT_PAGE_SIZE', 'a4')] ?? 'a4', + 'default_paper_size' => $dompdfPaperSizeMap[$exportPageSize] ?? 'a4', /** * The default paper orientation. @@ -268,15 +301,6 @@ */ 'font_height_ratio' => 1.1, - /** - * Enable CSS float. - * - * Allows people to disabled CSS float support - * - * @var bool - */ - 'enable_css_float' => true, - /** * Use the HTML5 Lib parser. * @@ -286,5 +310,4 @@ */ 'enable_html5_parser' => true, ], - ]; diff --git a/app/Config/snappy.php b/app/Config/snappy.php deleted file mode 100644 index a87ce805f92..00000000000 --- a/app/Config/snappy.php +++ /dev/null @@ -1,34 +0,0 @@ - 'A4', - 'letter' => 'Letter', -]; - -return [ - 'pdf' => [ - 'enabled' => true, - 'binary' => file_exists(base_path('wkhtmltopdf')) ? base_path('wkhtmltopdf') : env('WKHTMLTOPDF', false), - 'timeout' => false, - 'options' => [ - 'outline' => true, - 'page-size' => $snappyPaperSizeMap[env('EXPORT_PAGE_SIZE', 'a4')] ?? 'A4', - ], - 'env' => [], - ], - 'image' => [ - 'enabled' => false, - 'binary' => '/usr/local/bin/wkhtmltoimage', - 'timeout' => false, - 'options' => [], - 'env' => [], - ], -]; diff --git a/app/Entities/Tools/PdfGenerator.php b/app/Entities/Tools/PdfGenerator.php index d0c9158a91c..7502c10ff43 100644 --- a/app/Entities/Tools/PdfGenerator.php +++ b/app/Entities/Tools/PdfGenerator.php @@ -2,27 +2,32 @@ namespace BookStack\Entities\Tools; -use Barryvdh\DomPDF\Facade\Pdf as DomPDF; use Barryvdh\Snappy\Facades\SnappyPdf; +use Dompdf\Dompdf; class PdfGenerator { const ENGINE_DOMPDF = 'dompdf'; const ENGINE_WKHTML = 'wkhtml'; + const ENGINE_COMMAND = 'command'; /** * Generate PDF content from the given HTML content. */ public function fromHtml(string $html): string { - if ($this->getActiveEngine() === self::ENGINE_WKHTML) { + $engine = $this->getActiveEngine(); + + if ($engine === self::ENGINE_WKHTML) { $pdf = SnappyPDF::loadHTML($html); $pdf->setOption('print-media-type', true); - } else { - $pdf = DomPDF::loadHTML($html); + return $pdf->output(); + } else if ($engine === self::ENGINE_COMMAND) { + // TODO - Support PDF command + return ''; } - return $pdf->output(); + return $this->renderUsingDomPdf($html); } /** @@ -31,8 +36,45 @@ public function fromHtml(string $html): string */ public function getActiveEngine(): string { - $useWKHTML = config('snappy.pdf.binary') !== false && config('app.allow_untrusted_server_fetching') === true; + $wkhtmlBinaryPath = config('snappy.pdf.binary'); + if (file_exists(base_path('wkhtmltopdf'))) { + $wkhtmlBinaryPath = base_path('wkhtmltopdf'); + } + + if (is_string($wkhtmlBinaryPath) && config('app.allow_untrusted_server_fetching') === true) { + return self::ENGINE_WKHTML; + } + + return self::ENGINE_DOMPDF; + } + + protected function renderUsingDomPdf(string $html): string + { + $options = config('exports.dompdf'); + $domPdf = new Dompdf($options); + $domPdf->setBasePath(base_path('public')); - return $useWKHTML ? self::ENGINE_WKHTML : self::ENGINE_DOMPDF; + $domPdf->loadHTML($this->convertEntities($html)); + $domPdf->render(); + + return (string) $domPdf->output(); + } + + /** + * 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; } } diff --git a/composer.json b/composer.json index b22c7b44de9..94f64ec7245 100644 --- a/composer.json +++ b/composer.json @@ -17,9 +17,9 @@ "ext-mbstring": "*", "ext-xml": "*", "bacon/bacon-qr-code": "^2.0", - "barryvdh/laravel-dompdf": "^2.0", "barryvdh/laravel-snappy": "^1.0", "doctrine/dbal": "^3.5", + "dompdf/dompdf": "^2.0", "guzzlehttp/guzzle": "^7.4", "intervention/image": "^3.5", "laravel/framework": "^10.10", diff --git a/composer.lock b/composer.lock index 24c2215dd5f..657a5a7fb78 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ccfc07d0ecc580962915a0457f0466a7", + "content-hash": "c0c5a3169cb23d9ab8e34324202d4c37", "packages": [ { "name": "aws/aws-crt-php", @@ -209,83 +209,6 @@ }, "time": "2022-12-07T17:46:57+00:00" }, - { - "name": "barryvdh/laravel-dompdf", - "version": "v2.1.1", - "source": { - "type": "git", - "url": "https://github.com/barryvdh/laravel-dompdf.git", - "reference": "cb37868365f9b937039d316727a1fced1e87b31c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/cb37868365f9b937039d316727a1fced1e87b31c", - "reference": "cb37868365f9b937039d316727a1fced1e87b31c", - "shasum": "" - }, - "require": { - "dompdf/dompdf": "^2.0.3", - "illuminate/support": "^6|^7|^8|^9|^10|^11", - "php": "^7.2 || ^8.0" - }, - "require-dev": { - "larastan/larastan": "^1.0|^2.7.0", - "orchestra/testbench": "^4|^5|^6|^7|^8|^9", - "phpro/grumphp": "^1 || ^2.5", - "squizlabs/php_codesniffer": "^3.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - }, - "laravel": { - "providers": [ - "Barryvdh\\DomPDF\\ServiceProvider" - ], - "aliases": { - "Pdf": "Barryvdh\\DomPDF\\Facade\\Pdf", - "PDF": "Barryvdh\\DomPDF\\Facade\\Pdf" - } - } - }, - "autoload": { - "psr-4": { - "Barryvdh\\DomPDF\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Barry vd. Heuvel", - "email": "barryvdh@gmail.com" - } - ], - "description": "A DOMPDF Wrapper for Laravel", - "keywords": [ - "dompdf", - "laravel", - "pdf" - ], - "support": { - "issues": "https://github.com/barryvdh/laravel-dompdf/issues", - "source": "https://github.com/barryvdh/laravel-dompdf/tree/v2.1.1" - }, - "funding": [ - { - "url": "https://fruitcake.nl", - "type": "custom" - }, - { - "url": "https://github.com/barryvdh", - "type": "github" - } - ], - "time": "2024-03-15T12:48:39+00:00" - }, { "name": "barryvdh/laravel-snappy", "version": "v1.0.3", @@ -1127,16 +1050,16 @@ }, { "name": "dompdf/dompdf", - "version": "v2.0.4", + "version": "v2.0.7", "source": { "type": "git", "url": "https://github.com/dompdf/dompdf.git", - "reference": "093f2d9739cec57428e39ddadedfd4f3ae862c0f" + "reference": "ab0123052b42ad0867348f25df8c228f1ece8f14" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dompdf/dompdf/zipball/093f2d9739cec57428e39ddadedfd4f3ae862c0f", - "reference": "093f2d9739cec57428e39ddadedfd4f3ae862c0f", + "url": "https://api.github.com/repos/dompdf/dompdf/zipball/ab0123052b42ad0867348f25df8c228f1ece8f14", + "reference": "ab0123052b42ad0867348f25df8c228f1ece8f14", "shasum": "" }, "require": { @@ -1144,7 +1067,7 @@ "ext-mbstring": "*", "masterminds/html5": "^2.0", "phenx/php-font-lib": ">=0.5.4 <1.0.0", - "phenx/php-svg-lib": ">=0.3.3 <1.0.0", + "phenx/php-svg-lib": ">=0.5.2 <1.0.0", "php": "^7.1 || ^8.0" }, "require-dev": { @@ -1183,9 +1106,9 @@ "homepage": "https://github.com/dompdf/dompdf", "support": { "issues": "https://github.com/dompdf/dompdf/issues", - "source": "https://github.com/dompdf/dompdf/tree/v2.0.4" + "source": "https://github.com/dompdf/dompdf/tree/v2.0.7" }, - "time": "2023-12-12T20:19:39+00:00" + "time": "2024-04-15T12:40:33+00:00" }, { "name": "dragonmantank/cron-expression", @@ -4067,16 +3990,16 @@ }, { "name": "phenx/php-svg-lib", - "version": "0.5.3", + "version": "0.5.4", "source": { "type": "git", "url": "https://github.com/dompdf/php-svg-lib.git", - "reference": "0e46722c154726a5f9ac218197ccc28adba16fcf" + "reference": "46b25da81613a9cf43c83b2a8c2c1bdab27df691" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/0e46722c154726a5f9ac218197ccc28adba16fcf", - "reference": "0e46722c154726a5f9ac218197ccc28adba16fcf", + "url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/46b25da81613a9cf43c83b2a8c2c1bdab27df691", + "reference": "46b25da81613a9cf43c83b2a8c2c1bdab27df691", "shasum": "" }, "require": { @@ -4107,9 +4030,9 @@ "homepage": "https://github.com/PhenX/php-svg-lib", "support": { "issues": "https://github.com/dompdf/php-svg-lib/issues", - "source": "https://github.com/dompdf/php-svg-lib/tree/0.5.3" + "source": "https://github.com/dompdf/php-svg-lib/tree/0.5.4" }, - "time": "2024-02-23T20:39:24+00:00" + "time": "2024-04-08T12:52:34+00:00" }, { "name": "phpoption/phpoption", diff --git a/readme.md b/readme.md index 17e1a05f659..5adcc06bbcc 100644 --- a/readme.md +++ b/readme.md @@ -142,7 +142,6 @@ Note: This is not an exhaustive list of all libraries and projects that would be * [Google Material Icons](https://github.com/google/material-design-icons) - _[Apache-2.0](https://github.com/google/material-design-icons/blob/master/LICENSE)_ * [markdown-it](https://github.com/markdown-it/markdown-it) and [markdown-it-task-lists](https://github.com/revin/markdown-it-task-lists) - _[MIT](https://github.com/markdown-it/markdown-it/blob/master/LICENSE) and [ISC](https://github.com/revin/markdown-it-task-lists/blob/master/LICENSE)_ * [Dompdf](https://github.com/dompdf/dompdf) - _[LGPL v2.1](https://github.com/dompdf/dompdf/blob/master/LICENSE.LGPL)_ -* [BarryVD/Dompdf](https://github.com/barryvdh/laravel-dompdf) - _[MIT](https://github.com/barryvdh/laravel-dompdf/blob/master/LICENSE)_ * [BarryVD/Snappy (WKHTML2PDF)](https://github.com/barryvdh/laravel-snappy) - _[MIT](https://github.com/barryvdh/laravel-snappy/blob/master/LICENSE)_ * [WKHTMLtoPDF](http://wkhtmltopdf.org/index.html) - _[LGPL v3.0](https://github.com/wkhtmltopdf/wkhtmltopdf/blob/master/LICENSE)_ * [diagrams.net](https://github.com/jgraph/drawio) - _[Embedded Version Terms](https://www.diagrams.net/trust/) / [Source Project - Apache-2.0](https://github.com/jgraph/drawio/blob/dev/LICENSE)_ From 40200856af366f735283da4cc1b28519ddb3586b Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 24 Apr 2024 15:13:44 +0100 Subject: [PATCH 2/4] PDF: Removed barryvdh snappy to use snappy direct Also simplifies config format, and updates snappy implmentation to use the new config file. Not yet tested. --- app/Config/app.php | 1 - app/Config/exports.php | 21 ++------ app/Entities/Tools/PdfGenerator.php | 30 +++++++---- composer.json | 2 +- composer.lock | 80 +---------------------------- readme.md | 2 +- 6 files changed, 28 insertions(+), 108 deletions(-) diff --git a/app/Config/app.php b/app/Config/app.php index 67f31159f60..b96d0bdb788 100644 --- a/app/Config/app.php +++ b/app/Config/app.php @@ -116,7 +116,6 @@ // Application Service Providers 'providers' => ServiceProvider::defaultProviders()->merge([ // Third party service providers - Barryvdh\Snappy\ServiceProvider::class, SocialiteProviders\Manager\ServiceProvider::class, // BookStack custom service providers diff --git a/app/Config/exports.php b/app/Config/exports.php index 63cc2419d3b..88dc08cba35 100644 --- a/app/Config/exports.php +++ b/app/Config/exports.php @@ -31,22 +31,11 @@ // 2024-04: Snappy/WKHTMLtoPDF now considered deprecated in regard to BookStack support. 'snappy' => [ - 'pdf' => [ - 'enabled' => true, - 'binary' => file_exists(base_path('wkhtmltopdf')) ? base_path('wkhtmltopdf') : env('WKHTMLTOPDF', false), - 'timeout' => false, - 'options' => [ - 'outline' => true, - 'page-size' => $snappyPaperSizeMap[$exportPageSize] ?? 'A4', - ], - 'env' => [], - ], - 'image' => [ - 'enabled' => false, - 'binary' => '/usr/local/bin/wkhtmltoimage', - 'timeout' => false, - 'options' => [], - 'env' => [], + 'pdf_binary' => env('WKHTMLTOPDF', false), + 'options' => [ + 'print-media-type' => true, + 'outline' => true, + 'page-size' => $snappyPaperSizeMap[$exportPageSize] ?? 'A4', ], ], diff --git a/app/Entities/Tools/PdfGenerator.php b/app/Entities/Tools/PdfGenerator.php index 7502c10ff43..e187b9ab2c4 100644 --- a/app/Entities/Tools/PdfGenerator.php +++ b/app/Entities/Tools/PdfGenerator.php @@ -2,7 +2,7 @@ namespace BookStack\Entities\Tools; -use Barryvdh\Snappy\Facades\SnappyPdf; +use Knp\Snappy\Pdf as SnappyPdf; use Dompdf\Dompdf; class PdfGenerator @@ -19,9 +19,7 @@ public function fromHtml(string $html): string $engine = $this->getActiveEngine(); if ($engine === self::ENGINE_WKHTML) { - $pdf = SnappyPDF::loadHTML($html); - $pdf->setOption('print-media-type', true); - return $pdf->output(); + return $this->renderUsingWkhtml($html); } else if ($engine === self::ENGINE_COMMAND) { // TODO - Support PDF command return ''; @@ -36,18 +34,23 @@ public function fromHtml(string $html): string */ public function getActiveEngine(): string { - $wkhtmlBinaryPath = config('snappy.pdf.binary'); - if (file_exists(base_path('wkhtmltopdf'))) { - $wkhtmlBinaryPath = base_path('wkhtmltopdf'); - } - - if (is_string($wkhtmlBinaryPath) && config('app.allow_untrusted_server_fetching') === true) { + 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'); @@ -60,6 +63,13 @@ protected function renderUsingDomPdf(string $html): string return (string) $domPdf->output(); } + 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 diff --git a/composer.json b/composer.json index 94f64ec7245..b90ab224eaa 100644 --- a/composer.json +++ b/composer.json @@ -17,11 +17,11 @@ "ext-mbstring": "*", "ext-xml": "*", "bacon/bacon-qr-code": "^2.0", - "barryvdh/laravel-snappy": "^1.0", "doctrine/dbal": "^3.5", "dompdf/dompdf": "^2.0", "guzzlehttp/guzzle": "^7.4", "intervention/image": "^3.5", + "knplabs/knp-snappy": "^1.5", "laravel/framework": "^10.10", "laravel/socialite": "^5.10", "laravel/tinker": "^2.8", diff --git a/composer.lock b/composer.lock index 657a5a7fb78..ad5648d6bc3 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c0c5a3169cb23d9ab8e34324202d4c37", + "content-hash": "97259e40ffe5518cfcdf1e32eacbb175", "packages": [ { "name": "aws/aws-crt-php", @@ -209,84 +209,6 @@ }, "time": "2022-12-07T17:46:57+00:00" }, - { - "name": "barryvdh/laravel-snappy", - "version": "v1.0.3", - "source": { - "type": "git", - "url": "https://github.com/barryvdh/laravel-snappy.git", - "reference": "716dcb6db24de4ce8e6ae5941cfab152af337ea0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-snappy/zipball/716dcb6db24de4ce8e6ae5941cfab152af337ea0", - "reference": "716dcb6db24de4ce8e6ae5941cfab152af337ea0", - "shasum": "" - }, - "require": { - "illuminate/filesystem": "^9|^10|^11.0", - "illuminate/support": "^9|^10|^11.0", - "knplabs/knp-snappy": "^1.4.4", - "php": ">=7.2" - }, - "require-dev": { - "orchestra/testbench": "^7|^8|^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - }, - "laravel": { - "providers": [ - "Barryvdh\\Snappy\\ServiceProvider" - ], - "aliases": { - "PDF": "Barryvdh\\Snappy\\Facades\\SnappyPdf", - "SnappyImage": "Barryvdh\\Snappy\\Facades\\SnappyImage" - } - } - }, - "autoload": { - "psr-4": { - "Barryvdh\\Snappy\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Barry vd. Heuvel", - "email": "barryvdh@gmail.com" - } - ], - "description": "Snappy PDF/Image for Laravel", - "keywords": [ - "image", - "laravel", - "pdf", - "snappy", - "wkhtmltoimage", - "wkhtmltopdf" - ], - "support": { - "issues": "https://github.com/barryvdh/laravel-snappy/issues", - "source": "https://github.com/barryvdh/laravel-snappy/tree/v1.0.3" - }, - "funding": [ - { - "url": "https://fruitcake.nl", - "type": "custom" - }, - { - "url": "https://github.com/barryvdh", - "type": "github" - } - ], - "time": "2024-03-09T19:20:39+00:00" - }, { "name": "brick/math", "version": "0.11.0", diff --git a/readme.md b/readme.md index 5adcc06bbcc..c46e1641f19 100644 --- a/readme.md +++ b/readme.md @@ -142,7 +142,7 @@ Note: This is not an exhaustive list of all libraries and projects that would be * [Google Material Icons](https://github.com/google/material-design-icons) - _[Apache-2.0](https://github.com/google/material-design-icons/blob/master/LICENSE)_ * [markdown-it](https://github.com/markdown-it/markdown-it) and [markdown-it-task-lists](https://github.com/revin/markdown-it-task-lists) - _[MIT](https://github.com/markdown-it/markdown-it/blob/master/LICENSE) and [ISC](https://github.com/revin/markdown-it-task-lists/blob/master/LICENSE)_ * [Dompdf](https://github.com/dompdf/dompdf) - _[LGPL v2.1](https://github.com/dompdf/dompdf/blob/master/LICENSE.LGPL)_ -* [BarryVD/Snappy (WKHTML2PDF)](https://github.com/barryvdh/laravel-snappy) - _[MIT](https://github.com/barryvdh/laravel-snappy/blob/master/LICENSE)_ +* [KnpLabs/snappy](https://github.com/KnpLabs/snappy) - _[MIT](https://github.com/KnpLabs/snappy/blob/master/LICENSE)_ * [WKHTMLtoPDF](http://wkhtmltopdf.org/index.html) - _[LGPL v3.0](https://github.com/wkhtmltopdf/wkhtmltopdf/blob/master/LICENSE)_ * [diagrams.net](https://github.com/jgraph/drawio) - _[Embedded Version Terms](https://www.diagrams.net/trust/) / [Source Project - Apache-2.0](https://github.com/jgraph/drawio/blob/dev/LICENSE)_ * [OneLogin's SAML PHP Toolkit](https://github.com/onelogin/php-saml) - _[MIT](https://github.com/onelogin/php-saml/blob/master/LICENSE)_ From 1c7128c2cb08271aa456951f6d6b4ce930df5cb5 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 24 Apr 2024 16:09:53 +0100 Subject: [PATCH 3/4] PDF: Added implmentation of command PDF option Tested quickly manually but not yet covered by PHPUnit tests. --- app/Entities/Tools/PdfGenerator.php | 62 ++++++++++++++++++++++----- app/Exceptions/PdfExportException.php | 7 +++ phpunit.xml | 1 + 3 files changed, 60 insertions(+), 10 deletions(-) create mode 100644 app/Exceptions/PdfExportException.php diff --git a/app/Entities/Tools/PdfGenerator.php b/app/Entities/Tools/PdfGenerator.php index e187b9ab2c4..4f23ad334ec 100644 --- a/app/Entities/Tools/PdfGenerator.php +++ b/app/Entities/Tools/PdfGenerator.php @@ -2,8 +2,10 @@ namespace BookStack\Entities\Tools; +use BookStack\Exceptions\PdfExportException; use Knp\Snappy\Pdf as SnappyPdf; use Dompdf\Dompdf; +use Symfony\Component\Process\Process; class PdfGenerator { @@ -13,19 +15,15 @@ class PdfGenerator /** * Generate PDF content from the given HTML content. + * @throws PdfExportException */ public function fromHtml(string $html): string { - $engine = $this->getActiveEngine(); - - if ($engine === self::ENGINE_WKHTML) { - return $this->renderUsingWkhtml($html); - } else if ($engine === self::ENGINE_COMMAND) { - // TODO - Support PDF command - return ''; - } - - return $this->renderUsingDomPdf($html); + return match ($this->getActiveEngine()) { + self::ENGINE_COMMAND => $this->renderUsingCommand($html), + self::ENGINE_WKHTML => $this->renderUsingWkhtml($html), + default => $this->renderUsingDomPdf($html) + }; } /** @@ -34,6 +32,10 @@ public function fromHtml(string $html): string */ public function getActiveEngine(): string { + if (config('exports.pdf_command')) { + return self::ENGINE_COMMAND; + } + if ($this->getWkhtmlBinaryPath() && config('app.allow_untrusted_server_fetching') === true) { return self::ENGINE_WKHTML; } @@ -63,6 +65,46 @@ protected function renderUsingDomPdf(string $html): string 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_html_path}' => $outputPdf, + ]; + + foreach ($replacementsByPlaceholder as $placeholder => $replacement) { + $command = str_replace($placeholder, escapeshellarg($replacement), $command); + } + + file_put_contents($inputHtml, $html); + + $process = Process::fromShellCommandline($command); + $process->setTimeout(15); + $process->run(); + + 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()); diff --git a/app/Exceptions/PdfExportException.php b/app/Exceptions/PdfExportException.php new file mode 100644 index 00000000000..beeda814f83 --- /dev/null +++ b/app/Exceptions/PdfExportException.php @@ -0,0 +1,7 @@ + + From f0dd33c1b47a1ae26c58e0d32ef824d2635efeb0 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 26 Apr 2024 15:39:40 +0100 Subject: [PATCH 4/4] PDF: Added tests for pdf command, fixed old tests for changes --- app/Entities/Tools/PdfGenerator.php | 2 +- tests/Entity/ExportTest.php | 39 +++++++++++++++++++++++++++-- tests/Unit/ConfigTest.php | 16 ++++++------ 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/app/Entities/Tools/PdfGenerator.php b/app/Entities/Tools/PdfGenerator.php index 4f23ad334ec..7c6dfaa6e8b 100644 --- a/app/Entities/Tools/PdfGenerator.php +++ b/app/Entities/Tools/PdfGenerator.php @@ -76,7 +76,7 @@ protected function renderUsingCommand(string $html): string $replacementsByPlaceholder = [ '{input_html_path}' => $inputHtml, - '{output_html_path}' => $outputPdf, + '{output_pdf_path}' => $outputPdf, ]; foreach ($replacementsByPlaceholder as $placeholder => $replacement) { diff --git a/tests/Entity/ExportTest.php b/tests/Entity/ExportTest.php index eedcb672c99..040f69013a5 100644 --- a/tests/Entity/ExportTest.php +++ b/tests/Entity/ExportTest.php @@ -6,8 +6,8 @@ use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Page; use BookStack\Entities\Tools\PdfGenerator; +use BookStack\Exceptions\PdfExportException; use Illuminate\Support\Facades\Storage; -use Illuminate\Support\Str; use Tests\TestCase; class ExportTest extends TestCase @@ -483,7 +483,7 @@ public function test_wkhtmltopdf_only_used_when_allow_untrusted_is_true() { $page = $this->entities->page(); - config()->set('snappy.pdf.binary', '/abc123'); + config()->set('exports.snappy.pdf_binary', '/abc123'); config()->set('app.allow_untrusted_server_fetching', false); $resp = $this->asEditor()->get($page->getUrl('/export/pdf')); @@ -494,6 +494,41 @@ public function test_wkhtmltopdf_only_used_when_allow_untrusted_is_true() $resp->assertStatus(500); // Bad response indicates wkhtml usage } + public function test_pdf_command_option_used_if_set() + { + $page = $this->entities->page(); + $command = 'cp {input_html_path} {output_pdf_path}'; + config()->set('exports.pdf_command', $command); + + $resp = $this->asEditor()->get($page->getUrl('/export/pdf')); + $download = $resp->getContent(); + + $this->assertStringContainsString(e($page->name), $download); + $this->assertStringContainsString('set('exports.pdf_command', $command); + + $this->assertThrows(function () use ($page) { + $this->withoutExceptionHandling()->asEditor()->get($page->getUrl('/export/pdf')); + }, PdfExportException::class); + } + + public function test_pdf_command_option_errors_if_command_returns_error_status() + { + $page = $this->entities->page(); + $command = 'exit 1'; + config()->set('exports.pdf_command', $command); + + $this->assertThrows(function () use ($page) { + $this->withoutExceptionHandling()->asEditor()->get($page->getUrl('/export/pdf')); + }, PdfExportException::class); + } + public function test_html_exports_contain_csp_meta_tag() { $entities = [ diff --git a/tests/Unit/ConfigTest.php b/tests/Unit/ConfigTest.php index aedcb75aa19..d5c74392ffc 100644 --- a/tests/Unit/ConfigTest.php +++ b/tests/Unit/ConfigTest.php @@ -80,22 +80,22 @@ public function test_saml2_idp_authn_context_string_parsed_as_space_separated_ar public function test_dompdf_remote_fetching_controlled_by_allow_untrusted_server_fetching_false() { - $this->checkEnvConfigResult('ALLOW_UNTRUSTED_SERVER_FETCHING', 'false', 'dompdf.options.enable_remote', false); - $this->checkEnvConfigResult('ALLOW_UNTRUSTED_SERVER_FETCHING', 'true', 'dompdf.options.enable_remote', true); + $this->checkEnvConfigResult('ALLOW_UNTRUSTED_SERVER_FETCHING', 'false', 'exports.dompdf.enable_remote', false); + $this->checkEnvConfigResult('ALLOW_UNTRUSTED_SERVER_FETCHING', 'true', 'exports.dompdf.enable_remote', true); } public function test_dompdf_paper_size_options_are_limited() { - $this->checkEnvConfigResult('EXPORT_PAGE_SIZE', 'cat', 'dompdf.options.default_paper_size', 'a4'); - $this->checkEnvConfigResult('EXPORT_PAGE_SIZE', 'letter', 'dompdf.options.default_paper_size', 'letter'); - $this->checkEnvConfigResult('EXPORT_PAGE_SIZE', 'a4', 'dompdf.options.default_paper_size', 'a4'); + $this->checkEnvConfigResult('EXPORT_PAGE_SIZE', 'cat', 'exports.dompdf.default_paper_size', 'a4'); + $this->checkEnvConfigResult('EXPORT_PAGE_SIZE', 'letter', 'exports.dompdf.default_paper_size', 'letter'); + $this->checkEnvConfigResult('EXPORT_PAGE_SIZE', 'a4', 'exports.dompdf.default_paper_size', 'a4'); } public function test_snappy_paper_size_options_are_limited() { - $this->checkEnvConfigResult('EXPORT_PAGE_SIZE', 'cat', 'snappy.pdf.options.page-size', 'A4'); - $this->checkEnvConfigResult('EXPORT_PAGE_SIZE', 'letter', 'snappy.pdf.options.page-size', 'Letter'); - $this->checkEnvConfigResult('EXPORT_PAGE_SIZE', 'a4', 'snappy.pdf.options.page-size', 'A4'); + $this->checkEnvConfigResult('EXPORT_PAGE_SIZE', 'cat', 'exports.snappy.options.page-size', 'A4'); + $this->checkEnvConfigResult('EXPORT_PAGE_SIZE', 'letter', 'exports.snappy.options.page-size', 'Letter'); + $this->checkEnvConfigResult('EXPORT_PAGE_SIZE', 'a4', 'exports.snappy.options.page-size', 'A4'); } public function test_sendmail_command_is_configurable()