| 
									
										
										
										
											2021-11-25 23:12:32 +08:00
										 |  |  | <?php | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-15 23:14:11 +08:00
										 |  |  | namespace BookStack\Exports; | 
					
						
							| 
									
										
										
										
											2021-11-25 23:12:32 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-24 23:09:53 +08:00
										 |  |  | use BookStack\Exceptions\PdfExportException; | 
					
						
							| 
									
										
										
										
											2024-04-22 23:40:42 +08:00
										 |  |  | use Dompdf\Dompdf; | 
					
						
							| 
									
										
										
										
											2024-10-15 23:14:11 +08:00
										 |  |  | use Knp\Snappy\Pdf as SnappyPdf; | 
					
						
							| 
									
										
										
										
											2024-09-27 23:33:58 +08:00
										 |  |  | use Symfony\Component\Process\Exception\ProcessTimedOutException; | 
					
						
							| 
									
										
										
										
											2024-04-24 23:09:53 +08:00
										 |  |  | use Symfony\Component\Process\Process; | 
					
						
							| 
									
										
										
										
											2021-11-25 23:12:32 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | class PdfGenerator | 
					
						
							|  |  |  | { | 
					
						
							| 
									
										
										
										
											2022-01-25 01:24:00 +08:00
										 |  |  |     const ENGINE_DOMPDF = 'dompdf'; | 
					
						
							|  |  |  |     const ENGINE_WKHTML = 'wkhtml'; | 
					
						
							| 
									
										
										
										
											2024-04-22 23:40:42 +08:00
										 |  |  |     const ENGINE_COMMAND = 'command'; | 
					
						
							| 
									
										
										
										
											2022-01-25 01:24:00 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-25 23:12:32 +08:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * Generate PDF content from the given HTML content. | 
					
						
							| 
									
										
										
										
											2024-04-24 23:09:53 +08:00
										 |  |  |      * @throws PdfExportException | 
					
						
							| 
									
										
										
										
											2021-11-25 23:12:32 +08:00
										 |  |  |      */ | 
					
						
							|  |  |  |     public function fromHtml(string $html): string | 
					
						
							|  |  |  |     { | 
					
						
							| 
									
										
										
										
											2024-04-24 23:09:53 +08:00
										 |  |  |         return match ($this->getActiveEngine()) { | 
					
						
							|  |  |  |             self::ENGINE_COMMAND => $this->renderUsingCommand($html), | 
					
						
							|  |  |  |             self::ENGINE_WKHTML => $this->renderUsingWkhtml($html), | 
					
						
							|  |  |  |             default => $this->renderUsingDomPdf($html) | 
					
						
							|  |  |  |         }; | 
					
						
							| 
									
										
										
										
											2021-11-25 23:12:32 +08:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2022-01-25 01:24:00 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Get the currently active PDF engine. | 
					
						
							|  |  |  |      * Returns the value of an `ENGINE_` const on this class. | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     public function getActiveEngine(): string | 
					
						
							|  |  |  |     { | 
					
						
							| 
									
										
										
										
											2024-04-24 23:09:53 +08:00
										 |  |  |         if (config('exports.pdf_command')) { | 
					
						
							|  |  |  |             return self::ENGINE_COMMAND; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-24 22:13:44 +08:00
										 |  |  |         if ($this->getWkhtmlBinaryPath() && config('app.allow_untrusted_server_fetching') === true) { | 
					
						
							| 
									
										
										
										
											2024-04-22 23:40:42 +08:00
										 |  |  |             return self::ENGINE_WKHTML; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return self::ENGINE_DOMPDF; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-24 22:13:44 +08:00
										 |  |  |     protected function getWkhtmlBinaryPath(): string | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $wkhtmlBinaryPath = config('exports.snappy.pdf_binary'); | 
					
						
							|  |  |  |         if (file_exists(base_path('wkhtmltopdf'))) { | 
					
						
							|  |  |  |             $wkhtmlBinaryPath = base_path('wkhtmltopdf'); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return $wkhtmlBinaryPath ?: ''; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-22 23:40:42 +08:00
										 |  |  |     protected function renderUsingDomPdf(string $html): string | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $options = config('exports.dompdf'); | 
					
						
							|  |  |  |         $domPdf = new Dompdf($options); | 
					
						
							|  |  |  |         $domPdf->setBasePath(base_path('public')); | 
					
						
							| 
									
										
										
										
											2022-01-25 04:55:03 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-22 23:40:42 +08:00
										 |  |  |         $domPdf->loadHTML($this->convertEntities($html)); | 
					
						
							|  |  |  |         $domPdf->render(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return (string) $domPdf->output(); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-24 23:09:53 +08:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * @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, | 
					
						
							| 
									
										
										
										
											2024-04-26 22:39:40 +08:00
										 |  |  |             '{output_pdf_path}' => $outputPdf, | 
					
						
							| 
									
										
										
										
											2024-04-24 23:09:53 +08:00
										 |  |  |         ]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         foreach ($replacementsByPlaceholder as $placeholder => $replacement) { | 
					
						
							|  |  |  |             $command = str_replace($placeholder, escapeshellarg($replacement), $command); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         file_put_contents($inputHtml, $html); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-27 23:33:58 +08:00
										 |  |  |         $timeout = intval(config('exports.pdf_command_timeout')); | 
					
						
							| 
									
										
										
										
											2024-04-24 23:09:53 +08:00
										 |  |  |         $process = Process::fromShellCommandline($command); | 
					
						
							| 
									
										
										
										
											2024-09-27 23:33:58 +08:00
										 |  |  |         $process->setTimeout($timeout); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-01 23:19:11 +08:00
										 |  |  |         $cleanup = function () use ($inputHtml, $outputPdf) { | 
					
						
							|  |  |  |             foreach ([$inputHtml, $outputPdf] as $file) { | 
					
						
							|  |  |  |                 if (file_exists($file)) { | 
					
						
							|  |  |  |                     unlink($file); | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-27 23:33:58 +08:00
										 |  |  |         try { | 
					
						
							|  |  |  |             $process->run(); | 
					
						
							|  |  |  |         } catch (ProcessTimedOutException $e) { | 
					
						
							| 
									
										
										
										
											2025-01-01 23:19:11 +08:00
										 |  |  |             $cleanup(); | 
					
						
							| 
									
										
										
										
											2024-09-27 23:33:58 +08:00
										 |  |  |             throw new PdfExportException("PDF Export via command failed due to timeout at {$timeout} second(s)"); | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2024-04-24 23:09:53 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |         if (!$process->isSuccessful()) { | 
					
						
							| 
									
										
										
										
											2025-01-01 23:19:11 +08:00
										 |  |  |             $cleanup(); | 
					
						
							| 
									
										
										
										
											2024-04-24 23:09:53 +08:00
										 |  |  |             throw new PdfExportException("PDF Export via command failed with exit code {$process->getExitCode()}, stdout: {$process->getOutput()}, stderr: {$process->getErrorOutput()}"); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $pdfContents = file_get_contents($outputPdf); | 
					
						
							| 
									
										
										
										
											2025-01-01 23:19:11 +08:00
										 |  |  |         $cleanup(); | 
					
						
							| 
									
										
										
										
											2024-04-24 23:09:53 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |         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; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-24 22:13:44 +08:00
										 |  |  |     protected function renderUsingWkhtml(string $html): string | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $snappy = new SnappyPdf($this->getWkhtmlBinaryPath()); | 
					
						
							|  |  |  |         $options = config('exports.snappy.options'); | 
					
						
							|  |  |  |         return $snappy->getOutputFromHtml($html, $options); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-22 23:40:42 +08:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * 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; | 
					
						
							| 
									
										
										
										
											2022-01-25 01:24:00 +08:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-11-29 05:01:35 +08:00
										 |  |  | } |