1<?php 2 3namespace dokuwiki\plugin\dw2pdf\src; 4 5use Mpdf\MpdfException; 6 7/** 8 * Coordinates PDF generation and delivery 9 */ 10class PdfExportService 11{ 12 /** @var Config */ 13 protected Config $config; 14 15 /** @var AbstractCollector */ 16 protected AbstractCollector $collector; 17 18 /** @var Cache */ 19 protected Cache $cache; 20 21 /** @var string */ 22 protected string $tocHeader; 23 24 /** @var string */ 25 protected string $remoteUser; 26 27 /** 28 * @param Config $config Active configuration controlling the export 29 * @param AbstractCollector $collector Collector providing the export pages and metadata 30 * @param Cache $cache Cache wrapper governing reuse and storage of the PDF file 31 * @param string $tocHeader Localized header to display above the table of contents 32 * @param string $remoteUser Authenticated user name for template context 33 */ 34 public function __construct( 35 Config $config, 36 AbstractCollector $collector, 37 Cache $cache, 38 string $tocHeader, 39 string $remoteUser = '' 40 ) { 41 $this->config = $config; 42 $this->collector = $collector; 43 $this->cache = $cache; 44 $this->tocHeader = $tocHeader; 45 $this->remoteUser = $remoteUser; 46 } 47 48 /** 49 * Build the PDF (or return cached version) and provide its filesystem path. 50 * 51 * @return string 52 * @throws MpdfException 53 * @throws ExportException When the collector yields no pages to export 54 */ 55 public function getPdf(): string 56 { 57 if (!$this->config->useCache() || !$this->cache->useCache() || $this->config->isDebugEnabled()) { 58 set_time_limit(0); 59 $this->buildDocument($this->cache->cache); 60 } 61 62 return $this->cache->cache; 63 } 64 65 /** 66 * Send the PDF to the browser. When $cacheFile is omitted, the PDF will be built (or loaded) first. 67 * 68 * @param string|null $cacheFile Absolute path to an already generated PDF file, if available 69 * @return void 70 * @throws MpdfException 71 * @throws ExportException When the collector yields no pages to export 72 */ 73 public function sendPdf(?string $cacheFile = null): void 74 { 75 $cacheFile ??= $this->getPdf(); 76 $title = $this->collector->getTitle(); 77 78 header('Content-Type: application/pdf'); 79 header('Cache-Control: must-revalidate, no-transform, post-check=0, pre-check=0'); 80 header('Pragma: public'); 81 http_conditionalRequest(filemtime($cacheFile)); 82 83 $outputTarget = $this->config->getOutputTarget(); 84 $filename = rawurlencode(cleanID(strtr($title, ':/;"', ' '))); 85 if ($outputTarget === 'file') { 86 header('Content-Disposition: attachment; filename="' . $filename . '.pdf";'); 87 } else { 88 header('Content-Disposition: inline; filename="' . $filename . '.pdf";'); 89 } 90 91 // used by jQuery.fileDownload plugin used by bookcreator to detect when the download has started 92 header('Set-Cookie: fileDownload=true; path=/'); 93 94 http_sendfile($cacheFile); 95 96 $fp = @fopen($cacheFile, 'rb'); 97 if ($fp) { 98 http_rangeRequest($fp, filesize($cacheFile), 'application/pdf'); 99 } else { 100 header('HTTP/1.0 500 Internal Server Error'); 101 echo 'Could not read file - bad permissions?'; 102 } 103 exit(); 104 } 105 106 /** 107 * Build the PDF document and write it to the cache file. 108 * 109 * @param string $cacheFile Destination path for the generated PDF file 110 * @return void 111 * @throws MpdfException 112 * @throws ExportException When the collector yields no pages to export 113 */ 114 protected function buildDocument(string $cacheFile): void 115 { 116 $writer = $this->renderDocument(); 117 118 if ($this->config->isDebugEnabled()) { 119 header('Content-Type: text/html; charset=utf-8'); 120 echo $writer->getDebugHTML(); 121 exit(); 122 } 123 124 $writer->outputToFile($cacheFile); 125 } 126 127 /** 128 * Build the PDF document without writing it to disk and expose the debug HTML. 129 * 130 * @return string Debug HTML collected while rendering the document 131 * @throws MpdfException 132 * @throws ExportException When the collector yields no pages to export 133 */ 134 public function getDebugHtml(): string 135 { 136 if (!$this->config->isDebugEnabled()) { 137 throw new \RuntimeException('Debug HTML is only available when debug mode is enabled'); 138 } 139 140 return $this->renderDocument()->getDebugHTML(); 141 } 142 143 /** 144 * Compose the document using the collector and return the writer. 145 * 146 * @return Writer Writer instance containing the rendered document 147 * @throws MpdfException 148 * @throws ExportException When the collector yields no pages to export 149 */ 150 protected function renderDocument(): Writer 151 { 152 $mpdf = new DokuMpdf($this->config, $this->collector->getLanguage()); 153 $styles = new Styles($this->config); 154 $template = new Template($this->config); 155 $writer = new Writer($mpdf, $this->config, $template, $styles); 156 157 $pages = $this->collector->getPages(); 158 if ($pages === []) { 159 // tell an empty selection apart from one where every page was forbidden, without 160 // exposing anything about the skipped pages 161 throw $this->collector->getSkippedPages() === [] 162 ? new ExportException('empty') 163 : new ExportException('forbidden'); 164 } 165 166 $writer->startDocument($this->collector->getTitle()); 167 // initial context for placeholder replacements, before any pages are loaded 168 $template->setContext($this->collector, $pages[0], null); 169 $writer->cover(); 170 171 if ($this->config->hasToC()) { 172 $writer->toc($this->tocHeader); 173 } 174 175 foreach ($this->collector->getPages() as $page) { 176 $template->setContext($this->collector, $page, $this->remoteUser); 177 $writer->renderWikiPage($this->collector, $page); 178 } 179 180 $writer->back(); 181 $writer->endDocument(); 182 return $writer; 183 } 184} 185