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
12 if(!defined('DOKU_INC')) die();
13 
14 use dokuwiki\Action\Exception\ActionException;
15 use 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  */
24 class 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