xref: /plugin/dw2pdf/src/Config.php (revision a9cd524ed3465a8385c4a367213f90bb9717f5b7)
1<?php
2
3namespace dokuwiki\plugin\dw2pdf\src;
4
5use dokuwiki\plugin\dw2pdf\src\attributes\FromConfig;
6use dokuwiki\plugin\dw2pdf\src\attributes\FromInput;
7
8class Config
9{
10    protected string $tempDir = '';
11
12    // General PDF configuration
13    #[FromConfig, FromInput]
14    protected string $pagesize = 'A4';
15    #[FromConfig('orientation'), FromInput('orientation')]
16    protected bool $isLandscape = false;
17    #[FromConfig('font-size'), FromInput('font-size')]
18    protected int $fontSize = 11;
19    #[FromConfig('doublesided'), FromInput('doublesided')]
20    protected bool $isDoublesided = false;
21    #[FromConfig('toc'), FromInput('toc')]
22    protected bool $hasToC = false;
23    #[FromConfig, FromInput('toclevels')]
24    protected array $tocLevels = [];
25    #[FromConfig]
26    protected int $maxBookmarks = 5;
27    #[FromConfig('headernumber')]
28    protected bool $numberedHeaders = false;
29    #[FromConfig, FromInput]
30    protected string $watermark = '';
31    #[FromConfig, FromInput('tpl')]
32    protected string $template = 'default';
33    #[FromConfig('debug'), FromInput('debug')]
34    protected bool $isDebug = false;
35    #[FromConfig]
36    protected bool $usecache;
37    #[FromConfig]
38    protected array $useStyles = [];
39    #[FromConfig]
40    protected float $qrCodeScale = 0.0;
41    #[FromConfig('output'), FromInput('outputTarget')]
42    protected string $outputTarget = 'file';
43
44    // Collector-specific request data
45    #[FromConfig, FromInput('book_title')]
46    protected ?string $bookTitle = null;
47    #[FromConfig, FromInput('book_ns')]
48    protected string $bookNamespace = '';
49    #[FromConfig, FromInput('book_order')]
50    protected string $bookSortOrder = 'natural';
51    #[FromConfig, FromInput('book_nsdepth')]
52    protected int $bookNamespaceDepth = 0;
53    #[FromConfig, FromInput('excludes')]
54    protected array $bookExcludePages = [];
55    #[FromConfig, FromInput('excludesns')]
56    protected array $bookExcludeNamespaces = [];
57    #[FromConfig, FromInput('selection')]
58    protected ?string $liveSelection = null;
59    #[FromConfig, FromInput('savedselection')]
60    protected ?string $savedSelection = null;
61    #[FromConfig] // also read from global $ID
62    protected string $exportId = '';
63
64    /**
65     * @param array $pluginConf Plugin configuration
66     */
67    public function __construct(array $pluginConf = [])
68    {
69        global $conf;
70        $this->tempDir = $conf['tmpdir'] . '/mpdf';
71        io_mkdir_p($this->tempDir);
72
73        // set default ToC levels from main config
74        $this->tocLevels = $this->parseTocLevels($conf['toptoclevel'] . '-' . $conf['maxtoclevel']);
75
76        $this->loadPluginConfig($pluginConf);
77        $this->loadInputConfig();
78    }
79
80    /**
81     * Set a property with type casting and custom parsing
82     *
83     * @param string $prop The property name to set
84     * @param string|null $type The property type
85     * @param mixed $value The value to set
86     * @return void
87     * @see loadPluginConfig
88     * @see loadInputConfig
89     */
90    protected function setProperty(string $prop, ?string $type, $value)
91    {
92        // custom value parsing (lowercased property names to avoid mistakes)
93        $value = match (strtolower($prop)) {
94            'islandscape' => ($value === 'landscape'),
95            'toclevels' => is_array($value) ? $value : $this->parseTocLevels((string)$value),
96            'exportid' => cleanID((string)$value),
97            default => $value,
98        };
99
100        // standard type casting
101        $this->$prop = match ($type) {
102            'int' => (int)$value,
103            'bool' => (bool)$value,
104            'float' => (float)$value,
105            'array' => is_array($value)
106                ? $value
107                : array_filter(array_map('trim', explode(',', (string)$value))),
108            default => $value,
109        };
110    }
111
112    /**
113     * Apply the given configuration
114     *
115     * This will set all properties annotated with FromConfig
116     *
117     * @param array $conf (Plugin) configuration
118     */
119    public function loadPluginConfig(array $conf = [])
120    {
121        $reflection = new \ReflectionClass($this);
122        foreach ($reflection->getProperties() as $property) {
123            $attributes = $property->getAttributes(FromConfig::class);
124            if ($attributes === []) continue;
125            $attribute = $attributes[0]->newInstance(); // we only expect one
126
127            $prop = $property->getName();
128            $confName = $attribute->name ?? strtolower($prop);
129            $type = $property->getType()?->getName();
130
131            if (!isset($conf[$confName])) continue;
132            $this->setProperty($prop, $type, $conf[$confName]);
133        }
134    }
135
136    /**
137     * Load configuration provided by INPUT parameters
138     *
139     * Not all parameters are overridable here
140     *
141     * @return void
142     */
143    public function loadInputConfig()
144    {
145        global $INPUT, $ID, $conf;
146
147        if (!blank($ID)) $this->exportId = $ID; // default exportId to current page ID
148
149        $reflection = new \ReflectionClass($this);
150        foreach ($reflection->getProperties() as $property) {
151            $attributes = $property->getAttributes(FromInput::class);
152            if ($attributes === []) continue;
153            $attribute = $attributes[0]->newInstance(); // we only expect one
154
155            $prop = $property->getName();
156            $confName = $attribute->name ?? strtolower($prop);
157            $type = $property->getType()?->getName();
158
159            if (!$INPUT->has($confName)) continue;
160
161            // debug output exposes the raw mPDF input and bypasses the cache, so it may only be
162            // requested via URL when core debugging is enabled
163            if ($prop === 'isDebug' && empty($conf['allowdebug'])) continue;
164
165            $this->setProperty($prop, $type, $INPUT->param($confName));
166        }
167    }
168
169    /**
170     * Check whether ToC is enabled
171     *
172     * @return bool
173     */
174    public function hasToc(): bool
175    {
176        return $this->hasToC;
177    }
178
179    /**
180     * Check whether debug mode is enabled
181     *
182     * @return bool
183     */
184    public function isDebugEnabled(): bool
185    {
186        return $this->isDebug;
187    }
188
189    /**
190     * Check if caching is enabled
191     *
192     * @return bool
193     */
194    public function useCache(): bool
195    {
196        return $this->usecache;
197    }
198
199    /**
200     * Get a list of extensions whose screen styles should be applied
201     *
202     * @return string[]
203     */
204    public function getStyledExtensions(): array
205    {
206        return $this->useStyles;
207    }
208
209    /**
210     * Get the name of the selected template
211     *
212     * @return string
213     */
214    public function getTemplateName(): string
215    {
216        return $this->template;
217    }
218
219    /**
220     * Get the maximum number of bookmarks to include
221     *
222     * @return int
223     */
224    public function getMaxBookmarks(): int
225    {
226        return $this->maxBookmarks;
227    }
228
229    /**
230     * Check whether numbered headers are to be used
231     *
232     * @return bool
233     */
234    public function useNumberedHeaders(): bool
235    {
236        return $this->numberedHeaders;
237    }
238
239    /**
240     * Get the QR code scale
241     *
242     * @return float
243     */
244    public function getQRScale(): float
245    {
246        return $this->qrCodeScale;
247    }
248
249    /**
250     * Get desired PDF delivery target (inline or file download)
251     *
252     * @return string
253     */
254    public function getOutputTarget(): string
255    {
256        return $this->outputTarget;
257    }
258
259    /**
260     * Get a unique cache key for the current configuration
261     *
262     * @return string
263     */
264    public function getCacheKey(): string
265    {
266        return implode(',', [
267            $this->template,
268            $this->pagesize,
269            $this->isLandscape ? 'L' : 'P',
270            $this->fontSize,
271            $this->isDoublesided ? 'D' : 'S',
272            $this->hasToC ? 'T' : 'N',
273            $this->maxBookmarks,
274            $this->numberedHeaders ? 'H' : 'N',
275            implode('-', $this->tocLevels)
276        ]);
277    }
278
279    /**
280     * Parses the ToC levels configuration into an array
281     *
282     * @param string $toclevels eg. "2-4"
283     * @return array
284     */
285    protected function parseTocLevels(string $toclevels): array
286    {
287        $levels = [];
288        [$top, $max] = sexplode('-', $toclevels, 2);
289        $top = max(1, min(5, (int)$top));
290        $max = max(1, min(5, (int)$max));
291
292        if ($max < $top) {
293            $max = $top;
294        }
295
296        for ($level = $top; $level <= $max; $level++) {
297            $levels["H$level"] = $level - 1;
298        }
299
300        return $levels;
301    }
302
303
304    /**
305     * Return the paper format
306     *
307     * @return string
308     */
309    public function getFormat()
310    {
311        $format = $this->pagesize;
312        if ($this->isLandscape) {
313            $format .= '-L';
314        }
315        return $format;
316    }
317
318    /**
319     * Return the watermark text if any
320     *
321     * @return string
322     */
323    public function getWatermarkText(): string
324    {
325        return $this->watermark;
326    }
327
328    /**
329     * Get all configuration for mpdf as array
330     *
331     * Note: mode and writing direction are set in DokuPDF based on the language
332     *
333     * @link https://mpdf.github.io/reference/mpdf-variables/overview.html
334     * @return array
335     */
336    public function getMPdfConfig(): array
337    {
338        return [
339            'format' => $this->getFormat(),
340            'default_font_size' => $this->fontSize,
341            'tempDir' => $this->tempDir,
342            'mirrorMargins' => $this->isDoublesided,
343            'h2toc' => $this->hasToC ? $this->tocLevels : [],
344            'showWatermarkText' => $this->watermark !== '',
345
346            'setAutoTopMargin' => 'stretch',
347            'setAutoBottomMargin' => 'stretch',
348            'autoScriptToLang' => true,
349            'baseScript' => 1,
350            'autoVietnamese' => true,
351            'autoArabic' => true,
352            'autoLangToFont' => true,
353            'ignore_invalid_utf8' => true,
354            'tabSpaces' => 4,
355        ];
356    }
357
358    /**
359     * Get the requested book title override if provided.
360     *
361     * @return string|null
362     */
363    public function getBookTitle(): ?string
364    {
365        return $this->bookTitle;
366    }
367
368    /**
369     * Get the namespace to export for namespace/book selections.
370     *
371     * @return string
372     */
373    public function getBookNamespace(): string
374    {
375        return $this->bookNamespace;
376    }
377
378    /**
379     * Get the page sort order selected for namespace exports.
380     *
381     * @return string
382     */
383    public function getBookSortOrder(): string
384    {
385        return $this->bookSortOrder;
386    }
387
388    /**
389     * Get the maximum namespace depth to traverse for namespace exports.
390     *
391     * @return int
392     */
393    public function getBookNamespaceDepth(): int
394    {
395        return $this->bookNamespaceDepth;
396    }
397
398    /**
399     * Get the list of explicitly excluded page IDs.
400     *
401     * @return string[]
402     */
403    public function getBookExcludedPages(): array
404    {
405        return $this->bookExcludePages;
406    }
407
408    /**
409     * Get the list of excluded namespaces.
410     *
411     * @return string[]
412     */
413    public function getBookExcludedNamespaces(): array
414    {
415        return $this->bookExcludeNamespaces;
416    }
417
418    /**
419     * Get the raw JSON payload representing a live book selection.
420     *
421     * @return string|null
422     */
423    public function getLiveSelection(): ?string
424    {
425        return $this->liveSelection;
426    }
427
428    /**
429     * Check whether a live selection payload was supplied.
430     *
431     * @return bool
432     */
433    public function hasLiveSelection(): bool
434    {
435        return $this->liveSelection !== null;
436    }
437
438    /**
439     * Get the identifier of a saved book selection.
440     *
441     * @return string|null
442     */
443    public function getSavedSelection(): ?string
444    {
445        return $this->savedSelection;
446    }
447
448    /**
449     * Check whether a saved selection identifier was supplied.
450     *
451     * @return bool
452     */
453    public function hasSavedSelection(): bool
454    {
455        return $this->savedSelection !== null;
456    }
457
458    /**
459     * Get the requested page ID for single page exports.
460     *
461     * @return string
462     */
463    public function getExportId(): string
464    {
465        return $this->exportId;
466    }
467}
468