170028127SAndreas Gohr<?php 270028127SAndreas Gohr 370028127SAndreas Gohrnamespace dokuwiki\plugin\dw2pdf\src; 470028127SAndreas Gohr 570028127SAndreas Gohruse Mpdf\MpdfException; 670028127SAndreas Gohr 770028127SAndreas Gohr/** 870028127SAndreas Gohr * Coordinates PDF generation and delivery 970028127SAndreas Gohr */ 1070028127SAndreas Gohrclass PdfExportService 1170028127SAndreas Gohr{ 1270028127SAndreas Gohr /** @var Config */ 1370028127SAndreas Gohr protected Config $config; 1470028127SAndreas Gohr 1570028127SAndreas Gohr /** @var AbstractCollector */ 1670028127SAndreas Gohr protected AbstractCollector $collector; 1770028127SAndreas Gohr 1870028127SAndreas Gohr /** @var Cache */ 1970028127SAndreas Gohr protected Cache $cache; 2070028127SAndreas Gohr 2170028127SAndreas Gohr /** @var string */ 2270028127SAndreas Gohr protected string $tocHeader; 2370028127SAndreas Gohr 2470028127SAndreas Gohr /** @var string */ 2570028127SAndreas Gohr protected string $remoteUser; 2670028127SAndreas Gohr 2770028127SAndreas Gohr /** 2870028127SAndreas Gohr * @param Config $config Active configuration controlling the export 2970028127SAndreas Gohr * @param AbstractCollector $collector Collector providing the export pages and metadata 3070028127SAndreas Gohr * @param Cache $cache Cache wrapper governing reuse and storage of the PDF file 3170028127SAndreas Gohr * @param string $tocHeader Localized header to display above the table of contents 3270028127SAndreas Gohr * @param string $remoteUser Authenticated user name for template context 3370028127SAndreas Gohr */ 3470028127SAndreas Gohr public function __construct( 3570028127SAndreas Gohr Config $config, 3670028127SAndreas Gohr AbstractCollector $collector, 3770028127SAndreas Gohr Cache $cache, 3870028127SAndreas Gohr string $tocHeader, 3970028127SAndreas Gohr string $remoteUser = '' 4070028127SAndreas Gohr ) { 4170028127SAndreas Gohr $this->config = $config; 4270028127SAndreas Gohr $this->collector = $collector; 4370028127SAndreas Gohr $this->cache = $cache; 4470028127SAndreas Gohr $this->tocHeader = $tocHeader; 4570028127SAndreas Gohr $this->remoteUser = $remoteUser; 4670028127SAndreas Gohr } 4770028127SAndreas Gohr 4870028127SAndreas Gohr /** 4970028127SAndreas Gohr * Build the PDF (or return cached version) and provide its filesystem path. 5070028127SAndreas Gohr * 5170028127SAndreas Gohr * @return string 5270028127SAndreas Gohr * @throws MpdfException 534c569113SAndreas Gohr * @throws ExportException When the collector yields no pages to export 5470028127SAndreas Gohr */ 5570028127SAndreas Gohr public function getPdf(): string 5670028127SAndreas Gohr { 57da4f9133SAnna Dabrowska if (!$this->config->useCache() || !$this->cache->useCache() || $this->config->isDebugEnabled()) { 5870028127SAndreas Gohr set_time_limit(0); 5970028127SAndreas Gohr $this->buildDocument($this->cache->cache); 6070028127SAndreas Gohr } 6170028127SAndreas Gohr 6270028127SAndreas Gohr return $this->cache->cache; 6370028127SAndreas Gohr } 6470028127SAndreas Gohr 6570028127SAndreas Gohr /** 6670028127SAndreas Gohr * Send the PDF to the browser. When $cacheFile is omitted, the PDF will be built (or loaded) first. 6770028127SAndreas Gohr * 6870028127SAndreas Gohr * @param string|null $cacheFile Absolute path to an already generated PDF file, if available 6970028127SAndreas Gohr * @return void 7070028127SAndreas Gohr * @throws MpdfException 714c569113SAndreas Gohr * @throws ExportException When the collector yields no pages to export 7270028127SAndreas Gohr */ 73f2000117SAndreas Gohr public function sendPdf(?string $cacheFile = null): void 7470028127SAndreas Gohr { 7570028127SAndreas Gohr $cacheFile ??= $this->getPdf(); 7670028127SAndreas Gohr $title = $this->collector->getTitle(); 7770028127SAndreas Gohr 7870028127SAndreas Gohr header('Content-Type: application/pdf'); 7970028127SAndreas Gohr header('Cache-Control: must-revalidate, no-transform, post-check=0, pre-check=0'); 8070028127SAndreas Gohr header('Pragma: public'); 8170028127SAndreas Gohr http_conditionalRequest(filemtime($cacheFile)); 8270028127SAndreas Gohr 8370028127SAndreas Gohr $outputTarget = $this->config->getOutputTarget(); 8470028127SAndreas Gohr $filename = rawurlencode(cleanID(strtr($title, ':/;"', ' '))); 8570028127SAndreas Gohr if ($outputTarget === 'file') { 8670028127SAndreas Gohr header('Content-Disposition: attachment; filename="' . $filename . '.pdf";'); 8770028127SAndreas Gohr } else { 8870028127SAndreas Gohr header('Content-Disposition: inline; filename="' . $filename . '.pdf";'); 8970028127SAndreas Gohr } 9070028127SAndreas Gohr 914c569113SAndreas Gohr // used by jQuery.fileDownload plugin used by bookcreator to detect when the download has started 9270028127SAndreas Gohr header('Set-Cookie: fileDownload=true; path=/'); 9370028127SAndreas Gohr 9470028127SAndreas Gohr http_sendfile($cacheFile); 9570028127SAndreas Gohr 9670028127SAndreas Gohr $fp = @fopen($cacheFile, 'rb'); 9770028127SAndreas Gohr if ($fp) { 9870028127SAndreas Gohr http_rangeRequest($fp, filesize($cacheFile), 'application/pdf'); 9970028127SAndreas Gohr } else { 10070028127SAndreas Gohr header('HTTP/1.0 500 Internal Server Error'); 10170028127SAndreas Gohr echo 'Could not read file - bad permissions?'; 10270028127SAndreas Gohr } 10370028127SAndreas Gohr exit(); 10470028127SAndreas Gohr } 10570028127SAndreas Gohr 10670028127SAndreas Gohr /** 10770028127SAndreas Gohr * Build the PDF document and write it to the cache file. 10870028127SAndreas Gohr * 10970028127SAndreas Gohr * @param string $cacheFile Destination path for the generated PDF file 11070028127SAndreas Gohr * @return void 11170028127SAndreas Gohr * @throws MpdfException 1124c569113SAndreas Gohr * @throws ExportException When the collector yields no pages to export 11370028127SAndreas Gohr */ 11470028127SAndreas Gohr protected function buildDocument(string $cacheFile): void 11570028127SAndreas Gohr { 116f2000117SAndreas Gohr $writer = $this->renderDocument(); 117f2000117SAndreas Gohr 118f2000117SAndreas Gohr if ($this->config->isDebugEnabled()) { 119f2000117SAndreas Gohr header('Content-Type: text/html; charset=utf-8'); 120f2000117SAndreas Gohr echo $writer->getDebugHTML(); 121f2000117SAndreas Gohr exit(); 122f2000117SAndreas Gohr } 123f2000117SAndreas Gohr 124f2000117SAndreas Gohr $writer->outputToFile($cacheFile); 125f2000117SAndreas Gohr } 126f2000117SAndreas Gohr 127f2000117SAndreas Gohr /** 128f2000117SAndreas Gohr * Build the PDF document without writing it to disk and expose the debug HTML. 129f2000117SAndreas Gohr * 130f2000117SAndreas Gohr * @return string Debug HTML collected while rendering the document 131f2000117SAndreas Gohr * @throws MpdfException 1324c569113SAndreas Gohr * @throws ExportException When the collector yields no pages to export 133f2000117SAndreas Gohr */ 134f2000117SAndreas Gohr public function getDebugHtml(): string 135f2000117SAndreas Gohr { 136f2000117SAndreas Gohr if (!$this->config->isDebugEnabled()) { 137f2000117SAndreas Gohr throw new \RuntimeException('Debug HTML is only available when debug mode is enabled'); 138f2000117SAndreas Gohr } 139f2000117SAndreas Gohr 140f2000117SAndreas Gohr return $this->renderDocument()->getDebugHTML(); 141f2000117SAndreas Gohr } 142f2000117SAndreas Gohr 143f2000117SAndreas Gohr /** 144f2000117SAndreas Gohr * Compose the document using the collector and return the writer. 145f2000117SAndreas Gohr * 146f2000117SAndreas Gohr * @return Writer Writer instance containing the rendered document 147f2000117SAndreas Gohr * @throws MpdfException 1484c569113SAndreas Gohr * @throws ExportException When the collector yields no pages to export 149f2000117SAndreas Gohr */ 150f2000117SAndreas Gohr protected function renderDocument(): Writer 151f2000117SAndreas Gohr { 1521a52a777SAnna Dabrowska $mpdf = new DokuMpdf($this->config, $this->collector->getLanguage()); 15370028127SAndreas Gohr $styles = new Styles($this->config); 15470028127SAndreas Gohr $template = new Template($this->config); 155f2000117SAndreas Gohr $writer = new Writer($mpdf, $this->config, $template, $styles); 15670028127SAndreas Gohr 1574c569113SAndreas Gohr $pages = $this->collector->getPages(); 1584c569113SAndreas Gohr if ($pages === []) { 159*40a07da2SAndreas Gohr // tell an empty selection apart from one where every page was forbidden, without 160*40a07da2SAndreas Gohr // exposing anything about the skipped pages 161*40a07da2SAndreas Gohr throw $this->collector->getSkippedPages() === [] 162*40a07da2SAndreas Gohr ? new ExportException('empty') 163*40a07da2SAndreas Gohr : new ExportException('forbidden'); 1644c569113SAndreas Gohr } 1654c569113SAndreas Gohr 16670028127SAndreas Gohr $writer->startDocument($this->collector->getTitle()); 1673ef754f4SAnna Dabrowska // initial context for placeholder replacements, before any pages are loaded 1684c569113SAndreas Gohr $template->setContext($this->collector, $pages[0], null); 16970028127SAndreas Gohr $writer->cover(); 17070028127SAndreas Gohr 17170028127SAndreas Gohr if ($this->config->hasToC()) { 17270028127SAndreas Gohr $writer->toc($this->tocHeader); 17370028127SAndreas Gohr } 17470028127SAndreas Gohr 17570028127SAndreas Gohr foreach ($this->collector->getPages() as $page) { 17670028127SAndreas Gohr $template->setContext($this->collector, $page, $this->remoteUser); 17770028127SAndreas Gohr $writer->renderWikiPage($this->collector, $page); 17870028127SAndreas Gohr } 17970028127SAndreas Gohr 18070028127SAndreas Gohr $writer->back(); 18170028127SAndreas Gohr $writer->endDocument(); 182f2000117SAndreas Gohr return $writer; 18370028127SAndreas Gohr } 18470028127SAndreas Gohr} 185