1<?php 2 3use dokuwiki\Cache\Cache; 4use dokuwiki\Extension\ActionPlugin; 5use dokuwiki\Extension\Event; 6use dokuwiki\Extension\EventHandler; 7use dokuwiki\plugin\dw2pdf\MenuItem; 8use dokuwiki\StyleUtils; 9use Mpdf\MpdfException; 10 11/** 12 * dw2Pdf Plugin: Conversion from dokuwiki content to pdf. 13 * 14 * Export html content to pdf, for different url parameter configurations 15 * DokuPDF which extends mPDF is used for generating the pdf from html. 16 * 17 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 18 * @author Luigi Micco <l.micco@tiscali.it> 19 * @author Andreas Gohr <andi@splitbrain.org> 20 */ 21class action_plugin_dw2pdf extends ActionPlugin 22{ 23 /** 24 * Settings for current export, collected from url param, plugin config, global config 25 * 26 * @var array 27 */ 28 protected $exportConfig; 29 /** @var string template name, to use templates from dw2pdf/tpl/<template name> */ 30 protected $tpl; 31 /** @var string title of exported pdf */ 32 protected $title; 33 /** @var array list of pages included in exported pdf */ 34 protected $list = []; 35 /** @var bool|string path to temporary cachefile */ 36 protected $onetimefile = false; 37 protected $currentBookChapter = 0; 38 39 /** 40 * Constructor. Sets the correct template 41 */ 42 public function __construct() 43 { 44 $this->tpl = $this->getExportConfig('template'); 45 } 46 47 /** 48 * Delete cached files that were for one-time use 49 */ 50 public function __destruct() 51 { 52 if ($this->onetimefile) { 53 unlink($this->onetimefile); 54 } 55 } 56 57 /** 58 * Return the value of currentBookChapter, which is the order of the file to be added in a book generation 59 */ 60 public function getCurrentBookChapter() 61 { 62 return $this->currentBookChapter; 63 } 64 65 /** 66 * Register the events 67 * 68 * @param Doku_Event_Handler $controller 69 */ 70 public function register(EventHandler $controller) 71 { 72 $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'convert'); 73 $controller->register_hook('TEMPLATE_PAGETOOLS_DISPLAY', 'BEFORE', $this, 'addbutton'); 74 $controller->register_hook('MENU_ITEMS_ASSEMBLY', 'AFTER', $this, 'addsvgbutton'); 75 } 76 77 /** 78 * Do the HTML to PDF conversion work 79 * 80 * @param Doku_Event $event 81 */ 82 public function convert(Event $event) 83 { 84 global $REV, $DATE_AT; 85 global $conf, $INPUT; 86 87 // our event? 88 $allowedEvents = ['export_pdfbook', 'export_pdf', 'export_pdfns']; 89 if (!in_array($event->data, $allowedEvents)) return; 90 91 try { 92 //collect pages and check permissions 93 [$this->title, $this->list] = $this->collectExportablePages($event); 94 95 if ($event->data === 'export_pdf' && ($REV || $DATE_AT)) { 96 $cachefile = tempnam($conf['tmpdir'] . '/dwpdf', 'dw2pdf_'); 97 $this->onetimefile = $cachefile; 98 $generateNewPdf = true; 99 } else { 100 // prepare cache and its dependencies 101 $depends = []; 102 $cache = $this->prepareCache($depends); 103 $cachefile = $cache->cache; 104 $generateNewPdf = !$this->getConf('usecache') 105 || $this->getExportConfig('isDebug') 106 || !$cache->useCache($depends); 107 } 108 109 // hard work only when no cache available or needed for debugging 110 if ($generateNewPdf) { 111 // generating the pdf may take a long time for larger wikis / namespaces with many pages 112 set_time_limit(0); 113 //may throw Mpdf\MpdfException as well 114 $this->generatePDF($cachefile, $event); 115 } 116 } catch (Exception $e) { 117 if ($INPUT->has('selection')) { 118 http_status(400); 119 echo $e->getMessage(); 120 exit(); 121 } else { 122 //prevent Action/Export() 123 msg($e->getMessage(), -1); 124 $event->data = 'redirect'; 125 return; 126 } 127 } 128 $event->preventDefault(); // after prevent, $event->data cannot be changed 129 130 // deliver the file 131 $this->sendPDFFile($cachefile); //exits 132 } 133 134 /** 135 * Obtain list of pages and title, for different methods of exporting the pdf. 136 * - Return a title and selection, throw otherwise an exception 137 * - Check permisions 138 * 139 * @param Doku_Event $event 140 * @return array 141 * @throws Exception 142 */ 143 protected function collectExportablePages(Event $event) 144 { 145 global $ID, $REV; 146 global $INPUT; 147 global $conf, $lang; 148 149 // list of one or multiple pages 150 $list = []; 151 152 if ($event->data == 'export_pdf') { 153 if (auth_quickaclcheck($ID) < AUTH_READ) { // set more specific denied message 154 throw new Exception($lang['accessdenied']); 155 } 156 $list[0] = $ID; 157 $title = $INPUT->str('pdftitle'); //DEPRECATED 158 $title = $INPUT->str('book_title', $title, true); 159 if (empty($title)) { 160 $title = p_get_first_heading($ID); 161 } 162 // use page name if title is still empty 163 if (empty($title)) { 164 $title = noNS($ID); 165 } 166 167 $filename = wikiFN($ID, $REV); 168 if (!file_exists($filename)) { 169 throw new Exception($this->getLang('notexist')); 170 } 171 } elseif ($event->data == 'export_pdfns') { 172 //check input for title and ns 173 if (!$title = $INPUT->str('book_title')) { 174 throw new Exception($this->getLang('needtitle')); 175 } 176 $pdfnamespace = cleanID($INPUT->str('book_ns')); 177 if (!@is_dir(dirname(wikiFN($pdfnamespace . ':dummy')))) { 178 throw new Exception($this->getLang('needns')); 179 } 180 181 //sort order 182 $order = $INPUT->str('book_order', 'natural', true); 183 $sortoptions = ['pagename', 'date', 'natural']; 184 if (!in_array($order, $sortoptions)) { 185 $order = 'natural'; 186 } 187 188 //search depth 189 $depth = $INPUT->int('book_nsdepth', 0); 190 if ($depth < 0) { 191 $depth = 0; 192 } 193 194 //page search 195 $result = []; 196 $opts = ['depth' => $depth]; //recursive all levels 197 $dir = utf8_encodeFN(str_replace(':', '/', $pdfnamespace)); 198 search($result, $conf['datadir'], 'search_allpages', $opts, $dir); 199 200 // exclude ids 201 $excludes = $INPUT->arr('excludes'); 202 if (!empty($excludes)) { 203 $result = array_filter($result, function ($item) use ($excludes) { 204 return !in_array($item['id'], $excludes); 205 }); 206 } 207 // exclude namespaces 208 $excludesns = $INPUT->arr('excludesns'); 209 if (!empty($excludesns)) { 210 $result = array_filter($result, function ($item) use ($excludesns) { 211 foreach ($excludesns as $ns) { 212 if (strpos($item['id'], $ns . ':') === 0) return false; 213 } 214 return true; 215 }); 216 } 217 218 //sorting 219 if (count($result) > 0) { 220 if ($order == 'date') { 221 usort($result, [$this, 'cbDateSort']); 222 } elseif ($order == 'pagename' || $order == 'natural') { 223 usort($result, [$this, 'cbPagenameSort']); 224 } 225 } 226 227 foreach ($result as $item) { 228 $list[] = $item['id']; 229 } 230 231 if ($pdfnamespace !== '') { 232 if (!in_array($pdfnamespace . ':' . $conf['start'], $list, true)) { 233 if (file_exists(wikiFN(rtrim($pdfnamespace, ':')))) { 234 array_unshift($list, rtrim($pdfnamespace, ':')); 235 } 236 } 237 } 238 } elseif (!empty($_COOKIE['list-pagelist'])) { 239 /** @deprecated April 2016 replaced by localStorage version of Bookcreator */ 240 //is in Bookmanager of bookcreator plugin a title given? 241 $title = $INPUT->str('pdfbook_title'); //DEPRECATED 242 $title = $INPUT->str('book_title', $title, true); 243 if (empty($title)) { 244 throw new Exception($this->getLang('needtitle')); 245 } 246 247 $list = explode("|", $_COOKIE['list-pagelist']); 248 } elseif ($INPUT->has('selection')) { 249 //handle Bookcreator requests based at localStorage 250// if(!checkSecurityToken()) { 251// http_status(403); 252// print $this->getLang('empty'); 253// exit(); 254// } 255 256 $list = json_decode($INPUT->str('selection', '', true), true); 257 if (!is_array($list) || $list === []) { 258 throw new Exception($this->getLang('empty')); 259 } 260 261 $title = $INPUT->str('pdfbook_title'); //DEPRECATED 262 $title = $INPUT->str('book_title', $title, true); 263 if (empty($title)) { 264 throw new Exception($this->getLang('needtitle')); 265 } 266 } elseif ($INPUT->has('savedselection')) { 267 //export a saved selection of the Bookcreator Plugin 268 if (plugin_isdisabled('bookcreator')) { 269 throw new Exception($this->getLang('missingbookcreator')); 270 } 271 /** @var action_plugin_bookcreator_handleselection $SelectionHandling */ 272 $SelectionHandling = plugin_load('action', 'bookcreator_handleselection'); 273 $savedselection = $SelectionHandling->loadSavedSelection($INPUT->str('savedselection')); 274 $title = $savedselection['title']; 275 $title = $INPUT->str('book_title', $title, true); 276 $list = $savedselection['selection']; 277 278 if (empty($title)) { 279 throw new Exception($this->getLang('needtitle')); 280 } 281 } else { 282 //show empty bookcreator message 283 throw new Exception($this->getLang('empty')); 284 } 285 286 $list = array_map('cleanID', $list); 287 288 $skippedpages = []; 289 foreach ($list as $index => $pageid) { 290 if (auth_quickaclcheck($pageid) < AUTH_READ) { 291 $skippedpages[] = $pageid; 292 unset($list[$index]); 293 } 294 } 295 $list = array_filter($list, 'strlen'); //use of strlen() callback prevents removal of pagename '0' 296 297 //if selection contains forbidden pages throw (overridable) warning 298 if (!$INPUT->bool('book_skipforbiddenpages') && $skippedpages !== []) { 299 $msg = hsc(implode(', ', $skippedpages)); 300 throw new Exception(sprintf($this->getLang('forbidden'), $msg)); 301 } 302 303 return [$title, $list]; 304 } 305 306 /** 307 * Prepare cache 308 * 309 * @param array $depends (reference) array with dependencies 310 * @return cache 311 */ 312 protected function prepareCache(&$depends) 313 { 314 global $REV; 315 316 $cachekey = implode(',', $this->list) 317 . $REV 318 . $this->getExportConfig('template') 319 . $this->getExportConfig('pagesize') 320 . $this->getExportConfig('orientation') 321 . $this->getExportConfig('font-size') 322 . $this->getExportConfig('doublesided') 323 . $this->getExportConfig('headernumber') 324 . ($this->getExportConfig('hasToC') ? implode('-', $this->getExportConfig('levels')) : '0') 325 . $this->title; 326 $cache = new Cache($cachekey, '.dw2.pdf'); 327 328 $dependencies = []; 329 foreach ($this->list as $pageid) { 330 $relations = p_get_metadata($pageid, 'relation'); 331 332 if (is_array($relations)) { 333 if (array_key_exists('media', $relations) && is_array($relations['media'])) { 334 foreach ($relations['media'] as $mediaid => $exists) { 335 if ($exists) { 336 $dependencies[] = mediaFN($mediaid); 337 } 338 } 339 } 340 341 if (array_key_exists('haspart', $relations) && is_array($relations['haspart'])) { 342 foreach ($relations['haspart'] as $part_pageid => $exists) { 343 if ($exists) { 344 $dependencies[] = wikiFN($part_pageid); 345 } 346 } 347 } 348 } 349 350 $dependencies[] = metaFN($pageid, '.meta'); 351 } 352 353 $depends['files'] = array_map('wikiFN', $this->list); 354 $depends['files'][] = __FILE__; 355 $depends['files'][] = __DIR__ . '/renderer.php'; 356 $depends['files'][] = __DIR__ . '/mpdf/mpdf.php'; 357 $depends['files'] = array_merge( 358 $depends['files'], 359 $dependencies, 360 getConfigFiles('main') 361 ); 362 return $cache; 363 } 364 365 /** 366 * Returns the parsed Wikitext in dw2pdf for the given id and revision 367 * 368 * @param string $id page id 369 * @param string|int $rev revision timestamp or empty string 370 * @param string $date_at 371 * @return null|string 372 */ 373 protected function wikiToDW2PDF($id, $rev = '', $date_at = '') 374 { 375 $file = wikiFN($id, $rev); 376 377 if (!file_exists($file)) return ''; 378 379 //ensure $id is in global $ID (needed for parsing) 380 global $ID; 381 $keep = $ID; 382 $ID = $id; 383 384 if ($rev || $date_at) { 385 //no caching on old revisions 386 $ret = p_render('dw2pdf', p_get_instructions(io_readWikiPage($file, $id, $rev)), $info, $date_at); 387 } else { 388 $ret = p_cached_output($file, 'dw2pdf', $id); 389 } 390 391 //restore ID (just in case) 392 $ID = $keep; 393 394 return $ret; 395 } 396 397 /** 398 * Build a pdf from the html 399 * 400 * @param string $cachefile 401 * @param Doku_Event $event 402 * @throws MpdfException 403 */ 404 protected function generatePDF($cachefile, $event) 405 { 406 global $REV, $INPUT, $DATE_AT; 407 408 if ($event->data == 'export_pdf') { //only one page is exported 409 $rev = $REV; 410 $date_at = $DATE_AT; 411 } else { 412 //we are exporting entire namespace, ommit revisions 413 $rev = ''; 414 $date_at = ''; 415 } 416 417 //some shortcuts to export settings 418 $hasToC = $this->getExportConfig('hasToC'); 419 $levels = $this->getExportConfig('levels'); 420 $isDebug = $this->getExportConfig('isDebug'); 421 $watermark = $this->getExportConfig('watermark'); 422 423 // initialize PDF library 424 require_once(__DIR__ . "/DokuPDF.class.php"); 425 426 $mpdf = new DokuPDF( 427 $this->getExportConfig('pagesize'), 428 $this->getExportConfig('orientation'), 429 $this->getExportConfig('font-size'), 430 $this->getDocumentLanguage($this->list[0]) //use language of first page 431 ); 432 433 // let mpdf fix local links 434 $self = parse_url(DOKU_URL); 435 $url = $self['scheme'] . '://' . $self['host']; 436 if (!empty($self['port'])) { 437 $url .= ':' . $self['port']; 438 } 439 $mpdf->SetBasePath($url); 440 441 // Set the title 442 $mpdf->SetTitle($this->title); 443 444 // some default document settings 445 //note: double-sided document, starts at an odd page (first page is a right-hand side page) 446 // single-side document has only odd pages 447 $mpdf->mirrorMargins = $this->getExportConfig('doublesided'); 448 $mpdf->setAutoTopMargin = 'stretch'; 449 $mpdf->setAutoBottomMargin = 'stretch'; 450// $mpdf->pagenumSuffix = '/'; //prefix for {nbpg} 451 if ($hasToC) { 452 $mpdf->h2toc = $levels; 453 } 454 $mpdf->PageNumSubstitutions[] = ['from' => 1, 'reset' => 0, 'type' => '1', 'suppress' => 'off']; 455 456 // Watermarker 457 if ($watermark) { 458 $mpdf->SetWatermarkText($watermark); 459 $mpdf->showWatermarkText = true; 460 } 461 462 // load the template 463 $template = $this->loadTemplate(); 464 465 // prepare HTML header styles 466 $html = ''; 467 if ($isDebug) { 468 $html .= '<html><head>'; 469 $html .= '<style>'; 470 } 471 472 $styles = '@page { size:auto; ' . $template['page'] . '}'; 473 $styles .= '@page :first {' . $template['first'] . '}'; 474 475 $styles .= '@page landscape-page { size:landscape }'; 476 $styles .= 'div.dw2pdf-landscape { page:landscape-page }'; 477 $styles .= '@page portrait-page { size:portrait }'; 478 $styles .= 'div.dw2pdf-portrait { page:portrait-page }'; 479 $styles .= $this->loadCSS(); 480 481 $mpdf->WriteHTML($styles, 1); 482 483 if ($isDebug) { 484 $html .= $styles; 485 $html .= '</style>'; 486 $html .= '</head><body>'; 487 } 488 489 $body_start = $template['html']; 490 $body_start .= '<div class="dokuwiki">'; 491 492 // insert the cover page 493 $body_start .= $template['cover']; 494 495 $mpdf->WriteHTML($body_start, 2, true, false); //start body html 496 if ($isDebug) { 497 $html .= $body_start; 498 } 499 if ($hasToC) { 500 //Note: - for double-sided document the ToC is always on an even number of pages, so that the 501 // following content is on a correct odd/even page 502 // - first page of ToC starts always at odd page (so eventually an additional blank page 503 // is included before) 504 // - there is no page numbering at the pages of the ToC 505 $mpdf->TOCpagebreakByArray([ 506 'toc-preHTML' => '<h2>' . $this->getLang('tocheader') . '</h2>', 507 'toc-bookmarkText' => $this->getLang('tocheader'), 508 'links' => true, 509 'outdent' => '1em', 510 'pagenumstyle' => '1' 511 ]); 512 $html .= '<tocpagebreak>'; 513 } 514 515 // loop over all pages 516 $counter = 0; 517 $no_pages = count($this->list); 518 foreach ($this->list as $page) { 519 $this->currentBookChapter = $counter; 520 $counter++; 521 522 $pagehtml = $this->wikiToDW2PDF($page, $rev, $date_at); 523 //file doesn't exists 524 if ($pagehtml == '') { 525 continue; 526 } 527 $pagehtml .= $this->pageDependReplacements($template['cite'], $page); 528 if ($counter < $no_pages) { 529 $pagehtml .= '<pagebreak />'; 530 } 531 532 $mpdf->WriteHTML($pagehtml, 2, false, false); //intermediate body html 533 if ($isDebug) { 534 $html .= $pagehtml; 535 } 536 } 537 538 // insert the back page 539 $body_end = $template['back']; 540 541 $body_end .= '</div>'; 542 543 $mpdf->WriteHTML($body_end, 2, false, true); // finish body html 544 if ($isDebug) { 545 $html .= $body_end; 546 $html .= '</body>'; 547 $html .= '</html>'; 548 } 549 550 //Return html for debugging 551 if ($isDebug) { 552 if ($INPUT->str('debughtml', 'text', true) == 'text') { 553 header('Content-Type: text/plain; charset=utf-8'); 554 } 555 echo $html; 556 exit(); 557 } 558 559 // write to cache file 560 $mpdf->Output($cachefile, 'F'); 561 } 562 563 /** 564 * @param string $cachefile 565 */ 566 protected function sendPDFFile($cachefile) 567 { 568 header('Content-Type: application/pdf'); 569 header('Cache-Control: must-revalidate, no-transform, post-check=0, pre-check=0'); 570 header('Pragma: public'); 571 http_conditionalRequest(filemtime($cachefile)); 572 global $INPUT; 573 $outputTarget = $INPUT->str('outputTarget', $this->getConf('output')); 574 575 $filename = rawurlencode(cleanID(strtr($this->title, ':/;"', ' '))); 576 if ($outputTarget === 'file') { 577 header('Content-Disposition: attachment; filename="' . $filename . '.pdf";'); 578 } else { 579 header('Content-Disposition: inline; filename="' . $filename . '.pdf";'); 580 } 581 582 //Bookcreator uses jQuery.fileDownload.js, which requires a cookie. 583 header('Set-Cookie: fileDownload=true; path=/'); 584 585 //try to send file, and exit if done 586 http_sendfile($cachefile); 587 588 $fp = @fopen($cachefile, "rb"); 589 if ($fp) { 590 http_rangeRequest($fp, filesize($cachefile), 'application/pdf'); 591 } else { 592 header("HTTP/1.0 500 Internal Server Error"); 593 echo "Could not read file - bad permissions?"; 594 } 595 exit(); 596 } 597 598 /** 599 * Load the various template files and prepare the HTML/CSS for insertion 600 * 601 * @return array 602 */ 603 protected function loadTemplate() 604 { 605 global $ID; 606 global $conf; 607 global $INFO; 608 609 // this is what we'll return 610 $output = [ 611 'cover' => '', 612 'back' => '', 613 'html' => '', 614 'page' => '', 615 'first' => '', 616 'cite' => '', 617 ]; 618 619 // prepare header/footer elements 620 $html = ''; 621 foreach (['header', 'footer'] as $section) { 622 foreach (['', '_odd', '_even', '_first'] as $order) { 623 $file = DOKU_PLUGIN . 'dw2pdf/tpl/' . $this->tpl . '/' . $section . $order . '.html'; 624 if (file_exists($file)) { 625 $html .= '<htmlpage' . $section . ' name="' . $section . $order . '">' . DOKU_LF; 626 $html .= file_get_contents($file) . DOKU_LF; 627 $html .= '</htmlpage' . $section . '>' . DOKU_LF; 628 629 // register the needed pseudo CSS 630 if ($order == '_first') { 631 $output['first'] .= $section . ': html_' . $section . $order . ';' . DOKU_LF; 632 } elseif ($order == '_even') { 633 $output['page'] .= 'even-' . $section . '-name: html_' . $section . $order . ';' . DOKU_LF; 634 } elseif ($order == '_odd') { 635 $output['page'] .= 'odd-' . $section . '-name: html_' . $section . $order . ';' . DOKU_LF; 636 } else { 637 $output['page'] .= $section . ': html_' . $section . $order . ';' . DOKU_LF; 638 } 639 } 640 } 641 } 642 643 // prepare replacements 644 $replace = [ 645 '@PAGE@' => '{PAGENO}', 646 '@PAGES@' => '{nbpg}', //see also $mpdf->pagenumSuffix = ' / ' 647 '@TITLE@' => hsc($this->title), 648 '@WIKI@' => $conf['title'], 649 '@WIKIURL@' => DOKU_URL, 650 '@DATE@' => dformat(time()), 651 '@USERNAME@' => $INFO['userinfo']['name'] ?? '', 652 '@BASE@' => DOKU_BASE, 653 '@INC@' => DOKU_INC, 654 '@TPLBASE@' => DOKU_BASE . 'lib/plugins/dw2pdf/tpl/' . $this->tpl . '/', 655 '@TPLINC@' => DOKU_INC . 'lib/plugins/dw2pdf/tpl/' . $this->tpl . '/' 656 ]; 657 658 // set HTML element 659 $html = str_replace(array_keys($replace), array_values($replace), $html); 660 //TODO For bookcreator $ID (= bookmanager page) makes no sense 661 $output['html'] = $this->pageDependReplacements($html, $ID); 662 663 // cover page 664 $coverfile = DOKU_PLUGIN . 'dw2pdf/tpl/' . $this->tpl . '/cover.html'; 665 if (file_exists($coverfile)) { 666 $output['cover'] = file_get_contents($coverfile); 667 $output['cover'] = str_replace(array_keys($replace), array_values($replace), $output['cover']); 668 $output['cover'] = $this->pageDependReplacements($output['cover'], $ID); 669 $output['cover'] .= '<pagebreak />'; 670 } 671 672 // cover page 673 $backfile = DOKU_PLUGIN . 'dw2pdf/tpl/' . $this->tpl . '/back.html'; 674 if (file_exists($backfile)) { 675 $output['back'] = '<pagebreak />'; 676 $output['back'] .= file_get_contents($backfile); 677 $output['back'] = str_replace(array_keys($replace), array_values($replace), $output['back']); 678 $output['back'] = $this->pageDependReplacements($output['back'], $ID); 679 } 680 681 // citation box 682 $citationfile = DOKU_PLUGIN . 'dw2pdf/tpl/' . $this->tpl . '/citation.html'; 683 if (file_exists($citationfile)) { 684 $output['cite'] = file_get_contents($citationfile); 685 $output['cite'] = str_replace(array_keys($replace), array_values($replace), $output['cite']); 686 } 687 688 return $output; 689 } 690 691 /** 692 * @param string $raw code with placeholders 693 * @param string $id pageid 694 * @return string 695 */ 696 protected function pageDependReplacements($raw, $id) 697 { 698 global $REV, $DATE_AT; 699 700 // generate qr code for this page 701 $qr_code = ''; 702 if ($this->getConf('qrcodescale')) { 703 $url = hsc(wl($id, '', '&', true)); 704 $size = (float)$this->getConf('qrcodescale'); 705 $qr_code = sprintf( 706 '<barcode type="QR" code="%s" error="Q" disableborder="1" class="qrcode" size="%s" />', 707 $url, 708 $size 709 ); 710 } 711 // prepare replacements 712 $replace['@ID@'] = $id; 713 $replace['@UPDATE@'] = dformat(filemtime(wikiFN($id, $REV))); 714 715 $params = []; 716 if ($DATE_AT) { 717 $params['at'] = $DATE_AT; 718 } elseif ($REV) { 719 $params['rev'] = $REV; 720 } 721 $replace['@PAGEURL@'] = wl($id, $params, true, "&"); 722 $replace['@QRCODE@'] = $qr_code; 723 724 $content = $raw; 725 726 // let other plugins define their own replacements 727 $evdata = ['id' => $id, 'replace' => &$replace, 'content' => &$content]; 728 $event = new Event('PLUGIN_DW2PDF_REPLACE', $evdata); 729 if ($event->advise_before()) { 730 $content = str_replace(array_keys($replace), array_values($replace), $raw); 731 } 732 733 // plugins may post-process HTML, e.g to clean up unused replacements 734 $event->advise_after(); 735 736 // @DATE(<date>[, <format>])@ 737 $content = preg_replace_callback( 738 '/@DATE\((.*?)(?:,\s*(.*?))?\)@/', 739 [$this, 'replaceDate'], 740 $content 741 ); 742 743 return $content; 744 } 745 746 747 /** 748 * (callback) Replace date by request datestring 749 * e.g. '%m(30-11-1975)' is replaced by '11' 750 * 751 * @param array $match with [0]=>whole match, [1]=> first subpattern, [2] => second subpattern 752 * @return string 753 */ 754 public function replaceDate($match) 755 { 756 global $conf; 757 //no 2nd argument for default date format 758 if ($match[2] == null) { 759 $match[2] = $conf['dformat']; 760 } 761 return strftime($match[2], strtotime($match[1])); 762 } 763 764 /** 765 * Load all the style sheets and apply the needed replacements 766 */ 767 protected function loadCSS() 768 { 769 global $conf; 770 //reuse the CSS dispatcher functions without triggering the main function 771 define('SIMPLE_TEST', 1); 772 require_once(DOKU_INC . 'lib/exe/css.php'); 773 774 // prepare CSS files 775 $files = array_merge( 776 [ 777 DOKU_INC . 'lib/styles/screen.css' => DOKU_BASE . 'lib/styles/', 778 DOKU_INC . 'lib/styles/print.css' => DOKU_BASE . 'lib/styles/', 779 ], 780 $this->cssPluginPDFstyles(), 781 [ 782 DOKU_PLUGIN . 'dw2pdf/conf/style.css' => DOKU_BASE . 'lib/plugins/dw2pdf/conf/', 783 DOKU_PLUGIN . 'dw2pdf/tpl/' . $this->tpl . '/style.css' => 784 DOKU_BASE . 'lib/plugins/dw2pdf/tpl/' . $this->tpl . '/', 785 DOKU_PLUGIN . 'dw2pdf/conf/style.local.css' => DOKU_BASE . 'lib/plugins/dw2pdf/conf/', 786 ] 787 ); 788 $css = ''; 789 foreach ($files as $file => $location) { 790 $display = str_replace(fullpath(DOKU_INC), '', fullpath($file)); 791 $css .= "\n/* XXXXXXXXX $display XXXXXXXXX */\n"; 792 $css .= css_loadfile($file, $location); 793 } 794 795 if (function_exists('css_parseless')) { 796 // apply pattern replacements 797 if (function_exists('css_styleini')) { 798 // compatiblity layer for pre-Greebo releases of DokuWiki 799 $styleini = css_styleini($conf['template']); 800 } else { 801 // Greebo functionality 802 $styleUtils = new StyleUtils(); 803 $styleini = $styleUtils->cssStyleini($conf['template']); // older versions need still the template 804 } 805 $css = css_applystyle($css, $styleini['replacements']); 806 807 // parse less 808 $css = css_parseless($css); 809 } else { 810 // @deprecated 2013-12-19: fix backward compatibility 811 $css = css_applystyle($css, DOKU_INC . 'lib/tpl/' . $conf['template'] . '/'); 812 } 813 814 return $css; 815 } 816 817 /** 818 * Returns a list of possible Plugin PDF Styles 819 * 820 * Checks for a pdf.css, falls back to print.css 821 * 822 * @author Andreas Gohr <andi@splitbrain.org> 823 */ 824 protected function cssPluginPDFstyles() 825 { 826 $list = []; 827 $plugins = plugin_list(); 828 829 $usestyle = explode(',', $this->getConf('usestyles')); 830 foreach ($plugins as $p) { 831 if (in_array($p, $usestyle)) { 832 $list[DOKU_PLUGIN . "$p/screen.css"] = DOKU_BASE . "lib/plugins/$p/"; 833 $list[DOKU_PLUGIN . "$p/screen.less"] = DOKU_BASE . "lib/plugins/$p/"; 834 835 $list[DOKU_PLUGIN . "$p/style.css"] = DOKU_BASE . "lib/plugins/$p/"; 836 $list[DOKU_PLUGIN . "$p/style.less"] = DOKU_BASE . "lib/plugins/$p/"; 837 } 838 839 $list[DOKU_PLUGIN . "$p/all.css"] = DOKU_BASE . "lib/plugins/$p/"; 840 $list[DOKU_PLUGIN . "$p/all.less"] = DOKU_BASE . "lib/plugins/$p/"; 841 842 if (file_exists(DOKU_PLUGIN . "$p/pdf.css") || file_exists(DOKU_PLUGIN . "$p/pdf.less")) { 843 $list[DOKU_PLUGIN . "$p/pdf.css"] = DOKU_BASE . "lib/plugins/$p/"; 844 $list[DOKU_PLUGIN . "$p/pdf.less"] = DOKU_BASE . "lib/plugins/$p/"; 845 } else { 846 $list[DOKU_PLUGIN . "$p/print.css"] = DOKU_BASE . "lib/plugins/$p/"; 847 $list[DOKU_PLUGIN . "$p/print.less"] = DOKU_BASE . "lib/plugins/$p/"; 848 } 849 } 850 851 // template support 852 foreach ( 853 [ 854 'pdf.css', 855 'pdf.less', 856 'css/pdf.css', 857 'css/pdf.less', 858 'styles/pdf.css', 859 'styles/pdf.less' 860 ] as $file 861 ) { 862 if (file_exists(tpl_incdir() . $file)) { 863 $list[tpl_incdir() . $file] = tpl_basedir() . $file; 864 } 865 } 866 867 return $list; 868 } 869 870 /** 871 * Returns array of pages which will be included in the exported pdf 872 * 873 * @return array 874 */ 875 public function getExportedPages() 876 { 877 return $this->list; 878 } 879 880 /** 881 * usort callback to sort by file lastmodified time 882 * 883 * @param array $a 884 * @param array $b 885 * @return int 886 */ 887 public function cbDateSort($a, $b) 888 { 889 if ($b['rev'] < $a['rev']) return -1; 890 if ($b['rev'] > $a['rev']) return 1; 891 return strcmp($b['id'], $a['id']); 892 } 893 894 /** 895 * usort callback to sort by page id 896 * @param array $a 897 * @param array $b 898 * @return int 899 */ 900 public function cbPagenameSort($a, $b) 901 { 902 global $conf; 903 904 $partsA = explode(':', $a['id']); 905 $countA = count($partsA); 906 $partsB = explode(':', $b['id']); 907 $countB = count($partsB); 908 $max = max($countA, $countB); 909 910 911 // compare namepsace by namespace 912 for ($i = 0; $i < $max; $i++) { 913 $partA = $partsA[$i] ?: null; 914 $partB = $partsB[$i] ?: null; 915 916 // have we reached the page level? 917 if ($i === ($countA - 1) || $i === ($countB - 1)) { 918 // start page first 919 if ($partA == $conf['start']) return -1; 920 if ($partB == $conf['start']) return 1; 921 } 922 923 // prefer page over namespace 924 if ($partA === $partB) { 925 if (!isset($partsA[$i + 1])) return -1; 926 if (!isset($partsB[$i + 1])) return 1; 927 continue; 928 } 929 930 931 // simply compare 932 return strnatcmp($partA, $partB); 933 } 934 935 return strnatcmp($a['id'], $b['id']); 936 } 937 938 /** 939 * Collects settings from: 940 * 1. url parameters 941 * 2. plugin config 942 * 3. global config 943 */ 944 protected function loadExportConfig() 945 { 946 global $INPUT; 947 global $conf; 948 949 $this->exportConfig = []; 950 951 // decide on the paper setup from param or config 952 $this->exportConfig['pagesize'] = $INPUT->str('pagesize', $this->getConf('pagesize'), true); 953 $this->exportConfig['orientation'] = $INPUT->str('orientation', $this->getConf('orientation'), true); 954 955 // decide on the font-size from param or config 956 $this->exportConfig['font-size'] = $INPUT->str('font-size', $this->getConf('font-size'), true); 957 958 $doublesided = $INPUT->bool('doublesided', (bool)$this->getConf('doublesided')); 959 $this->exportConfig['doublesided'] = $doublesided ? '1' : '0'; 960 961 $this->exportConfig['watermark'] = $INPUT->str('watermark', ''); 962 963 $hasToC = $INPUT->bool('toc', (bool)$this->getConf('toc')); 964 $levels = []; 965 if ($hasToC) { 966 $toclevels = $INPUT->str('toclevels', $this->getConf('toclevels'), true); 967 [$top_input, $max_input] = array_pad(explode('-', $toclevels, 2), 2, ''); 968 [$top_conf, $max_conf] = array_pad(explode('-', $this->getConf('toclevels'), 2), 2, ''); 969 $bounds_input = [ 970 'top' => [ 971 (int)$top_input, 972 (int)$top_conf 973 ], 974 'max' => [ 975 (int)$max_input, 976 (int)$max_conf 977 ] 978 ]; 979 $bounds = [ 980 'top' => $conf['toptoclevel'], 981 'max' => $conf['maxtoclevel'] 982 983 ]; 984 foreach ($bounds_input as $bound => $values) { 985 foreach ($values as $value) { 986 if ($value > 0 && $value <= 5) { 987 //stop at valid value and store 988 $bounds[$bound] = $value; 989 break; 990 } 991 } 992 } 993 994 if ($bounds['max'] < $bounds['top']) { 995 $bounds['max'] = $bounds['top']; 996 } 997 998 for ($level = $bounds['top']; $level <= $bounds['max']; $level++) { 999 $levels["H$level"] = $level - 1; 1000 } 1001 } 1002 $this->exportConfig['hasToC'] = $hasToC; 1003 $this->exportConfig['levels'] = $levels; 1004 1005 $this->exportConfig['maxbookmarks'] = $INPUT->int('maxbookmarks', $this->getConf('maxbookmarks'), true); 1006 1007 $tplconf = $this->getConf('template'); 1008 $tpl = $INPUT->str('tpl', $tplconf, true); 1009 if (!is_dir(DOKU_PLUGIN . 'dw2pdf/tpl/' . $tpl)) { 1010 $tpl = $tplconf; 1011 } 1012 if (!$tpl) { 1013 $tpl = 'default'; 1014 } 1015 $this->exportConfig['template'] = $tpl; 1016 1017 $this->exportConfig['isDebug'] = $conf['allowdebug'] && $INPUT->has('debughtml'); 1018 } 1019 1020 /** 1021 * Returns requested config 1022 * 1023 * @param string $name 1024 * @param mixed $notset 1025 * @return mixed|bool 1026 */ 1027 public function getExportConfig($name, $notset = false) 1028 { 1029 if ($this->exportConfig === null) { 1030 $this->loadExportConfig(); 1031 } 1032 1033 return $this->exportConfig[$name] ?? $notset; 1034 } 1035 1036 /** 1037 * Add 'export pdf'-button to pagetools 1038 * 1039 * @param Doku_Event $event 1040 */ 1041 public function addbutton(Event $event) 1042 { 1043 global $ID, $REV, $DATE_AT; 1044 1045 if ($this->getConf('showexportbutton') && $event->data['view'] == 'main') { 1046 $params = ['do' => 'export_pdf']; 1047 if ($DATE_AT) { 1048 $params['at'] = $DATE_AT; 1049 } elseif ($REV) { 1050 $params['rev'] = $REV; 1051 } 1052 1053 // insert button at position before last (up to top) 1054 $event->data['items'] = array_slice($event->data['items'], 0, -1, true) + 1055 ['export_pdf' => sprintf( 1056 '<li><a href="%s" class="%s" rel="nofollow" title="%s"><span>%s</span></a></li>', 1057 wl($ID, $params), 1058 'action export_pdf', 1059 $this->getLang('export_pdf_button'), 1060 $this->getLang('export_pdf_button') 1061 )] + 1062 array_slice($event->data['items'], -1, 1, true); 1063 } 1064 } 1065 1066 /** 1067 * Add 'export pdf' button to page tools, new SVG based mechanism 1068 * 1069 * @param Doku_Event $event 1070 */ 1071 public function addsvgbutton(Event $event) 1072 { 1073 global $INFO; 1074 if ($event->data['view'] != 'page' || !$this->getConf('showexportbutton')) { 1075 return; 1076 } 1077 1078 if (!$INFO['exists']) { 1079 return; 1080 } 1081 1082 array_splice($event->data['items'], -1, 0, [new MenuItem()]); 1083 } 1084 1085 /** 1086 * Get the language of the current document 1087 * 1088 * Uses the translation plugin if available 1089 * @return string 1090 */ 1091 protected function getDocumentLanguage($pageid) 1092 { 1093 global $conf; 1094 1095 $lang = $conf['lang']; 1096 /** @var helper_plugin_translation $trans */ 1097 $trans = plugin_load('helper', 'translation'); 1098 if ($trans) { 1099 $tr = $trans->getLangPart($pageid); 1100 if ($tr) $lang = $tr; 1101 } 1102 1103 return $lang; 1104 } 1105} 1106