1<?php 2/** 3 * ODT export Plugin component. Mainly based at dw2pdf export action plugin component. 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 * @author Gerrit Uitslag <klapinklapin@gmail.com> 9 */ 10 11// must be run within Dokuwiki 12if(!defined('DOKU_INC')) die(); 13 14use dokuwiki\Action\Exception\ActionException; 15use dokuwiki\Action\Exception\ActionAbort; 16 17/** 18 * Class action_plugin_odt_export 19 * 20 * Collect pages and export these. GUI is available via bookcreator. 21 * 22 * @package DokuWiki\Action\Export 23 */ 24class action_plugin_odt_export extends DokuWiki_Action_Plugin { 25 protected $config = null; 26 27 /** 28 * @var array 29 */ 30 protected $list = array(); 31 32 /** 33 * Register the events 34 * 35 * @param Doku_Event_Handler $controller 36 */ 37 public function register(Doku_Event_Handler $controller) { 38 $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'convert', array()); 39 $controller->register_hook('TEMPLATE_PAGETOOLS_DISPLAY', 'BEFORE', $this, 'addbutton_odt', array()); 40 $controller->register_hook('TEMPLATE_PAGETOOLS_DISPLAY', 'BEFORE', $this, 'addbutton_pdf', array()); 41 $controller->register_hook('MENU_ITEMS_ASSEMBLY', 'AFTER', $this, 'addbutton_odt_new', array()); 42 $controller->register_hook('MENU_ITEMS_ASSEMBLY', 'AFTER', $this, 'addbutton_pdf_new', array()); 43 } 44 45 /** 46 * Add 'export odt'-button to pagetools 47 * 48 * @param Doku_Event $event 49 */ 50 public function addbutton_odt(Doku_Event $event) { 51 global $ID, $REV; 52 53 if($this->getConf('showexportbutton') && $event->data['view'] == 'main') { 54 $params = array('do' => 'export_odt'); 55 if($REV) { 56 $params['rev'] = $REV; 57 } 58 59 // insert button at position before last (up to top) 60 $event->data['items'] = array_slice($event->data['items'], 0, -1, true) + 61 array('export_odt' => 62 '<li>' 63 . '<a href="' . wl($ID, $params) . '" class="action export_odt" rel="nofollow" title="' . $this->getLang('export_odt_button') . '">' 64 . '<span>' . $this->getLang('export_odt_button') . '</span>' 65 . '</a>' 66 . '</li>' 67 ) + 68 array_slice($event->data['items'], -1, 1, true); 69 } 70 } 71 72 /** 73 * Add 'export odt=>pdf'-button to pagetools 74 * 75 * @param Doku_Event $event 76 */ 77 public function addbutton_pdf(Doku_Event $event) { 78 global $ID, $REV; 79 80 if($this->getConf('showpdfexportbutton') && $event->data['view'] == 'main') { 81 $params = array('do' => 'export_odt_pdf'); 82 if($REV) { 83 $params['rev'] = $REV; 84 } 85 86 // insert button at position before last (up to top) 87 $event->data['items'] = array_slice($event->data['items'], 0, -1, true) + 88 array('export_odt_pdf' => 89 '<li>' 90 . '<a href="' . wl($ID, $params) . '" class="action export_odt_pdf" rel="nofollow" title="' . $this->getLang('export_odt_pdf_button') . '">' 91 . '<span>' . $this->getLang('export_odt_pdf_button') . '</span>' 92 . '</a>' 93 . '</li>' 94 ) + 95 array_slice($event->data['items'], -1, 1, true); 96 } 97 } 98 99 /** 100 * Add 'export odt' button to page tools, new SVG based mechanism 101 * 102 * @param Doku_Event $event 103 */ 104 public function addbutton_odt_new(Doku_Event $event) { 105 if($event->data['view'] != 'page') return; 106 if($this->getConf('showexportbutton')) { 107 array_splice($event->data['items'], -1, 0, [new \dokuwiki\plugin\odt\MenuItemODT()]); 108 } 109 } 110 111 /** 112 * Add 'export odt pdf' button to page tools, new SVG based mechanism 113 * 114 * @param Doku_Event $event 115 */ 116 public function addbutton_pdf_new(Doku_Event $event) { 117 if($event->data['view'] != 'page') return; 118 if($this->getConf('showpdfexportbutton')) { 119 array_splice($event->data['items'], -1, 0, [new \dokuwiki\plugin\odt\MenuItemODTPDF()]); 120 } 121 } 122 123 /*********************************************************************************** 124 * Book export * 125 ***********************************************************************************/ 126 127 /** 128 * Do article(s) to ODT conversion work 129 * 130 * @param Doku_Event $event 131 * @return bool 132 */ 133 public function convert(Doku_Event $event) { 134 global $ID; 135 $format = NULL; 136 137 $action = act_clean($event->data); 138 139 // Any kind of ODT export? 140 $odt_export = false; 141 if (strncmp($action, 'export_odt', strlen('export_odt')) == 0) { 142 $odt_export = true; 143 } 144 145 // check conversion format 146 if ($odt_export && strpos($action, '_pdf') !== false) { 147 $format = 'pdf'; 148 } 149 150 // single page export: 151 // rename action to the actual renderer component 152 if($action == 'export_odt') { 153 $event->data = 'export_odt_page'; 154 } else if ($action == 'export_odt_pdf') { 155 $event->data = 'export_odt_pagepdf'; 156 } 157 158 if( !is_array($action) && $odt_export ) { 159 // On export to ODT load config helper if not done yet 160 // and stop on errors. 161 if ( !isset($this->config) ) { 162 $this->config = plugin_load('helper', 'odt_config'); 163 $this->config->load($warning); 164 165 if (!empty($warning)) { 166 $this->showPageWithErrorMsg($event, NULL, $warning); 167 return false; 168 } 169 } 170 $this->config->setConvertTo($format); 171 } 172 173 // the book export? 174 if(($action != 'export_odtbook') && ($action != 'export_odtns')) return false; 175 176 // check user's rights 177 if(auth_quickaclcheck($ID) < AUTH_READ) return false; 178 179 if($data = $this->collectExportPages($event)) { 180 list($title, $this->list) = $data; 181 } else { 182 return false; 183 } 184 185 // it's ours, no one else's 186 $event->preventDefault(); 187 188 // prepare cache and its dependencies 189 $depends = array(); 190 $cache = $this->prepareCache($title, $depends); 191 192 // hard work only when no cache available 193 if(!$this->getConf('usecache') || !$cache->useCache($depends)) { 194 $this->generateODT($cache->cache, $title); 195 } 196 197 // deliver the file 198 $this->sendODTFile($cache->cache, $title); 199 return true; 200 } 201 202 203 /** 204 * Obtain list of pages and title, based on url parameters 205 * 206 * @param Doku_Event $event 207 * @return string|bool 208 */ 209 protected function collectExportPages(Doku_Event $event) { 210 global $ID; 211 global $INPUT; 212 213 // Load config helper if not done yet 214 if ( !isset($this->config) ) { 215 $this->config = plugin_load('helper', 'odt_config'); 216 $this->config->load($warning); 217 } 218 219 // list of one or multiple pages 220 $list = array(); 221 222 $action = $event->data; 223 if($action == 'export_odt') { 224 $list[0] = $ID; 225 $title = $INPUT->str('book_title'); 226 if(!$title) { 227 $title = p_get_first_heading($ID); 228 } 229 230 } elseif($action == 'export_odtns') { 231 //check input for title and ns 232 if(!$title = $INPUT->str('book_title')) { 233 $this->showPageWithErrorMsg($event, 'needtitle'); 234 return false; 235 } 236 $docnamespace = cleanID($INPUT->str('book_ns')); 237 if(!@is_dir(dirname(wikiFN($docnamespace . ':dummy')))) { 238 $this->showPageWithErrorMsg($event, 'needns'); 239 return false; 240 } 241 242 //sort order 243 $order = $INPUT->str('book_order', 'natural', true); 244 $sortoptions = array('pagename', 'date', 'natural'); 245 if(!in_array($order, $sortoptions)) { 246 $order = 'natural'; 247 } 248 249 //search depth 250 $depth = $INPUT->int('book_nsdepth', 0); 251 if($depth < 0) { 252 $depth = 0; 253 } 254 255 //page search 256 $result = array(); 257 $opts = array('depth' => $depth); //recursive all levels 258 $dir = utf8_encodeFN(str_replace(':', '/', $docnamespace)); 259 search($result, $this->config->getParam('datadir'), 'search_allpages', $opts, $dir); 260 261 //sorting 262 if(count($result) > 0) { 263 if($order == 'date') { 264 usort($result, array($this, '_datesort')); 265 } elseif($order == 'pagename') { 266 usort($result, array($this, '_pagenamesort')); 267 } 268 } 269 270 foreach($result as $item) { 271 $list[] = $item['id']; 272 } 273 274 } elseif(isset($_COOKIE['list-pagelist']) && !empty($_COOKIE['list-pagelist'])) { 275 // Here is $action == 'export_odtbook' 276 277 /** @deprecated April 2016 replaced by localStorage version of Bookcreator*/ 278 279 //is in Bookmanager of bookcreator plugin a title given? 280 if(!$title = $INPUT->str('book_title')) { 281 $this->showPageWithErrorMsg($event, 'needtitle'); 282 return false; 283 } else { 284 $list = explode("|", $_COOKIE['list-pagelist']); 285 } 286 287 } elseif($INPUT->has('selection')) { 288 //handle Bookcreator requests based at localStorage 289// if(!checkSecurityToken()) { 290// http_status(403); 291// print $this->getLang('empty'); 292// exit(); 293// } 294 295 $json = new JSON(JSON_LOOSE_TYPE); 296 $list = $json->decode($INPUT->post->str('selection', '', true)); 297 if(!is_array($list) || empty($list)) { 298 http_status(400); 299 print $this->getLang('empty'); 300 exit(); 301 } 302 303 $title = $INPUT->str('pdfbook_title'); //DEPRECATED 304 $title = $INPUT->str('book_title', $title, true); 305 if(empty($title)) { 306 http_status(400); 307 print $this->getLang('needtitle'); 308 exit(); 309 } 310 311 } else { 312 //show empty bookcreator message 313 $this->showPageWithErrorMsg($event, 'empty'); 314 return false; 315 } 316 317 $list = array_map('cleanID', $list); 318 319 $skippedpages = array(); 320 foreach($list as $index => $pageid) { 321 if(auth_quickaclcheck($pageid) < AUTH_READ) { 322 $skippedpages[] = $pageid; 323 unset($list[$index]); 324 } 325 } 326 $list = array_filter($list); //removes also pages mentioned '0' 327 328 //if selection contains forbidden pages throw (overridable) warning 329 if(!$INPUT->bool('book_skipforbiddenpages') && !empty($skippedpages)) { 330 $msg = sprintf($this->getLang('forbidden'), hsc(join(', ', $skippedpages))); 331 if($INPUT->has('selection')) { 332 http_status(400); 333 print $msg; 334 exit(); 335 } else { 336 $this->showPageWithErrorMsg($event, null, $msg); 337 return false; 338 } 339 340 } 341 342 return array($title, $list); 343 } 344 345 346 /** 347 * Set error notification and reload page again 348 * 349 * @param Doku_Event $event 350 * @param string $msglangkey key of translation key 351 */ 352 private function showPageWithErrorMsg(Doku_Event $event, $msglangkey, $translatedMsg=NULL) { 353 if (!empty($msglangkey)) { 354 // Message need to be translated. 355 msg($this->getLang($msglangkey), -1); 356 } else { 357 // Message already has been translated. 358 msg($translatedMsg, -1); 359 } 360 361 $event->data = 'show'; 362 $_SERVER['REQUEST_METHOD'] = 'POST'; //clears url 363 } 364 365 /** 366 * Prepare cache 367 * 368 * @param string $title 369 * @param array $depends (reference) array with dependencies 370 * @return cache 371 */ 372 protected function prepareCache($title, &$depends) { 373 global $REV; 374 global $INPUT; 375 376 //different caches for varying config settings 377 $template = $this->getConf("odt_template"); 378 $template = $INPUT->get->str('odt_template', $template, true); 379 380 381 $cachekey = join(',', $this->list) 382 . $REV 383 . $template 384 . $title; 385 $cache = new cache($cachekey, '.odt'); 386 387 $dependencies = array(); 388 foreach($this->list as $pageid) { 389 $relations = p_get_metadata($pageid, 'relation'); 390 391 if(is_array($relations)) { 392 if(array_key_exists('media', $relations) && is_array($relations['media'])) { 393 foreach($relations['media'] as $mediaid => $exists) { 394 if($exists) { 395 $dependencies[] = mediaFN($mediaid); 396 } 397 } 398 } 399 400 if(array_key_exists('haspart', $relations) && is_array($relations['haspart'])) { 401 foreach($relations['haspart'] as $part_pageid => $exists) { 402 if($exists) { 403 $dependencies[] = wikiFN($part_pageid); 404 } 405 } 406 } 407 } 408 409 $dependencies[] = metaFN($pageid, '.meta'); 410 } 411 412 $depends['files'] = array_map('wikiFN', $this->list); 413 $depends['files'][] = __FILE__; 414 $depends['files'][] = dirname(__FILE__) . '/../renderer/page.php'; 415 $depends['files'][] = dirname(__FILE__) . '/../renderer/book.php'; 416 $depends['files'][] = dirname(__FILE__) . '/../plugin.info.txt'; 417 $depends['files'] = array_merge( 418 $depends['files'], 419 $dependencies, 420 getConfigFiles('main') 421 ); 422 return $cache; 423 } 424 425 /** 426 * Build a ODT from the articles 427 * 428 * @param string $cachefile 429 * @param string $title 430 */ 431 protected function generateODT($cachefile, $title) { 432 global $ID; 433 global $REV; 434 435 /** @var renderer_plugin_odt_book $odt */ 436 $odt = plugin_load('renderer','odt_book'); 437 438 // store original pageid 439 $keep = $ID; 440 441 // loop over all pages 442 $xmlcontent = ''; 443 foreach($this->list as $page) { 444 $filename = wikiFN($page, $REV); 445 446 if(!file_exists($filename)) { 447 continue; 448 } 449 // set global pageid to the rendered page 450 $ID = $page; 451 $xmlcontent .= p_render('odt_book', p_cached_instructions($filename, false, $page), $info); 452 } 453 454 //restore ID 455 $ID = $keep; 456 457 $odt->doc = $xmlcontent; 458 $odt->setTitle($title); 459 $odt->finalize_ODTfile(); 460 461 // write to cache file 462 io_savefile($cachefile, $odt->doc); 463 } 464 465 /** 466 * @param string $cachefile 467 * @param string $title 468 */ 469 protected function sendODTFile($cachefile, $title) { 470 header('Content-Type: application/vnd.oasis.opendocument.text'); 471 header('Cache-Control: must-revalidate, no-transform, post-check=0, pre-check=0'); 472 header('Pragma: public'); 473 http_conditionalRequest(filemtime($cachefile)); 474 475 $filename = rawurlencode(cleanID(strtr($title, ':/;"', ' '))); 476 if($this->getConf('output') == 'file') { 477 header('Content-Disposition: attachment; filename="' . $filename . '.odt";'); 478 } else { 479 header('Content-Disposition: inline; filename="' . $filename . '.odt";'); 480 } 481 482 //Bookcreator uses jQuery.fileDownload.js, which requires a cookie. 483 header('Set-Cookie: fileDownload=true; path=/'); 484 485 //try to send file, and exit if done 486 http_sendfile($cachefile); 487 488 $fp = @fopen($cachefile, "rb"); 489 if($fp) { 490 http_rangeRequest($fp, filesize($cachefile), 'application/vnd.oasis.opendocument.text'); 491 } else { 492 header("HTTP/1.0 500 Internal Server Error"); 493 print "Could not read file - bad permissions?"; 494 } 495 exit(); 496 } 497 498 /** 499 * Returns array of wiki pages which will be included in the exported document 500 * 501 * @return array 502 */ 503 public function getExportedPages() { 504 return $this->list; 505 } 506 507 /** 508 * usort callback to sort by file lastmodified time 509 * 510 * @param array $a 511 * @param array $b 512 * @return int 513 */ 514 public function _datesort($a, $b) { 515 if($b['rev'] < $a['rev']) return -1; 516 if($b['rev'] > $a['rev']) return 1; 517 return strcmp($b['id'], $a['id']); 518 } 519 520 /** 521 * usort callback to sort by page id 522 * 523 * @param array $a 524 * @param array $b 525 * @return int 526 */ 527 public function _pagenamesort($a, $b) { 528 if($a['id'] <= $b['id']) return -1; 529 if($a['id'] > $b['id']) return 1; 530 return 0; 531 } 532} 533