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