xref: /plugin/dw2pdf/action.php (revision e53f1ec01e4586e0076da6bcb0b944b8f646cbe8)
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     * Settings for current export, collected from url param, plugin config, global config
22     *
23     * @var array
24     */
25    protected $exportConfig = null;
26    protected $tpl;
27    protected $title;
28    protected $list = array();
29
30    /**
31     * Constructor. Sets the correct template
32     *
33     * @param string $title
34     */
35    public function __construct($title=null) {
36        $this->tpl   = $this->getExportConfig('template');
37        $this->title = $title ? $title : '';
38    }
39
40    /**
41     * Register the events
42     *
43     * @param Doku_Event_Handler $controller
44     */
45    public function register(Doku_Event_Handler $controller) {
46        $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'convert', array());
47        $controller->register_hook('TEMPLATE_PAGETOOLS_DISPLAY', 'BEFORE', $this, 'addbutton', array());
48    }
49
50    /**
51     * Do the HTML to PDF conversion work
52     *
53     * @param Doku_Event $event
54     * @return bool
55     */
56    public function convert(Doku_Event $event) {
57        global $ACT;
58        global $ID;
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        if($data = $this->collectExportPages($event)) {
67            list($this->title, $this->list) = $data;
68        } else {
69            return false;
70        }
71
72        // it's ours, no one else's
73        $event->preventDefault();
74
75        // prepare cache and its dependencies
76        $depends = array();
77        $cache = $this->prepareCache($depends);
78
79        // hard work only when no cache available or needed for debugging
80        if(!$this->getConf('usecache') || $this->getExportConfig('isDebug') || !$cache->useCache($depends)) {
81            // generating the pdf may take a long time for larger wikis / namespaces with many pages
82            set_time_limit(0);
83            try {
84                $this->generatePDF($cache->cache);
85            } catch (Mpdf\MpdfException $e) {
86                //prevent act_export()
87                $ACT = 'show';
88                msg($e->getMessage(), -1);
89                return false;
90            }
91
92        }
93
94        // deliver the file
95        $this->sendPDFFile($cache->cache);
96        return true;
97    }
98
99    /**
100     * Obtain list of pages and title, based on url parameters
101     *
102     * @param Doku_Event $event
103     * @return string|bool
104     */
105    protected function collectExportPages(Doku_Event $event) {
106        global $ACT;
107        global $ID;
108        global $INPUT;
109        global $conf;
110
111        // list of one or multiple pages
112        $list = array();
113
114        if($ACT == 'export_pdf') {
115            $list[0] = $ID;
116            $this->title = $INPUT->str('pdftitle'); //DEPRECATED
117            $this->title = $INPUT->str('book_title', $this->title, true);
118            if(empty($this->title)) {
119                $this->title = p_get_first_heading($ID);
120            }
121
122        } elseif($ACT == 'export_pdfns') {
123            //check input for title and ns
124            if(!$this->title = $INPUT->str('book_title')) {
125                $this->showPageWithErrorMsg($event, 'needtitle');
126                return false;
127            }
128            $pdfnamespace = cleanID($INPUT->str('book_ns'));
129            if(!@is_dir(dirname(wikiFN($pdfnamespace . ':dummy')))) {
130                $this->showPageWithErrorMsg($event, 'needns');
131                return false;
132            }
133
134            //sort order
135            $order = $INPUT->str('book_order', 'natural', true);
136            $sortoptions = array('pagename', 'date', 'natural');
137            if(!in_array($order, $sortoptions)) {
138                $order = 'natural';
139            }
140
141            //search depth
142            $depth = $INPUT->int('book_nsdepth', 0);
143            if($depth < 0) {
144                $depth = 0;
145            }
146
147            //page search
148            $result = array();
149            $opts = array('depth' => $depth); //recursive all levels
150            $dir = utf8_encodeFN(str_replace(':', '/', $pdfnamespace));
151            search($result, $conf['datadir'], 'search_allpages', $opts, $dir);
152
153            //sorting
154            if(count($result) > 0) {
155                if($order == 'date') {
156                    usort($result, array($this, '_datesort'));
157                } elseif($order == 'pagename') {
158                    usort($result, array($this, '_pagenamesort'));
159                }
160            }
161
162            foreach($result as $item) {
163                $list[] = $item['id'];
164            }
165
166            if ($pdfnamespace !== '') {
167                if (!in_array($pdfnamespace . ':' . $conf['start'], $list, true)) {
168                    if (file_exists(wikiFN(rtrim($pdfnamespace,':')))) {
169                        array_unshift($list,rtrim($pdfnamespace,':'));
170                    }
171                }
172            }
173
174        } elseif(isset($_COOKIE['list-pagelist']) && !empty($_COOKIE['list-pagelist'])) {
175            /** @deprecated  April 2016 replaced by localStorage version of Bookcreator*/
176            //is in Bookmanager of bookcreator plugin a title given?
177            $this->title = $INPUT->str('pdfbook_title'); //DEPRECATED
178            $this->title = $INPUT->str('book_title', $this->title, true);
179            if(empty($this->title)) {
180                $this->showPageWithErrorMsg($event, 'needtitle');
181                return false;
182            } else {
183                $list = explode("|", $_COOKIE['list-pagelist']);
184            }
185
186        } elseif($INPUT->has('selection')) {
187            //handle Bookcreator requests based at localStorage
188//            if(!checkSecurityToken()) {
189//                http_status(403);
190//                print $this->getLang('empty');
191//                exit();
192//            }
193
194            $json = new JSON(JSON_LOOSE_TYPE);
195            $list = $json->decode($INPUT->post->str('selection', '', true));
196            if(!is_array($list) || empty($list)) {
197                http_status(400);
198                print $this->getLang('empty');
199                exit();
200            }
201
202            $this->title = $INPUT->str('pdfbook_title'); //DEPRECATED
203            $this->title = $INPUT->str('book_title', $this->title, true);
204            if(empty($this->title)) {
205                http_status(400);
206                print $this->getLang('needtitle');
207                exit();
208            }
209
210        } else {
211            //show empty bookcreator message
212            $this->showPageWithErrorMsg($event, 'empty');
213            return false;
214        }
215
216        $list = array_map('cleanID', $list);
217
218        $skippedpages = array();
219        foreach($list as $index => $pageid) {
220            if(auth_quickaclcheck($pageid) < AUTH_READ) {
221                $skippedpages[] = $pageid;
222                unset($list[$index]);
223            }
224        }
225        $list = array_filter($list); //removes also pages mentioned '0'
226
227        //if selection contains forbidden pages throw (overridable) warning
228        if(!$INPUT->bool('book_skipforbiddenpages') && !empty($skippedpages)) {
229            $msg = hsc(join(', ', $skippedpages));
230            if($INPUT->has('selection')) {
231                http_status(400);
232                print sprintf($this->getLang('forbidden'), $msg);
233                exit();
234            } else {
235                $this->showPageWithErrorMsg($event, 'forbidden', $msg);
236                return false;
237            }
238
239        }
240
241        return array($this->title, $list);
242    }
243
244    /**
245     * Get $meta['relations'] for the given page and revision
246     *
247     * @param        $id
248     * @param string $rev
249     * @return mixed
250     */
251    protected function getMetaRelation($id, $rev='') {
252        //current revision
253        if ($rev == '') return p_get_metadata($id, 'relation');
254
255        // get instructions
256        $instructions = p_cached_instructions(wikiFN($id, $rev),false,$id);
257
258        // set up the renderer
259        $renderer = new Doku_Renderer_metadata();
260
261        // loop through the instructions
262        foreach ($instructions as $instruction){
263            //execute only relation['media'] and relation['haspart'] functions
264            if ($instruction[0] != 'locallink' &&
265                $instruction[0] != 'internallink' &&
266                $instruction[0] != 'externallink' &&
267                $instruction[0] != 'interwikilink' &&
268                $instruction[0] != 'windowssharelink' &&
269                $instruction[0] != 'emaillink' &&
270                $instruction[0] != 'internalmedia' &&
271                $instruction[0] != 'rss') continue;
272
273            // execute the callback against the renderer
274            call_user_func_array(array(&$renderer, $instruction[0]), (array) $instruction[1]);
275        }
276
277        return $renderer->meta['relation'];
278    }
279
280    /**
281     * Prepare cache
282     *
283     * @param array  $depends (reference) array with dependencies
284     * @return cache
285     */
286    protected function prepareCache(&$depends) {
287        global $REV, $DATE_AT, $ACT;
288
289        if ($ACT == 'export_pdf') { //only one page is exported
290            $rev = $REV;
291            $date_at = $DATE_AT;
292        } else { //we are exporting entre namespace, ommit revisions
293            $rev = $date_at = '';
294        }
295
296        $cachekey = join(',', $this->list)
297            . $rev
298            . $date_at
299            . $this->getExportConfig('template')
300            . $this->getExportConfig('pagesize')
301            . $this->getExportConfig('orientation')
302            . $this->getExportConfig('font-size')
303            . $this->getExportConfig('doublesided')
304            . ($this->getExportConfig('hasToC') ? join('-', $this->getExportConfig('levels')) : '0')
305            . $this->title;
306        $cache = new cache($cachekey, '.dw2.pdf');
307
308        $dependencies = array();
309        foreach($this->list as $pageid) {
310            $relations = $this->getMetaRelation($pageid, $rev);
311
312            if(is_array($relations)) {
313                if(array_key_exists('media', $relations) && is_array($relations['media'])) {
314                    foreach($relations['media'] as $mediaid => $exists) {
315                        if($exists) {
316                            $rev = '';
317                            if ($DATE_AT) {
318                                $medialog     = new MediaChangeLog($mediaid);
319                                $medialog_rev = $medialog->getLastRevisionAt($DATE_AT);
320                                if($medialog_rev !== false) {
321                                    $rev = $medialog_rev;
322                                }
323                            }
324
325                            $dependencies[] = mediaFN($mediaid, $rev);
326                        }
327                    }
328                }
329
330                if(array_key_exists('haspart', $relations) && is_array($relations['haspart'])) {
331                    foreach($relations['haspart'] as $part_pageid => $exists) {
332                        if($exists) {
333                            $dependencies[] = wikiFN($part_pageid);
334                        }
335                    }
336                }
337            }
338
339            $dependencies[] = metaFN($pageid, '.meta');
340        }
341
342        $depends['files'] = array_map('wikiFN', $this->list);
343        $depends['files'][] = __FILE__;
344        $depends['files'][] = dirname(__FILE__) . '/renderer.php';
345        $depends['files'][] = dirname(__FILE__) . '/mpdf/mpdf.php';
346        $depends['files'] = array_merge(
347            $depends['files'],
348            $dependencies,
349            getConfigFiles('main')
350        );
351        return $cache;
352    }
353
354    /**
355     * Set error notification and reload page again
356     *
357     * @param Doku_Event $event
358     * @param string $msglangkey key of translation key
359     * @param string $replacement
360     */
361    private function showPageWithErrorMsg(Doku_Event $event, $msglangkey, $replacement=null) {
362        if(empty($replacement)) {
363            $msg = $this->getLang($msglangkey);
364        } else {
365            $msg = sprintf($this->getLang($msglangkey), $replacement);
366        }
367        msg($msg, -1);
368
369        $event->data = 'show';
370        $_SERVER['REQUEST_METHOD'] = 'POST'; //clears url
371    }
372
373    /**
374     * Returns the parsed Wikitext in dw2pdf for the given id and revision
375     *
376     * @param string     $id  page id
377     * @param string|int $rev revision timestamp or empty string
378     * @param string     $date_at
379     * @return null|string
380     */
381    protected function p_wiki_dw2pdf($id, $rev = '', $date_at = '') {
382        $file = wikiFN($id, $rev);
383
384        if(!file_exists($file)) return '';
385
386        //ensure $id is in global $ID (needed for parsing)
387        global $ID;
388        $keep = $ID;
389        $ID   = $id;
390
391        $ret  = '';
392
393        if($rev || $date_at) {
394            $ret = p_render('dw2pdf', p_get_instructions(io_readWikiPage($file, $id, $rev)), $info, $date_at); //no caching on old revisions
395        } else {
396            $ret = p_cached_output($file, 'dw2pdf', $id);
397        }
398
399        //restore ID (just in case)
400        $ID = $keep;
401
402        return $ret;
403    }
404
405    /**
406     * Build a pdf from the html
407     *
408     * @param string $cachefile
409     */
410    protected function generatePDF($cachefile) {
411        global $ID, $REV, $INPUT, $DATE_AT, $ACT;
412
413        if ($ACT == 'export_pdf') { //only one page is exported
414            $rev = $REV;
415            $date_at = $DATE_AT;
416        } else { //we are exporting entre namespace, ommit revisions
417            $rev = $date_at = '';
418        }
419
420        //some shortcuts to export settings
421        $hasToC = $this->getExportConfig('hasToC');
422        $levels = $this->getExportConfig('levels');
423        $isDebug = $this->getExportConfig('isDebug');
424
425        // initialize PDF library
426        require_once(dirname(__FILE__) . "/DokuPDF.class.php");
427
428        $mpdf = new DokuPDF($this->getExportConfig('pagesize'),
429                            $this->getExportConfig('orientation'),
430                            $this->getExportConfig('font-size'));
431
432        // let mpdf fix local links
433        $self = parse_url(DOKU_URL);
434        $url = $self['scheme'] . '://' . $self['host'];
435        if($self['port']) {
436            $url .= ':' . $self['port'];
437        }
438        $mpdf->setBasePath($url);
439
440        // Set the title
441        $mpdf->SetTitle($this->title);
442
443        // some default document settings
444        //note: double-sided document, starts at an odd page (first page is a right-hand side page)
445        //      single-side document has only odd pages
446        $mpdf->mirrorMargins = $this->getExportConfig('doublesided');
447        $mpdf->setAutoTopMargin = 'stretch';
448        $mpdf->setAutoBottomMargin = 'stretch';
449//            $mpdf->pagenumSuffix = '/'; //prefix for {nbpg}
450        if($hasToC) {
451            $mpdf->PageNumSubstitutions[] = array('from' => 1, 'reset' => 0, 'type' => 'i', 'suppress' => 'off'); //use italic pageno until ToC
452            $mpdf->h2toc = $levels;
453        } else {
454            $mpdf->PageNumSubstitutions[] = array('from' => 1, 'reset' => 0, 'type' => '1', 'suppress' => 'off');
455        }
456
457        // load the template
458        $template = $this->load_template();
459
460        // prepare HTML header styles
461        $html = '';
462        if($isDebug) {
463            $html .= '<html><head>';
464            $html .= '<style type="text/css">';
465        }
466
467        $styles = '@page { size:auto; ' . $template['page'] . '}';
468        $styles .= '@page :first {' . $template['first'] . '}';
469
470        $styles .= '@page landscape-page { size:landscape }';
471        $styles .= 'div.dw2pdf-landscape { page:landscape-page }';
472        $styles .= '@page portrait-page { size:portrait }';
473        $styles .= 'div.dw2pdf-portrait { page:portrait-page }';
474        $styles .= $this->load_css();
475
476        $mpdf->WriteHTML($styles, 1);
477
478        if($isDebug) {
479            $html .= $styles;
480            $html .= '</style>';
481            $html .= '</head><body>';
482        }
483
484        $body_start = $template['html'];
485        $body_start .= '<div class="dokuwiki">';
486
487        // insert the cover page
488        $body_start .= $template['cover'];
489
490        $mpdf->WriteHTML($body_start, 2, true, false); //start body html
491        if($isDebug) {
492            $html .= $body_start;
493        }
494        if($hasToC) {
495            //Note: - for double-sided document the ToC is always on an even number of pages, so that the following content is on a correct odd/even page
496            //      - first page of ToC starts always at odd page (so eventually an additional blank page is included before)
497            //      - there is no page numbering at the pages of the ToC
498            $mpdf->TOCpagebreakByArray(
499                array(
500                    'toc-preHTML' => '<h2>' . $this->getLang('tocheader') . '</h2>',
501                    'toc-bookmarkText' => $this->getLang('tocheader'),
502                    'links' => true,
503                    'outdent' => '1em',
504                    'resetpagenum' => true, //start pagenumbering after ToC
505                    'pagenumstyle' => '1'
506                )
507            );
508            $html .= '<tocpagebreak>';
509        }
510
511        // store original pageid
512        $keep = $ID;
513
514        // loop over all pages
515        $counter = 0;
516        $no_pages = count($this->list);
517        foreach($this->list as $page) {
518            $counter++;
519
520            // set global pageid to the rendered page
521            $ID = $page;
522
523            //$pagehtml = p_cached_output($filename, 'dw2pdf', $page);
524            $pagehtml = $this->p_wiki_dw2pdf($ID, $rev, $date_at);
525            //file doesn't exists
526            if($pagehtml == '') {
527                continue;
528            }
529            $pagehtml .= $this->page_depend_replacements($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        //restore ID
540        $ID = $keep;
541
542        // insert the back page
543        $body_end = $template['back'];
544
545        $body_end .= '</div>';
546
547        $mpdf->WriteHTML($body_end, 2, false, true); // finish body html
548        if($isDebug) {
549            $html .= $body_end;
550            $html .= '</body>';
551            $html .= '</html>';
552        }
553
554        //Return html for debugging
555        if($isDebug) {
556            if($INPUT->str('debughtml', 'text', true) == 'html') {
557                echo $html;
558            } else {
559                header('Content-Type: text/plain; charset=utf-8');
560                echo $html;
561            }
562            exit();
563        };
564
565        // write to cache file
566        $mpdf->Output($cachefile, 'F');
567    }
568
569    /**
570     * @param string $cachefile
571     */
572    protected function sendPDFFile($cachefile) {
573        header('Content-Type: application/pdf');
574        header('Cache-Control: must-revalidate, no-transform, post-check=0, pre-check=0');
575        header('Pragma: public');
576        http_conditionalRequest(filemtime($cachefile));
577
578        $filename = rawurlencode(cleanID(strtr($this->title, ':/;"', '    ')));
579        if($this->getConf('output') == 'file') {
580            header('Content-Disposition: attachment; filename="' . $filename . '.pdf";');
581        } else {
582            header('Content-Disposition: inline; filename="' . $filename . '.pdf";');
583        }
584
585        //Bookcreator uses jQuery.fileDownload.js, which requires a cookie.
586        header('Set-Cookie: fileDownload=true; path=/');
587
588        //try to send file, and exit if done
589        http_sendfile($cachefile);
590
591        $fp = @fopen($cachefile, "rb");
592        if($fp) {
593            http_rangeRequest($fp, filesize($cachefile), 'application/pdf');
594        } else {
595            header("HTTP/1.0 500 Internal Server Error");
596            print "Could not read file - bad permissions?";
597        }
598        exit();
599    }
600
601    /**
602     * Load the various template files and prepare the HTML/CSS for insertion
603     *
604     * @return array
605     */
606    protected function load_template() {
607        global $ID;
608        global $conf;
609
610        // this is what we'll return
611        $output = array(
612            'cover' => '',
613            'html'  => '',
614            'page'  => '',
615            'first' => '',
616            'cite'  => '',
617        );
618
619        // prepare header/footer elements
620        $html = '';
621        foreach(array('header', 'footer') as $section) {
622            foreach(array('', '_odd', '_even', '_first') as $order) {
623                $file = DOKU_PLUGIN . 'dw2pdf/tpl/' . $this->tpl . '/' . $section . $order . '.html';
624                if(file_exists($file)) {
625                    $html .= '<htmlpage' . $section . ' name="' . $section . $order . '">' . DOKU_LF;
626                    $html .= file_get_contents($file) . DOKU_LF;
627                    $html .= '</htmlpage' . $section . '>' . DOKU_LF;
628
629                    // register the needed pseudo CSS
630                    if($order == '_first') {
631                        $output['first'] .= $section . ': html_' . $section . $order . ';' . DOKU_LF;
632                    } elseif($order == '_even') {
633                        $output['page'] .= 'even-' . $section . '-name: html_' . $section . $order . ';' . DOKU_LF;
634                    } elseif($order == '_odd') {
635                        $output['page'] .= 'odd-' . $section . '-name: html_' . $section . $order . ';' . DOKU_LF;
636                    } else {
637                        $output['page'] .= $section . ': html_' . $section . $order . ';' . DOKU_LF;
638                    }
639                }
640            }
641        }
642
643        // prepare replacements
644        $replace = array(
645            '@PAGE@'    => '{PAGENO}',
646            '@PAGES@'   => '{nbpg}', //see also $mpdf->pagenumSuffix = ' / '
647            '@TITLE@'   => hsc($this->title),
648            '@WIKI@'    => $conf['title'],
649            '@WIKIURL@' => DOKU_URL,
650            '@DATE@'    => dformat(time()),
651            '@BASE@'    => DOKU_BASE,
652            '@TPLBASE@' => DOKU_BASE . 'lib/plugins/dw2pdf/tpl/' . $this->tpl . '/'
653        );
654
655        // set HTML element
656        $html = str_replace(array_keys($replace), array_values($replace), $html);
657        //TODO For bookcreator $ID (= bookmanager page) makes no sense
658        $output['html'] = $this->page_depend_replacements($html, $ID);
659
660        // cover page
661        $coverfile = DOKU_PLUGIN . 'dw2pdf/tpl/' . $this->tpl . '/cover.html';
662        if(file_exists($coverfile)) {
663            $output['cover'] = file_get_contents($coverfile);
664            $output['cover'] = str_replace(array_keys($replace), array_values($replace), $output['cover']);
665            $output['cover'] = $this->page_depend_replacements($output['cover'], $ID);
666            $output['cover'] .= '<pagebreak />';
667        }
668
669        // cover page
670        $backfile = DOKU_PLUGIN . 'dw2pdf/tpl/' . $this->tpl . '/back.html';
671        if(file_exists($backfile)) {
672            $output['back'] = '<pagebreak />';
673            $output['back'] .= file_get_contents($backfile);
674            $output['back'] = str_replace(array_keys($replace), array_values($replace), $output['back']);
675            $output['back'] = $this->page_depend_replacements($output['back'], $ID);
676        }
677
678        // citation box
679        $citationfile = DOKU_PLUGIN . 'dw2pdf/tpl/' . $this->tpl . '/citation.html';
680        if(file_exists($citationfile)) {
681            $output['cite'] = file_get_contents($citationfile);
682            $output['cite'] = str_replace(array_keys($replace), array_values($replace), $output['cite']);
683        }
684
685        return $output;
686    }
687
688    /**
689     * @param string $raw code with placeholders
690     * @param string $id  pageid
691     * @return string
692     */
693    protected function page_depend_replacements($raw, $id) {
694        global $REV, $DATE_AT;
695
696        // generate qr code for this page using google infographics api
697        $qr_code = '';
698        if($this->getConf('qrcodesize')) {
699            $url = urlencode(wl($id, '', '&', true));
700            $qr_code = '<img src="https://chart.googleapis.com/chart?chs=' .
701                $this->getConf('qrcodesize') . '&cht=qr&chl=' . $url . '" />';
702        }
703        // prepare replacements
704        $replace['@ID@']      = $id;
705        $replace['@UPDATE@']  = dformat(filemtime(wikiFN($id, $REV)));
706
707        $params = array();
708        if($DATE_AT) {
709            $params['at'] = $DATE_AT;
710        } elseif($REV) {
711            $params['rev'] = $REV;
712        }
713        $replace['@PAGEURL@'] = wl($id, $params, true, "&");
714        $replace['@QRCODE@']  = $qr_code;
715
716        $content = str_replace(array_keys($replace), array_values($replace), $raw);
717
718        // @DATE(<date>[, <format>])@
719        $content = preg_replace_callback(
720            '/@DATE\((.*?)(?:,\s*(.*?))?\)@/',
721            array($this, 'replacedate'),
722            $content
723        );
724
725        return $content;
726    }
727
728
729    /**
730     * (callback) Replace date by request datestring
731     * e.g. '%m(30-11-1975)' is replaced by '11'
732     *
733     * @param array $match with [0]=>whole match, [1]=> first subpattern, [2] => second subpattern
734     * @return string
735     */
736    function replacedate($match) {
737        global $conf;
738        //no 2nd argument for default date format
739        if($match[2] == null) {
740            $match[2] = $conf['dformat'];
741        }
742        return strftime($match[2], strtotime($match[1]));
743    }
744
745    /**
746     * Load all the style sheets and apply the needed replacements
747     */
748    protected function load_css() {
749        global $conf;
750        //reusue the CSS dispatcher functions without triggering the main function
751        define('SIMPLE_TEST', 1);
752        require_once(DOKU_INC . 'lib/exe/css.php');
753
754        // prepare CSS files
755        $files = array_merge(
756            array(
757                DOKU_INC . 'lib/styles/screen.css'
758                    => DOKU_BASE . 'lib/styles/',
759                DOKU_INC . 'lib/styles/print.css'
760                    => DOKU_BASE . 'lib/styles/',
761            ),
762            css_pluginstyles('all'),
763            $this->css_pluginPDFstyles(),
764            array(
765                DOKU_PLUGIN . 'dw2pdf/conf/style.css'
766                    => DOKU_BASE . 'lib/plugins/dw2pdf/conf/',
767                DOKU_PLUGIN . 'dw2pdf/tpl/' . $this->tpl . '/style.css'
768                    => DOKU_BASE . 'lib/plugins/dw2pdf/tpl/' . $this->tpl . '/',
769                DOKU_PLUGIN . 'dw2pdf/conf/style.local.css'
770                    => DOKU_BASE . 'lib/plugins/dw2pdf/conf/',
771            )
772        );
773        $css = '';
774        foreach($files as $file => $location) {
775            $display = str_replace(fullpath(DOKU_INC), '', fullpath($file));
776            $css .= "\n/* XXXXXXXXX $display XXXXXXXXX */\n";
777            $css .= css_loadfile($file, $location);
778        }
779
780        if(function_exists('css_parseless')) {
781            // apply pattern replacements
782            $styleini = css_styleini($conf['template']);
783            $css = css_applystyle($css, $styleini['replacements']);
784
785            // parse less
786            $css = css_parseless($css);
787        } else {
788            // @deprecated 2013-12-19: fix backward compatibility
789            $css = css_applystyle($css, DOKU_INC . 'lib/tpl/' . $conf['template'] . '/');
790        }
791
792        return $css;
793    }
794
795    /**
796     * Returns a list of possible Plugin PDF Styles
797     *
798     * Checks for a pdf.css, falls back to print.css
799     *
800     * @author Andreas Gohr <andi@splitbrain.org>
801     */
802    protected function css_pluginPDFstyles() {
803        $list = array();
804        $plugins = plugin_list();
805
806        $usestyle = explode(',', $this->getConf('usestyles'));
807        foreach($plugins as $p) {
808            if(in_array($p, $usestyle)) {
809                $list[DOKU_PLUGIN . "$p/screen.css"] = DOKU_BASE . "lib/plugins/$p/";
810                $list[DOKU_PLUGIN . "$p/style.css"] = DOKU_BASE . "lib/plugins/$p/";
811            }
812
813            if(file_exists(DOKU_PLUGIN . "$p/pdf.css")) {
814                $list[DOKU_PLUGIN . "$p/pdf.css"] = DOKU_BASE . "lib/plugins/$p/";
815            } else {
816                $list[DOKU_PLUGIN . "$p/print.css"] = DOKU_BASE . "lib/plugins/$p/";
817            }
818        }
819        return $list;
820    }
821
822    /**
823     * Returns array of pages which will be included in the exported pdf
824     *
825     * @return array
826     */
827    public function getExportedPages() {
828        return $this->list;
829    }
830
831    /**
832     * usort callback to sort by file lastmodified time
833     *
834     * @param array $a
835     * @param array $b
836     * @return int
837     */
838    public function _datesort($a, $b) {
839        if($b['rev'] < $a['rev']) return -1;
840        if($b['rev'] > $a['rev']) return 1;
841        return strcmp($b['id'], $a['id']);
842    }
843
844    /**
845     * usort callback to sort by page id
846     * @param array $a
847     * @param array $b
848     * @return int
849     */
850    public function _pagenamesort($a, $b) {
851        if($a['id'] <= $b['id']) return -1;
852        if($a['id'] > $b['id']) return 1;
853        return 0;
854    }
855
856    /**
857     * Return settings read from:
858     *   1. url parameters
859     *   2. plugin config
860     *   3. global config
861     *
862     * @return array
863     */
864    protected function loadExportConfig() {
865        global $INPUT;
866        global $conf;
867
868        $this->exportConfig = array();
869
870        // decide on the paper setup from param or config
871        $this->exportConfig['pagesize'] = $INPUT->str('pagesize', $this->getConf('pagesize'), true);
872        $this->exportConfig['orientation'] = $INPUT->str('orientation', $this->getConf('orientation'), true);
873
874        // decide on the font-size from param or config
875        $this->exportConfig['font-size'] = $INPUT->str('font-size', $this->getConf('font-size'), true);
876
877        $doublesided = $INPUT->bool('doublesided', (bool) $this->getConf('doublesided'));
878        $this->exportConfig['doublesided'] = $doublesided ? '1' : '0';
879
880        $hasToC = $INPUT->bool('toc', (bool) $this->getConf('toc'));
881        $levels = array();
882        if($hasToC) {
883            $toclevels = $INPUT->str('toclevels', $this->getConf('toclevels'), true);
884            list($top_input, $max_input) = explode('-', $toclevels, 2);
885            list($top_conf, $max_conf) = explode('-', $this->getConf('toclevels'), 2);
886            $bounds_input = array(
887                'top' => array(
888                    (int) $top_input,
889                    (int) $top_conf
890                ),
891                'max' => array(
892                    (int) $max_input,
893                    (int) $max_conf
894                )
895            );
896            $bounds = array(
897                'top' => $conf['toptoclevel'],
898                'max' => $conf['maxtoclevel']
899
900            );
901            foreach($bounds_input as $bound => $values) {
902                foreach($values as $value) {
903                    if($value > 0 && $value <= 5) {
904                        //stop at valid value and store
905                        $bounds[$bound] = $value;
906                        break;
907                    }
908                }
909            }
910
911            if($bounds['max'] < $bounds['top']) {
912                $bounds['max'] = $bounds['top'];
913            }
914
915            for($level = $bounds['top']; $level <= $bounds['max']; $level++) {
916                $levels["H$level"] = $level - 1;
917            }
918        }
919        $this->exportConfig['hasToC'] = $hasToC;
920        $this->exportConfig['levels'] = $levels;
921
922        $this->exportConfig['maxbookmarks'] = $INPUT->int('maxbookmarks', $this->getConf('maxbookmarks'), true);
923
924        $tplconf = $this->getConf('template');
925        $tpl = $INPUT->str('tpl', $tplconf, true);
926        if(!is_dir(DOKU_PLUGIN . 'dw2pdf/tpl/' . $tpl)) {
927            $tpl = $tplconf;
928        }
929        if(!$tpl){
930            $tpl = 'default';
931        }
932        $this->exportConfig['template'] = $tpl;
933
934        $this->exportConfig['isDebug'] = $conf['allowdebug'] && $INPUT->has('debughtml');
935    }
936
937    /**
938     * Returns requested config
939     *
940     * @param string $name
941     * @param mixed  $notset
942     * @return mixed|bool
943     */
944    public function getExportConfig($name, $notset = false) {
945        if ($this->exportConfig === null){
946            $this->loadExportConfig();
947        }
948
949        if(isset($this->exportConfig[$name])){
950            return $this->exportConfig[$name];
951        }else{
952            return $notset;
953        }
954    }
955
956    /**
957     * Add 'export pdf'-button to pagetools
958     *
959     * @param Doku_Event $event
960     */
961    public function addbutton(Doku_Event $event) {
962        global $ID, $REV, $DATE_AT;
963
964        if($this->getConf('showexportbutton') && $event->data['view'] == 'main') {
965            $params = array('do' => 'export_pdf');
966            if($DATE_AT) {
967                $params['at'] = $DATE_AT;
968            } elseif($REV) {
969                $params['rev'] = $REV;
970            }
971
972            // insert button at position before last (up to top)
973            $event->data['items'] = array_slice($event->data['items'], 0, -1, true) +
974                array('export_pdf' =>
975                          '<li>'
976                          . '<a href="' . wl($ID, $params) . '"  class="action export_pdf" rel="nofollow" title="' . $this->getLang('export_pdf_button') . '">'
977                          . '<span>' . $this->getLang('export_pdf_button') . '</span>'
978                          . '</a>'
979                          . '</li>'
980                ) +
981                array_slice($event->data['items'], -1, 1, true);
982        }
983    }
984}
985