xref: /plugin/dw2pdf/src/PdfExportService.php (revision 40a07da2a0a1fe205d2fb62c8cd8b8968e580efa)
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