diff --git a/.env.example.complete b/.env.example.complete index 1a4f421f0..6b4936648 100644 --- a/.env.example.complete +++ b/.env.example.complete @@ -334,6 +334,11 @@ EXPORT_PAGE_SIZE=a4 # Example: EXPORT_PDF_COMMAND="/scripts/convert.sh {input_html_path} {output_pdf_path}" EXPORT_PDF_COMMAND=false +# Export PDF Command Timeout +# The number of seconds that the export PDF command will run before a timeout occurs. +# Only applies for the EXPORT_PDF_COMMAND option, not for DomPDF or wkhtmltopdf. +EXPORT_PDF_COMMAND_TIMEOUT=15 + # 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/exports.php b/app/Config/exports.php index 88dc08cba..ba535ca7e 100644 --- a/app/Config/exports.php +++ b/app/Config/exports.php @@ -29,6 +29,10 @@ return [ // Example: EXPORT_PDF_COMMAND="/scripts/convert.sh {input_html_path} {output_pdf_path}" 'pdf_command' => env('EXPORT_PDF_COMMAND', false), + // The amount of time allowed for PDF generation command to run + // before the process times out and is stopped. + 'pdf_command_timeout' => env('EXPORT_PDF_COMMAND_TIMEOUT', 15), + // 2024-04: Snappy/WKHTMLtoPDF now considered deprecated in regard to BookStack support. 'snappy' => [ 'pdf_binary' => env('WKHTMLTOPDF', false), diff --git a/app/Entities/Tools/PdfGenerator.php b/app/Entities/Tools/PdfGenerator.php index 7c6dfaa6e..79cd1b02f 100644 --- a/app/Entities/Tools/PdfGenerator.php +++ b/app/Entities/Tools/PdfGenerator.php @@ -5,6 +5,7 @@ namespace BookStack\Entities\Tools; 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 @@ -85,9 +86,15 @@ class PdfGenerator file_put_contents($inputHtml, $html); + $timeout = intval(config('exports.pdf_command_timeout')); $process = Process::fromShellCommandline($command); - $process->setTimeout(15); - $process->run(); + $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()}"); diff --git a/tests/Entity/ExportTest.php b/tests/Entity/ExportTest.php index 040f69013..7aafa3b79 100644 --- a/tests/Entity/ExportTest.php +++ b/tests/Entity/ExportTest.php @@ -529,6 +529,22 @@ class ExportTest extends TestCase }, PdfExportException::class); } + public function test_pdf_command_timout_option_limits_export_time() + { + $page = $this->entities->page(); + $command = 'php -r \'sleep(4);\''; + config()->set('exports.pdf_command', $command); + config()->set('exports.pdf_command_timeout', 1); + + $this->assertThrows(function () use ($page) { + $start = time(); + $this->withoutExceptionHandling()->asEditor()->get($page->getUrl('/export/pdf')); + + $this->assertTrue(time() < ($start + 3)); + }, PdfExportException::class, + "PDF Export via command failed due to timeout at 1 second(s)"); + } + public function test_html_exports_contain_csp_meta_tag() { $entities = [