1<?php 2/** 3 * dw2Pdf Plugin: Conversion from dokuwiki content to pdf. 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author Luigi Micco <l.micco@tiscali.it> 7 * @author Andreas Gohr <andi@splitbrain.org> 8 */ 9 10// must be run within Dokuwiki 11if(!defined('DOKU_INC')) die(); 12 13/** 14 * Class action_plugin_dw2pdf 15 * 16 * Export hmtl content to pdf, for different url parameter configurations 17 * DokuPDF which extends mPDF is used for generating the pdf from html. 18 */ 19class action_plugin_dw2pdf extends DokuWiki_Action_Plugin { 20 21 protected $tpl; 22 protected $list = array(); 23 24 /** 25 * Constructor. Sets the correct template 26 */ 27 public function __construct() { 28 $tpl = false; 29 if(isset($_REQUEST['tpl'])) { 30 $tpl = trim(preg_replace('/[^A-Za-z0-9_\-]+/', '', $_REQUEST['tpl'])); 31 } 32 if(!$tpl) $tpl = $this->getConf('template'); 33 if(!$tpl) $tpl = 'default'; 34 if(!is_dir(DOKU_PLUGIN . 'dw2pdf/tpl/' . $tpl)) $tpl = 'default'; 35 36 $this->tpl = $tpl; 37 } 38 39 /** 40 * Register the events 41 */ 42 public function register(Doku_Event_Handler $controller) { 43 $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'convert', array()); 44 $controller->register_hook('TEMPLATE_PAGETOOLS_DISPLAY', 'BEFORE', $this, 'addbutton', array()); 45 } 46 47 /** 48 * Do the HTML to PDF conversion work 49 * 50 * @param Doku_Event $event 51 * @param array $param 52 * @return bool 53 */ 54 public function convert(&$event, $param) { 55 global $ACT; 56 global $REV; 57 global $ID; 58 global $INPUT, $conf; 59 60 // our event? 61 if(($ACT != 'export_pdfbook') && ($ACT != 'export_pdf') && ($ACT != 'export_pdfns')) return false; 62 63 // check user's rights 64 if(auth_quickaclcheck($ID) < AUTH_READ) return false; 65 66 // one or multiple pages? 67 $this->list = array(); 68 69 if($ACT == 'export_pdf') { 70 $this->list[0] = $ID; 71 $title = p_get_first_heading($ID); 72 73 } elseif($ACT == 'export_pdfns') { 74 //check input for title and ns 75 if(!$title = $INPUT->str('pdfns_title')) { 76 $this->showPageWithErrorMsg($event, 'needtitle'); 77 return false; 78 } 79 $pdfnamespace = cleanID($INPUT->str('pdfns_ns')); 80 if(!@is_dir(dirname(wikiFN($pdfnamespace . ':dummy')))) { 81 $this->showPageWithErrorMsg($event, 'needns'); 82 return false; 83 } 84 85 //sort order 86 $order = $INPUT->str('pdfns_order', 'natural', true); 87 $sortoptions = array('pagename', 'date', 'natural'); 88 if(!in_array($order, $sortoptions)) { 89 $order = 'natural'; 90 } 91 92 //search depth 93 $depth = $INPUT->int('pdfns_depth', 0); 94 if($depth < 0) { 95 $depth = 0; 96 } 97 98 //page search 99 $result = array(); 100 $opts = array('depth' => $depth); //recursive all levels 101 $dir = utf8_encodeFN(str_replace(':', '/', $pdfnamespace)); 102 search($result, $conf['datadir'], 'search_allpages', $opts, $dir); 103 104 //sorting 105 if(count($result) > 0) { 106 if($order == 'date') { 107 usort($result, array($this, '_datesort')); 108 } elseif($order == 'pagename') { 109 usort($result, array($this, '_pagenamesort')); 110 } 111 } 112 113 foreach($result as $item) { 114 $this->list[] = $item['id']; 115 } 116 117 } elseif(isset($_COOKIE['list-pagelist']) && !empty($_COOKIE['list-pagelist'])) { 118 //is in Bookmanager of bookcreator plugin a title given? 119 if(!$title = $INPUT->str('pdfbook_title')) { //TODO when title is changed, the cached file contains the old title 120 $this->showPageWithErrorMsg($event, 'needtitle'); 121 return false; 122 } else { 123 $this->list = explode("|", $_COOKIE['list-pagelist']); 124 } 125 126 } else { 127 //show empty bookcreator message 128 $this->showPageWithErrorMsg($event, 'empty'); 129 return false; 130 } 131 132 // it's ours, no one else's 133 $event->preventDefault(); 134 135 // decide on the paper setup from param or config 136 $pagesize = $INPUT->str('pagesize', $this->getConf('pagesize'), true); 137 $orientation = $INPUT->str('orientation', $this->getConf('orientation'), true); 138 139 // prepare cache 140 $cache = new cache(join(',', $this->list) . $REV . $this->tpl . $pagesize . $orientation, '.dw2.pdf'); 141 $depends['files'] = array_map('wikiFN', $this->list); 142 $depends['files'][] = __FILE__; 143 $depends['files'][] = dirname(__FILE__) . '/renderer.php'; 144 $depends['files'][] = dirname(__FILE__) . '/mpdf/mpdf.php'; 145 $depends['files'] = array_merge($depends['files'], getConfigFiles('main')); 146 147 // hard work only when no cache available 148 if(!$this->getConf('usecache') || !$cache->useCache($depends)) { 149 // debug enabled? 150 $debug = $conf['allowdebug'] && isset($_GET['debughtml']); 151 152 // initialize PDF library 153 require_once(dirname(__FILE__) . "/DokuPDF.class.php"); 154 155 $mpdf = new DokuPDF($pagesize, $orientation); 156 157 // let mpdf fix local links 158 $self = parse_url(DOKU_URL); 159 $url = $self['scheme'] . '://' . $self['host']; 160 if($self['port']) $url .= ':' . $self['port']; 161 $mpdf->setBasePath($url); 162 163 // Set the title 164 $mpdf->SetTitle($title); 165 166 // some default settings 167 $mpdf->mirrorMargins = 1; 168 $mpdf->useOddEven = 1; 169 $mpdf->setAutoTopMargin = 'stretch'; 170 $mpdf->setAutoBottomMargin = 'stretch'; 171 172 // load the template 173 $template = $this->load_template($title); 174 175 // prepare HTML header styles 176 $html = ''; 177 if($debug) { 178 $html .= '<html><head>'; 179 $html .= '<style type="text/css">'; 180 } 181 $styles = $this->load_css(); 182 $styles .= '@page { size:auto; ' . $template['page'] . '}'; 183 $styles .= '@page :first {' . $template['first'] . '}'; 184 $mpdf->WriteHTML($styles, 1); 185 186 if($debug) { 187 $html .= $styles; 188 $html .= '</style>'; 189 $html .= '</head><body>'; 190 } 191 192 $body_start = $template['html']; 193 $body_start .= '<div class="dokuwiki">'; 194 195 // insert the cover page 196 $body_start .= $template['cover']; 197 198 $mpdf->WriteHTML($body_start, 2, true, false); //start body html 199 if($debug) { 200 $html .= $body_start; 201 } 202 203 // store original pageid 204 $keep = $ID; 205 206 // loop over all pages 207 $cnt = count($this->list); 208 for($n = 0; $n < $cnt; $n++) { 209 $page = $this->list[$n]; 210 211 // set global pageid to the rendered page 212 $ID = $page; 213 214 $pagehtml = p_cached_output(wikiFN($page, $REV), 'dw2pdf', $page); 215 $pagehtml .= $this->page_depend_replacements($template['cite'], cleanID($page)); 216 if($n < ($cnt - 1)) { 217 $pagehtml .= '<pagebreak />'; 218 } 219 220 $mpdf->WriteHTML($pagehtml, 2, false, false); //intermediate body html 221 if($debug) { 222 $html .= $pagehtml; 223 } 224 } 225 //restore ID 226 $ID = $keep; 227 228 // insert the back page 229 $body_end = $template['back']; 230 231 $body_end .= '</div>'; 232 233 $mpdf->WriteHTML($body_end, 2, false, true); // end body html 234 if($debug) { 235 $html .= $body_end; 236 $html .= '</body>'; 237 $html .= '</html>'; 238 } 239 240 //Return html for debugging 241 if($debug) { 242 if($_GET['debughtml'] == 'html') { 243 echo $html; 244 } else { 245 header('Content-Type: text/plain; charset=utf-8'); 246 echo $html; 247 } 248 exit(); 249 }; 250 251 // write to cache file 252 $mpdf->Output($cache->cache, 'F'); 253 } 254 255 // deliver the file 256 header('Content-Type: application/pdf'); 257 header('Cache-Control: must-revalidate, no-transform, post-check=0, pre-check=0'); 258 header('Pragma: public'); 259 http_conditionalRequest(filemtime($cache->cache)); 260 261 $filename = rawurlencode(cleanID(strtr($title, ':/;"', ' '))); 262 if($this->getConf('output') == 'file') { 263 header('Content-Disposition: attachment; filename="' . $filename . '.pdf";'); 264 } else { 265 header('Content-Disposition: inline; filename="' . $filename . '.pdf";'); 266 } 267 268 //try to send file, and exit if done 269 http_sendfile($cache->cache); 270 271 $fp = @fopen($cache->cache, "rb"); 272 if($fp) { 273 http_rangeRequest($fp, filesize($cache->cache), 'application/pdf'); 274 } else { 275 header("HTTP/1.0 500 Internal Server Error"); 276 print "Could not read file - bad permissions?"; 277 } 278 exit(); 279 } 280 281 /** 282 * Add 'export pdf'-button to pagetools 283 * 284 * @param Doku_Event $event 285 * @param mixed $param not defined 286 */ 287 public function addbutton(&$event, $param) { 288 global $ID, $REV; 289 290 if($this->getConf('showexportbutton') && $event->data['view'] == 'main') { 291 $params = array('do' => 'export_pdf'); 292 if($REV) $params['rev'] = $REV; 293 294 // insert button at position before last (up to top) 295 $event->data['items'] = array_slice($event->data['items'], 0, -1, true) + 296 array('export_pdf' => 297 '<li>' 298 . '<a href=' . wl($ID, $params) . ' class="action export_pdf" rel="nofollow" title="' . $this->getLang('export_pdf_button') . '">' 299 . '<span>' . $this->getLang('export_pdf_button') . '</span>' 300 . '</a>' 301 . '</li>' 302 ) + 303 array_slice($event->data['items'], -1, 1, true); 304 } 305 } 306 307 /** 308 * Load the various template files and prepare the HTML/CSS for insertion 309 */ 310 protected function load_template($title) { 311 global $ID; 312 global $conf; 313 $tpl = $this->tpl; 314 315 // this is what we'll return 316 $output = array( 317 'cover' => '', 318 'html' => '', 319 'page' => '', 320 'first' => '', 321 'cite' => '', 322 ); 323 324 // prepare header/footer elements 325 $html = ''; 326 foreach(array('header', 'footer') as $section) { 327 foreach(array('', '_odd', '_even', '_first') as $order) { 328 $file = DOKU_PLUGIN . 'dw2pdf/tpl/' . $tpl . '/' . $section . $order . '.html'; 329 if(file_exists($file)) { 330 $html .= '<htmlpage' . $section . ' name="' . $section . $order . '">' . DOKU_LF; 331 $html .= file_get_contents($file) . DOKU_LF; 332 $html .= '</htmlpage' . $section . '>' . DOKU_LF; 333 334 // register the needed pseudo CSS 335 if($order == '_first') { 336 $output['first'] .= $section . ': html_' . $section . $order . ';' . DOKU_LF; 337 } elseif($order == '_even') { 338 $output['page'] .= 'even-' . $section . '-name: html_' . $section . $order . ';' . DOKU_LF; 339 } elseif($order == '_odd') { 340 $output['page'] .= 'odd-' . $section . '-name: html_' . $section . $order . ';' . DOKU_LF; 341 } else { 342 $output['page'] .= $section . ': html_' . $section . $order . ';' . DOKU_LF; 343 } 344 } 345 } 346 } 347 348 // prepare replacements 349 $replace = array( 350 '@PAGE@' => '{PAGENO}', 351 '@PAGES@' => '{nb}', 352 '@TITLE@' => hsc($title), 353 '@WIKI@' => $conf['title'], 354 '@WIKIURL@' => DOKU_URL, 355 '@DATE@' => dformat(time()), 356 '@BASE@' => DOKU_BASE, 357 '@TPLBASE@' => DOKU_BASE . 'lib/plugins/dw2pdf/tpl/' . $tpl . '/' 358 ); 359 360 // set HTML element 361 $html = str_replace(array_keys($replace), array_values($replace), $html); 362 //TODO For bookcreator $ID (= bookmanager page) makes no sense 363 $output['html'] = $this->page_depend_replacements($html, $ID); 364 365 // cover page 366 $coverfile = DOKU_PLUGIN . 'dw2pdf/tpl/' . $tpl . '/cover.html'; 367 if(file_exists($coverfile)) { 368 $output['cover'] = file_get_contents($coverfile); 369 $output['cover'] = str_replace(array_keys($replace), array_values($replace), $output['cover']); 370 $output['cover'] .= '<pagebreak />'; 371 } 372 373 // cover page 374 $backfile = DOKU_PLUGIN . 'dw2pdf/tpl/' . $tpl . '/back.html'; 375 if(file_exists($backfile)) { 376 $output['back'] = '<pagebreak />'; 377 $output['back'] .= file_get_contents($backfile); 378 $output['back'] = str_replace(array_keys($replace), array_values($replace), $output['back']); 379 } 380 381 // citation box 382 $citationfile = DOKU_PLUGIN . 'dw2pdf/tpl/' . $tpl . '/citation.html'; 383 if(file_exists($citationfile)) { 384 $output['cite'] = file_get_contents($citationfile); 385 $output['cite'] = str_replace(array_keys($replace), array_values($replace), $output['cite']); 386 } 387 388 return $output; 389 } 390 391 /** 392 * @param string $raw code with placeholders 393 * @param string $id pageid 394 * @return string 395 */ 396 protected function page_depend_replacements($raw, $id) { 397 global $REV; 398 399 // generate qr code for this page using google infographics api 400 $qr_code = ''; 401 if($this->getConf('qrcodesize')) { 402 $url = urlencode(wl($id, '', '&', true)); 403 $qr_code = '<img src="https://chart.googleapis.com/chart?chs=' . 404 $this->getConf('qrcodesize') . '&cht=qr&chl=' . $url . '" />'; 405 } 406 // prepare replacements 407 $replace['@ID@'] = $id; 408 $replace['@UPDATE@'] = dformat(filemtime(wikiFN($id, $REV))); 409 $replace['@PAGEURL@'] = wl($id, ($REV) ? array('rev' => $REV) : false, true, "&"); 410 $replace['@QRCODE@'] = $qr_code; 411 412 return str_replace(array_keys($replace), array_values($replace), $raw); 413 } 414 415 /** 416 * Load all the style sheets and apply the needed replacements 417 */ 418 protected function load_css() { 419 global $conf; 420 //reusue the CSS dispatcher functions without triggering the main function 421 define('SIMPLE_TEST', 1); 422 require_once(DOKU_INC . 'lib/exe/css.php'); 423 424 // prepare CSS files 425 $files = array_merge( 426 array( 427 DOKU_INC . 'lib/styles/screen.css' 428 => DOKU_BASE . 'lib/styles/', 429 DOKU_INC . 'lib/styles/print.css' 430 => DOKU_BASE . 'lib/styles/', 431 ), 432 css_pluginstyles('all'), 433 $this->css_pluginPDFstyles(), 434 array( 435 DOKU_PLUGIN . 'dw2pdf/conf/style.css' 436 => DOKU_BASE . 'lib/plugins/dw2pdf/conf/', 437 DOKU_PLUGIN . 'dw2pdf/tpl/' . $this->tpl . '/style.css' 438 => DOKU_BASE . 'lib/plugins/dw2pdf/tpl/' . $this->tpl . '/', 439 DOKU_PLUGIN . 'dw2pdf/conf/style.local.css' 440 => DOKU_BASE . 'lib/plugins/dw2pdf/conf/', 441 ) 442 ); 443 $css = ''; 444 foreach($files as $file => $location) { 445 $display = str_replace(fullpath(DOKU_INC), '', fullpath($file)); 446 $css .= "\n/* XXXXXXXXX $display XXXXXXXXX */\n"; 447 $css .= css_loadfile($file, $location); 448 } 449 450 if(function_exists('css_parseless')) { 451 // apply pattern replacements 452 $styleini = css_styleini($conf['template']); 453 $css = css_applystyle($css, $styleini['replacements']); 454 455 // parse less 456 $css = css_parseless($css); 457 } else { 458 // @deprecated 2013-12-19: fix backward compatibility 459 $css = css_applystyle($css, DOKU_INC . 'lib/tpl/' . $conf['template'] . '/'); 460 } 461 462 return $css; 463 } 464 465 /** 466 * Returns a list of possible Plugin PDF Styles 467 * 468 * Checks for a pdf.css, falls back to print.css 469 * 470 * @author Andreas Gohr <andi@splitbrain.org> 471 */ 472 protected function css_pluginPDFstyles() { 473 $list = array(); 474 $plugins = plugin_list(); 475 476 $usestyle = explode(',', $this->getConf('usestyles')); 477 foreach($plugins as $p) { 478 if(in_array($p, $usestyle)) { 479 $list[DOKU_PLUGIN . "$p/screen.css"] = DOKU_BASE . "lib/plugins/$p/"; 480 $list[DOKU_PLUGIN . "$p/style.css"] = DOKU_BASE . "lib/plugins/$p/"; 481 } 482 483 if(file_exists(DOKU_PLUGIN . "$p/pdf.css")) { 484 $list[DOKU_PLUGIN . "$p/pdf.css"] = DOKU_BASE . "lib/plugins/$p/"; 485 } else { 486 $list[DOKU_PLUGIN . "$p/print.css"] = DOKU_BASE . "lib/plugins/$p/"; 487 } 488 } 489 return $list; 490 } 491 492 /** 493 * Returns array of pages which will be included in the exported pdf 494 * 495 * @return array 496 */ 497 public function getExportedPages() { 498 return $this->list; 499 } 500 501 /** 502 * usort callback to sort by file lastmodified time 503 */ 504 public function _datesort($a, $b) { 505 if($b['rev'] < $a['rev']) return -1; 506 if($b['rev'] > $a['rev']) return 1; 507 return strcmp($b['id'], $a['id']); 508 } 509 510 /** 511 * usort callback to sort by page id 512 */ 513 public function _pagenamesort($a, $b) { 514 if($a['id'] <= $b['id']) return -1; 515 if($a['id'] > $b['id']) return 1; 516 return 0; 517 } 518 519 /** 520 * Set error notification and reload page again 521 * 522 * @param Doku_Event $event 523 * @param string $msglangkey key of translation key 524 */ 525 private function showPageWithErrorMsg(&$event, $msglangkey) { 526 msg($this->getLang($msglangkey), -1); 527 528 $event->data = 'show'; 529 $_SERVER['REQUEST_METHOD'] = 'POST'; //clears url 530 } 531} 532