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