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