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); // 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 * @return string css styles 770 */ 771 protected function loadCSS() 772 { 773 global $conf; 774 //reuse the CSS dispatcher functions without triggering the main function 775 define('SIMPLE_TEST', 1); 776 require_once(DOKU_INC . 'lib/exe/css.php'); 777 778 // prepare CSS files 779 $files = array_merge( 780 [ 781 DOKU_INC . 'lib/styles/screen.css' => DOKU_BASE . 'lib/styles/', 782 DOKU_INC . 'lib/styles/print.css' => DOKU_BASE . 'lib/styles/', 783 ], 784 $this->cssPluginPDFstyles(), 785 [ 786 DOKU_PLUGIN . 'dw2pdf/conf/style.css' => DOKU_BASE . 'lib/plugins/dw2pdf/conf/', 787 DOKU_PLUGIN . 'dw2pdf/tpl/' . $this->tpl . '/style.css' => 788 DOKU_BASE . 'lib/plugins/dw2pdf/tpl/' . $this->tpl . '/', 789 DOKU_PLUGIN . 'dw2pdf/conf/style.local.css' => DOKU_BASE . 'lib/plugins/dw2pdf/conf/', 790 ] 791 ); 792 $css = ''; 793 foreach ($files as $file => $location) { 794 $display = str_replace(fullpath(DOKU_INC), '', fullpath($file)); 795 $css .= "\n/* XXXXXXXXX $display XXXXXXXXX */\n"; 796 $css .= css_loadfile($file, $location); 797 } 798 799 // apply pattern replacements 800 if (function_exists('css_styleini')) { 801 // compatiblity layer for pre-Greebo releases of DokuWiki 802 $styleini = css_styleini($conf['template']); 803 } else { 804 // Greebo functionality 805 $styleUtils = new StyleUtils(); 806 $styleini = $styleUtils->cssStyleini($conf['template']); // older versions need still the template 807 } 808 $css = css_applystyle($css, $styleini['replacements']); 809 810 // parse less 811 return css_parseless($css); 812 } 813 814 /** 815 * Returns a list of possible Plugin PDF Styles 816 * 817 * Checks for a pdf.css, falls back to print.css 818 * 819 * @author Andreas Gohr <andi@splitbrain.org> 820 */ 821 protected function cssPluginPDFstyles() 822 { 823 $list = []; 824 $plugins = plugin_list(); 825 826 $usestyle = explode(',', $this->getConf('usestyles')); 827 foreach ($plugins as $p) { 828 if (in_array($p, $usestyle)) { 829 $list[DOKU_PLUGIN . "$p/screen.css"] = DOKU_BASE . "lib/plugins/$p/"; 830 $list[DOKU_PLUGIN . "$p/screen.less"] = DOKU_BASE . "lib/plugins/$p/"; 831 832 $list[DOKU_PLUGIN . "$p/style.css"] = DOKU_BASE . "lib/plugins/$p/"; 833 $list[DOKU_PLUGIN . "$p/style.less"] = DOKU_BASE . "lib/plugins/$p/"; 834 } 835 836 $list[DOKU_PLUGIN . "$p/all.css"] = DOKU_BASE . "lib/plugins/$p/"; 837 $list[DOKU_PLUGIN . "$p/all.less"] = DOKU_BASE . "lib/plugins/$p/"; 838 839 if (file_exists(DOKU_PLUGIN . "$p/pdf.css") || file_exists(DOKU_PLUGIN . "$p/pdf.less")) { 840 $list[DOKU_PLUGIN . "$p/pdf.css"] = DOKU_BASE . "lib/plugins/$p/"; 841 $list[DOKU_PLUGIN . "$p/pdf.less"] = DOKU_BASE . "lib/plugins/$p/"; 842 } else { 843 $list[DOKU_PLUGIN . "$p/print.css"] = DOKU_BASE . "lib/plugins/$p/"; 844 $list[DOKU_PLUGIN . "$p/print.less"] = DOKU_BASE . "lib/plugins/$p/"; 845 } 846 } 847 848 // template support 849 foreach ( 850 [ 851 'pdf.css', 852 'pdf.less', 853 'css/pdf.css', 854 'css/pdf.less', 855 'styles/pdf.css', 856 'styles/pdf.less' 857 ] as $file 858 ) { 859 if (file_exists(tpl_incdir() . $file)) { 860 $list[tpl_incdir() . $file] = tpl_basedir() . $file; 861 } 862 } 863 864 return $list; 865 } 866 867 /** 868 * Returns array of pages which will be included in the exported pdf 869 * 870 * @return array 871 */ 872 public function getExportedPages() 873 { 874 return $this->list; 875 } 876 877 /** 878 * usort callback to sort by file lastmodified time 879 * 880 * @param array $a 881 * @param array $b 882 * @return int 883 */ 884 public function cbDateSort($a, $b) 885 { 886 if ($b['rev'] < $a['rev']) return -1; 887 if ($b['rev'] > $a['rev']) return 1; 888 return strcmp($b['id'], $a['id']); 889 } 890 891 /** 892 * usort callback to sort by page id 893 * @param array $a 894 * @param array $b 895 * @return int 896 */ 897 public function cbPagenameSort($a, $b) 898 { 899 global $conf; 900 901 $partsA = explode(':', $a['id']); 902 $countA = count($partsA); 903 $partsB = explode(':', $b['id']); 904 $countB = count($partsB); 905 $max = max($countA, $countB); 906 907 908 // compare namepsace by namespace 909 for ($i = 0; $i < $max; $i++) { 910 $partA = $partsA[$i] ?: null; 911 $partB = $partsB[$i] ?: null; 912 913 // have we reached the page level? 914 if ($i === ($countA - 1) || $i === ($countB - 1)) { 915 // start page first 916 if ($partA == $conf['start']) return -1; 917 if ($partB == $conf['start']) return 1; 918 } 919 920 // prefer page over namespace 921 if ($partA === $partB) { 922 if (!isset($partsA[$i + 1])) return -1; 923 if (!isset($partsB[$i + 1])) return 1; 924 continue; 925 } 926 927 928 // simply compare 929 return strnatcmp($partA, $partB); 930 } 931 932 return strnatcmp($a['id'], $b['id']); 933 } 934 935 /** 936 * Collects settings from: 937 * 1. url parameters 938 * 2. plugin config 939 * 3. global config 940 */ 941 protected function loadExportConfig() 942 { 943 global $INPUT; 944 global $conf; 945 946 $this->exportConfig = []; 947 948 // decide on the paper setup from param or config 949 $this->exportConfig['pagesize'] = $INPUT->str('pagesize', $this->getConf('pagesize'), true); 950 $this->exportConfig['orientation'] = $INPUT->str('orientation', $this->getConf('orientation'), true); 951 952 // decide on the font-size from param or config 953 $this->exportConfig['font-size'] = $INPUT->str('font-size', $this->getConf('font-size'), true); 954 955 $doublesided = $INPUT->bool('doublesided', (bool)$this->getConf('doublesided')); 956 $this->exportConfig['doublesided'] = $doublesided ? '1' : '0'; 957 958 $this->exportConfig['watermark'] = $INPUT->str('watermark', ''); 959 960 $hasToC = $INPUT->bool('toc', (bool)$this->getConf('toc')); 961 $levels = []; 962 if ($hasToC) { 963 $toclevels = $INPUT->str('toclevels', $this->getConf('toclevels'), true); 964 [$top_input, $max_input] = array_pad(explode('-', $toclevels, 2), 2, ''); 965 [$top_conf, $max_conf] = array_pad(explode('-', $this->getConf('toclevels'), 2), 2, ''); 966 $bounds_input = [ 967 'top' => [ 968 (int)$top_input, 969 (int)$top_conf 970 ], 971 'max' => [ 972 (int)$max_input, 973 (int)$max_conf 974 ] 975 ]; 976 $bounds = [ 977 'top' => $conf['toptoclevel'], 978 'max' => $conf['maxtoclevel'] 979 980 ]; 981 foreach ($bounds_input as $bound => $values) { 982 foreach ($values as $value) { 983 if ($value > 0 && $value <= 5) { 984 //stop at valid value and store 985 $bounds[$bound] = $value; 986 break; 987 } 988 } 989 } 990 991 if ($bounds['max'] < $bounds['top']) { 992 $bounds['max'] = $bounds['top']; 993 } 994 995 for ($level = $bounds['top']; $level <= $bounds['max']; $level++) { 996 $levels["H$level"] = $level - 1; 997 } 998 } 999 $this->exportConfig['hasToC'] = $hasToC; 1000 $this->exportConfig['levels'] = $levels; 1001 1002 $this->exportConfig['maxbookmarks'] = $INPUT->int('maxbookmarks', $this->getConf('maxbookmarks'), true); 1003 1004 $tplconf = $this->getConf('template'); 1005 $tpl = $INPUT->str('tpl', $tplconf, true); 1006 if (!is_dir(DOKU_PLUGIN . 'dw2pdf/tpl/' . $tpl)) { 1007 $tpl = $tplconf; 1008 } 1009 if (!$tpl) { 1010 $tpl = 'default'; 1011 } 1012 $this->exportConfig['template'] = $tpl; 1013 1014 $this->exportConfig['isDebug'] = $conf['allowdebug'] && $INPUT->has('debughtml'); 1015 } 1016 1017 /** 1018 * Returns requested config 1019 * 1020 * @param string $name 1021 * @param mixed $notset 1022 * @return mixed|bool 1023 */ 1024 public function getExportConfig($name, $notset = false) 1025 { 1026 if ($this->exportConfig === null) { 1027 $this->loadExportConfig(); 1028 } 1029 1030 return $this->exportConfig[$name] ?? $notset; 1031 } 1032 1033 /** 1034 * Add 'export pdf'-button to pagetools 1035 * 1036 * @param Doku_Event $event 1037 */ 1038 public function addbutton(Event $event) 1039 { 1040 global $ID, $REV, $DATE_AT; 1041 1042 if ($this->getConf('showexportbutton') && $event->data['view'] == 'main') { 1043 $params = ['do' => 'export_pdf']; 1044 if ($DATE_AT) { 1045 $params['at'] = $DATE_AT; 1046 } elseif ($REV) { 1047 $params['rev'] = $REV; 1048 } 1049 1050 // insert button at position before last (up to top) 1051 $event->data['items'] = array_slice($event->data['items'], 0, -1, true) + 1052 ['export_pdf' => sprintf( 1053 '<li><a href="%s" class="%s" rel="nofollow" title="%s"><span>%s</span></a></li>', 1054 wl($ID, $params), 1055 'action export_pdf', 1056 $this->getLang('export_pdf_button'), 1057 $this->getLang('export_pdf_button') 1058 )] + 1059 array_slice($event->data['items'], -1, 1, true); 1060 } 1061 } 1062 1063 /** 1064 * Add 'export pdf' button to page tools, new SVG based mechanism 1065 * 1066 * @param Doku_Event $event 1067 */ 1068 public function addsvgbutton(Event $event) 1069 { 1070 global $INFO; 1071 if ($event->data['view'] != 'page' || !$this->getConf('showexportbutton')) { 1072 return; 1073 } 1074 1075 if (!$INFO['exists']) { 1076 return; 1077 } 1078 1079 array_splice($event->data['items'], -1, 0, [new MenuItem()]); 1080 } 1081 1082 /** 1083 * Get the language of the current document 1084 * 1085 * Uses the translation plugin if available 1086 * @return string 1087 */ 1088 protected function getDocumentLanguage($pageid) 1089 { 1090 global $conf; 1091 1092 $lang = $conf['lang']; 1093 /** @var helper_plugin_translation $trans */ 1094 $trans = plugin_load('helper', 'translation'); 1095 if ($trans) { 1096 $tr = $trans->getLangPart($pageid); 1097 if ($tr) $lang = $tr; 1098 } 1099 1100 return $lang; 1101 } 1102} 1103