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