xref: /plugin/include/helper.php (revision 4052f2330b3eb783e8f2e17b858bce81cd6fdc0e)
1<?php
2/**
3 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
4 * @author     Esther Brunner <wikidesign@gmail.com>
5 * @author     Christopher Smith <chris@jalakai.co.uk>
6 * @author     Gina Häußge, Michael Klier <dokuwiki@chimeric.de>
7 */
8
9// must be run within Dokuwiki
10if (!defined('DOKU_INC')) die();
11
12if (!defined('DOKU_LF')) define('DOKU_LF', "\n");
13if (!defined('DOKU_TAB')) define('DOKU_TAB', "\t");
14if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC.'lib/plugins/');
15
16class helper_plugin_include extends DokuWiki_Plugin { // DokuWiki_Helper_Plugin
17
18    var $pages     = array();   // filechain of included pages
19    var $page      = array();   // associative array with data about the page to include
20    var $ins       = array();   // instructions array
21    var $doc       = '';        // the final output XHTML string
22    var $mode      = 'section'; // inclusion mode: 'page' or 'section'
23    var $clevel    = 0;         // current section level
24    var $firstsec  = 0;         // show first section only
25    var $editbtn   = 1;         // show edit button
26    var $footer    = 1;         // show metaline below page
27    var $noheader  = 0;         // omit header
28    var $permalink = 0;         // make first headline permalink to included page
29    var $header    = array();   // included page / section header
30    var $renderer  = NULL;      // DokuWiki renderer object
31
32    var $INCLUDE_LIMIT = 12;
33
34    // private variables
35    var $_offset   = NULL;
36
37    /**
38     * Constructor loads some config settings
39     */
40    function helper_plugin_include() {
41        $this->firstsec = $this->getConf('firstseconly');
42        $this->editbtn  = $this->getConf('showeditbtn');
43        $this->footer   = $this->getConf('showfooter');
44        $this->noheader = 0;
45        $this->permalink = 0;
46        $this->header   = array();
47    }
48
49    function getInfo() {
50        return array(
51                'author' => 'Gina Häußge, Michael Klier, Esther Brunner',
52                'email'  => 'dokuwiki@chimeric.de',
53                'date'   => @file_get_contents(DOKU_PLUGIN . 'blog/VERSION'),
54                'name'   => 'Include Plugin (helper class)',
55                'desc'   => 'Functions to include another page in a wiki page',
56                'url'    => 'http://wiki.splitbrain.org/plugin:include',
57                );
58    }
59
60    function getMethods() {
61        $result = array();
62        $result[] = array(
63                'name'   => 'setPage',
64                'desc'   => 'sets the page to include',
65                'params' => array("page attributes, 'id' required, 'section' for filtering" => 'array'),
66                'return' => array('success' => 'boolean'),
67                );
68        $result[] = array(
69                'name'   => 'setMode',
70                'desc'   => 'sets inclusion mode: should indention be merged?',
71                'params' => array("'page' (original) or 'section' (merged indention)" => 'string'),
72                );
73        $result[] = array(
74                'name'   => 'setLevel',
75                'desc'   => 'sets the indention for the current section level',
76                'params' => array('level: 0 to 5' => 'integer'),
77                'return' => array('success' => 'boolean'),
78                );
79        $result[] = array(
80                'name'   => 'setFlags',
81                'desc'   => 'overrides standard values for showfooter and firstseconly settings',
82                'params' => array('flags' => 'array'),
83                );
84        $result[] = array(
85                'name'   => 'renderXHTML',
86                'desc'   => 'renders the XHTML output of the included page',
87                'params' => array('DokuWiki renderer' => 'object'),
88                'return' => array('XHTML' => 'string'),
89                );
90        return $result;
91    }
92
93    /**
94     * Sets the page to include if it is not already included (prevent recursion)
95     * and the current user is allowed to read it
96     */
97    function setPage($page) {
98        global $ID;
99
100        $id     = $page['id'];
101        $fullid = $id.'#'.$page['section'];
102
103        if (!$id) return false;       // no page id given
104        if ($id == $ID) return false; // page can't include itself
105
106        // prevent include recursion
107        if ($this->_in_filechain($id,$page['section']) || (count($this->pages) >= $this->INCLUDE_LIMIT)) return false;
108
109        // we need to make sure 'perm', 'file' and 'exists' are set
110        if (!isset($page['perm'])) $page['perm'] = auth_quickaclcheck($page['id']);
111        if (!isset($page['file'])) $page['file'] = wikiFN($page['id']);
112        if (!isset($page['exists'])) $page['exists'] = @file_exists($page['file']);
113
114        // check permission
115        if ($page['perm'] < AUTH_READ) return false;
116
117        // add the page to the filechain
118        $this->page = $page;
119        return true;
120    }
121
122    function _push_page($id,$section) {
123        global $ID;
124        if (empty($this->pages)) array_push($this->pages, $ID.'#');
125        array_push($this->pages, $id.'#'.$section);
126    }
127
128    function _pop_page() {
129        $page = array_pop($this->pages);
130        if (count($this->pages=1)) $this->pages = array();
131
132        return $page;
133    }
134
135    function _in_filechain($id,$section) {
136        $pattern = $section ? "/^($id#$section|$id#)$/" : "/^$id#/";
137        $match = preg_grep($pattern, $this->pages);
138
139        return (!empty($match));
140    }
141
142    /**
143     * Sets the inclusion mode: 'page' or 'section'
144     */
145    function setMode($mode) {
146        $this->mode = $mode;
147    }
148
149    /**
150     * Sets the right indention for a given section level
151     */
152    function setLevel($level) {
153        if ((is_numeric($level)) && ($level >= 0) && ($level <= 5)) {
154            $this->clevel = $level;
155            return true;
156        }
157        return false;
158    }
159
160    /**
161     * Overrides standard values for showfooter and firstseconly settings
162     */
163    function setFlags($flags) {
164        foreach ($flags as $flag) {
165            switch ($flag) {
166                case 'footer':
167                    $this->footer = 1;
168                    break;
169                case 'nofooter':
170                    $this->footer = 0;
171                    break;
172                case 'firstseconly':
173                case 'firstsectiononly':
174                    $this->firstsec = 1;
175                    break;
176                case 'fullpage':
177                    $this->firstsec = 0;
178                    break;
179                case 'noheader':
180                    $this->noheader = 1;
181                    break;
182                case 'editbtn':
183                case 'editbutton':
184                    $this->editbtn = 1;
185                    break;
186                case 'noeditbtn':
187                case 'noeditbutton':
188                    $this->editbtn = 0;
189                    break;
190                case 'permalink':
191                    $this->permalink = 1;
192                    break;
193                case 'nopermalink':
194                    $this->permalink = 0;
195                    break;
196            }
197        }
198    }
199
200    /**
201     * Builds the XHTML to embed the page to include
202     */
203    function renderXHTML(&$renderer, &$info) {
204        global $ID;
205
206        if (!$this->page['id']) return ''; // page must be set first
207        if (!$this->page['exists'] && ($this->page['perm'] < AUTH_CREATE)) return '';
208
209        $this->_push_page($this->page['id'],$this->page['section']);
210
211        // prepare variables
212        $rdoc  = $renderer->doc;
213        $doc = '';
214        $this->renderer =& $renderer;
215
216        $page = $this->page;
217        $clevel = $this->clevel;
218        $mode = $this->mode;
219
220        // exchange page ID for included one
221        $backupID = $ID;               // store the current ID
222        $ID       = $this->page['id']; // change ID to the included page
223
224        // get instructions and render them on the fly
225        $this->ins = p_cached_instructions($this->page['file']);
226
227        // show only a given section?
228        if ($this->page['section'] && $this->page['exists']) $this->_getSection();
229
230        // convert relative links
231        $this->_convertInstructions();
232
233        $xhtml = p_render('xhtml', $this->ins, $info);
234        $ID = $backupID;               // restore ID
235
236        $this->mode = $mode;
237        $this->clevel = $clevel;
238        $this->page = $page;
239
240        // render the included page
241        $content = '<div class="entry-content">'.DOKU_LF.
242            $this->_cleanXHTML($xhtml).DOKU_LF.
243            '</div><!-- .entry-content -->'.DOKU_LF;
244
245        // restore ID
246        $ID = $backupID;
247
248        // embed the included page
249        $class = ($this->page['draft'] ? 'include draft' : 'include');
250
251        $doc .= DOKU_LF.'<!-- including '.$this->page['id'].' // '.$this->page['file'].' -->'.DOKU_LF;
252        $doc .= '<div class="'.$class.' hentry"'.$this->_showTagLogos().'>'.DOKU_LF;
253        if (!$this->header && $this->clevel && ($this->mode == 'section'))
254            $doc .= '<div class="level'.$this->clevel.'">'.DOKU_LF;
255
256        if ((@file_exists(DOKU_PLUGIN.'editsections/action.php'))
257                && (!plugin_isdisabled('editsections'))) { // for Edit Section Reorganizer Plugin
258            $doc .= $this->_editButton().$content;
259        } else {
260            $doc .= $content.$this->_editButton();
261        }
262
263        // output meta line (if wanted) and remove page from filechain
264        $doc .= $this->_footer($this->page);
265
266        if (!$this->header && $this->clevel && ($this->mode == 'section'))
267            $doc .= '</div>'.DOKU_LF; // class="level?"
268        $doc .= '</div>'.DOKU_LF; // class="include hentry"
269        $doc .= DOKU_LF.'<!-- /including '.$this->page['id'].' -->'.DOKU_LF;
270
271        // reset defaults
272        $this->helper_plugin_include();
273        $this->_pop_page();
274
275        // return XHTML
276        $renderer->doc = $rdoc.$doc;
277        return $doc;
278    }
279
280    /* ---------- Private Methods ---------- */
281
282    /**
283     * Get a section including its subsections
284     */
285    function _getSection() {
286        foreach ($this->ins as $ins) {
287            if ($ins[0] == 'header') {
288
289                // found the right header
290                if (cleanID($ins[1][0]) == $this->page['section']) {
291                    $level = $ins[1][1];
292                    $i[] = $ins;
293
294                    // next header of the same or higher level -> exit
295                } elseif ($ins[1][1] <= $level) {
296                    $this->ins = $i;
297                    return true;
298                } elseif (isset($level)) {
299                    $i[] = $ins;
300                }
301
302                // add instructions from our section
303            } elseif (isset($level)) {
304                $i[] = $ins;
305            }
306        }
307        $this->ins = $i;
308        return true;
309    }
310
311    /**
312     * Corrects relative internal links and media and
313     * converts headers of included pages to subheaders of the current page
314     */
315    function _convertInstructions() {
316        global $ID;
317
318        if (!$this->page['exists']) return false;
319
320        // check if included page is in same namespace
321        $ns      = getNS($this->page['id']);
322        $convert = (getNS($ID) == $ns ? false : true);
323
324        $n = count($this->ins);
325        for ($i = 0; $i < $n; $i++) {
326            $current = $this->ins[$i][0];
327
328            // convert internal links and media from relative to absolute
329            if ($convert && (substr($current, 0, 8) == 'internal')) {
330                $this->ins[$i][1][0] = $this->_convertInternalLink($this->ins[$i][1][0], $ns);
331
332                // set header level to current section level + header level
333            } elseif ($current == 'header') {
334                $this->_convertHeader($i);
335
336                // the same for sections
337            } elseif (($current == 'section_open') && ($this->mode == 'section')) {
338                $this->ins[$i][1][0] = $this->_convertSectionLevel($this->ins[$i][1][0]);
339
340                // show only the first section?
341            } elseif ($this->firstsec && ($current == 'section_close')
342                    && ($this->ins[$i-1][0] != 'section_open')) {
343                $this->_readMore($i);
344                return true;
345            }
346        }
347        $this->_finishConvert();
348        return true;
349    }
350
351    /**
352     * Convert relative internal links and media
353     *
354     * @param    integer $i: counter for current instruction
355     * @param    string  $ns: namespace of included page
356     * @return   string  $link: converted, now absolute link
357     */
358    function _convertInternalLink($link, $ns) {
359
360        // relative subnamespace
361        if ($link{0} == '.') {
362            if ($link{1} == '.') return getNS($ns).':'.substr($link, 2); // parent namespace
363            else return $ns.':'.substr($link, 1);                        // current namespace
364
365            // relative link
366        } elseif (strpos($link, ':') === false) {
367            return $ns.':'.$link;
368
369            // absolute link - don't change
370        } else {
371            return $link;
372        }
373    }
374
375    /**
376     * Convert header level and add header to TOC
377     *
378     * @param    integer $i: counter for current instruction
379     * @return   boolean true
380     */
381    function _convertHeader($i) {
382        global $conf;
383
384        $text = $this->ins[$i][1][0];
385        $hid  = $this->renderer->_headerToLink($text, 'true');
386        if (empty($this->header)) {
387            $this->_offset = $this->clevel - $this->ins[$i][1][1] + 1;
388            $level = $this->_convertSectionLevel(1);
389            $this->header = array('hid' => $hid, 'title' => hsc($text), 'level' => $level);
390            if ($this->noheader) {
391                unset($this->ins[$i]);
392                return true;
393            } else if ($this->permalink){
394                $this->ins[$i] = $this->_permalinkHeader($text, $level, $this->ins[$i][1][2]);
395            }
396        } else {
397            $level = $this->_convertSectionLevel($this->ins[$i][1][1]);
398        }
399        if ($this->mode == 'section') {
400            if (is_array($this->ins[$i][1][1])) { // permalink header
401                $this->ins[$i][1][1][1] = $level;
402            } else { // normal header
403                $this->ins[$i][1][1] = $level;
404            }
405        }
406
407        // add TOC item
408        if (($level >= $conf['toptoclevel']) && ($level <= $conf['maxtoclevel'])) {
409            $this->renderer->toc[] = array(
410                    'hid'   => $hid,
411                    'title' => $text,
412                    'type'  => 'ul',
413                    'level' => $level - $conf['toptoclevel'] + 1
414                    );
415        }
416        return true;
417    }
418
419    /**
420     * Create instruction item for a permalink header
421     *
422     * @param   string  $text: Headline text
423     * @param   integer $level: Headline level
424     * @param   integer $pos: I wish I knew what this is for...
425     *
426     * @author Gina Haeussge <osd@foosel.net>
427     */
428    function _permalinkHeader($text, $level, $pos) {
429        $newIns = array(
430            'plugin',
431            array(
432                'include_header',
433                array(
434                    $text,
435                    $level
436                ),
437            ),
438            $pos
439        );
440
441        return $newIns;
442    }
443
444    /**
445     * Convert the level of headers and sections
446     *
447     * @param    integer $in: current level
448     * @return   integer $out: converted level
449     */
450    function _convertSectionLevel($in) {
451        $out = $in + $this->_offset;
452        if ($out >= 5) return 5;
453        if ($out <= $this->clevel + 1) return $this->clevel + 1;
454        return $out;
455    }
456
457    /**
458     * Adds a read more... link at the bottom of the first section
459     *
460     * @param    integer $i: counter for current instruction
461     * @return   boolean true
462     */
463    function _readMore($i) {
464        $more = ((is_array($this->ins[$i+1])) && ($this->ins[$i+1][0] != 'document_end'));
465
466        if ($this->ins[0][0] == 'document_start') $this->ins = array_slice($this->ins, 1, $i);
467        else $this->ins = array_slice($this->ins, 0, $i);
468
469        if ($more) {
470            array_unshift($this->ins, array('document_start', array(), 0));
471            $last = array_pop($this->ins);
472            $this->ins[] = array('p_open', array(), $last[2]);
473            $this->ins[] = array('internallink',array($this->page['id'], $this->getLang('readmore')),$last[2]);
474            $this->ins[] = array('p_close', array(), $last[2]);
475            $this->ins[] = $last;
476            $this->ins[] = array('document_end', array(), $last[2]);
477        } else {
478            $this->_finishConvert();
479        }
480        return true;
481    }
482
483    /**
484     * Adds 'document_start' and 'document_end' instructions if not already there
485     */
486    function _finishConvert() {
487        if ($this->ins[0][0] != 'document_start')
488            array_unshift($this->ins, array('document_start', array(), 0));
489        $c = count($this->ins) - 1;
490        if ($this->ins[$c][0] != 'document_end')
491            $this->ins[] = array('document_end', array(), 0);
492    }
493
494    /**
495     * Remove TOC, section edit buttons and tags
496     */
497    function _cleanXHTML($xhtml) {
498        $replace = array(
499                '!<div class="toc">.*?(</div>\n</div>)!s'   => '', // remove toc
500                '#<!-- SECTION "(.*?)" \[(\d+-\d*)\] -->#e' => '', // remove section edit buttons
501                '!<div class="tags">.*?(</div>)!s'          => '', // remove category tags
502                );
503        if ($this->clevel)
504            $replace['#<div class="footnotes">#s'] = '<div class="footnotes level'.$this->clevel.'">';
505        $xhtml  = preg_replace(array_keys($replace), array_values($replace), $xhtml);
506        return $xhtml;
507    }
508
509    /**
510     * Optionally display logo for the first tag found in the included page
511     */
512    function _showTagLogos() {
513        if ((!$this->getConf('showtaglogos'))
514                || (plugin_isdisabled('tag'))
515                || (!$taghelper =& plugin_load('helper', 'tag')))
516            return '';
517
518        $subject = p_get_metadata($this->page['id'], 'subject');
519        if (is_array($subject)) $tag = $subject[0];
520        else list($tag, $rest) = explode(' ', $subject, 2);
521        $title = str_replace('_', ' ', noNS($tag));
522        resolve_pageid($taghelper->namespace, $tag, $exists); // resolve shortcuts
523
524        $logosrc = mediaFN($logoID);
525        $types = array('.png', '.jpg', '.gif'); // auto-detect filetype
526        foreach ($types as $type) {
527            if (!@file_exists($logosrc.$type)) continue;
528            $logoID   = $tag.$type;
529            $logosrc .= $type;
530            list($w, $h, $t, $a) = getimagesize($logosrc);
531            return ' style="min-height: '.$h.'px">'.
532                '<img class="mediaright" src="'.ml($logoID).'" alt="'.$title.'"/';
533        }
534        return '';
535    }
536
537    /**
538     * Display an edit button for the included page
539     */
540    function _editButton() {
541        if ($this->page['exists']) {
542            if (($this->page['perm'] >= AUTH_EDIT) && (is_writable($this->page['file'])))
543                $action = 'edit';
544            else return '';
545        } elseif ($this->page['perm'] >= AUTH_CREATE) {
546            $action = 'create';
547        }
548        if ($this->editbtn) {
549            return '<div class="secedit">'.DOKU_LF.DOKU_TAB.
550                html_btn($action, $this->page['id'], '', array('do' => 'edit'), 'post').DOKU_LF.
551                '</div>'.DOKU_LF;
552        } else {
553            return '';
554        }
555    }
556
557    /**
558     * Returns the meta line below the included page
559     */
560    function _footer($page) {
561        global $conf, $ID;
562
563        if (!$this->footer) return ''; // '<div class="inclmeta">&nbsp;</div>'.DOKU_LF;
564
565        $id   = $page['id'];
566        $meta = p_get_metadata($id);
567        $ret  = array();
568
569        // permalink
570        if ($this->getConf('showlink')) {
571            $title = ($page['title'] ? $page['title'] : $meta['title']);
572            if (!$title) $title = str_replace('_', ' ', noNS($id));
573            $class = ($page['exists'] ? 'wikilink1' : 'wikilink2');
574            $link = array(
575                    'url'    => wl($id),
576                    'title'  => $id,
577                    'name'   => hsc($title),
578                    'target' => $conf['target']['wiki'],
579                    'class'  => $class.' permalink',
580                    'more'   => 'rel="bookmark"',
581                    );
582            $ret[] = $this->renderer->_formatLink($link);
583        }
584
585        // date
586        if ($this->getConf('showdate')) {
587            $date = ($page['date'] ? $page['date'] : $meta['date']['created']);
588            if ($date)
589                $ret[] = '<abbr class="published" title="'.strftime('%Y-%m-%dT%H:%M:%SZ', $date).'">'.
590                    strftime($conf['dformat'], $date).
591                    '</abbr>';
592        }
593
594        // author
595        if ($this->getConf('showuser')) {
596            $author   = ($page['user'] ? $page['user'] : $meta['creator']);
597            if ($author) {
598                $userpage = cleanID($this->getConf('usernamespace').':'.$author);
599                resolve_pageid(getNS($ID), $userpage, $exists);
600                $class = ($exists ? 'wikilink1' : 'wikilink2');
601                $link = array(
602                        'url'    => wl($userpage),
603                        'title'  => $userpage,
604                        'name'   => hsc($author),
605                        'target' => $conf['target']['wiki'],
606                        'class'  => $class.' url fn',
607                        'pre'    => '<span class="vcard author">',
608                        'suf'    => '</span>',
609                        );
610                $ret[]    = $this->renderer->_formatLink($link);
611            }
612        }
613
614        // comments - let Discussion Plugin do the work for us
615        if (!$page['section'] && $this->getConf('showcomments')
616                && (!plugin_isdisabled('discussion'))
617                && ($discussion =& plugin_load('helper', 'discussion'))) {
618            $disc = $discussion->td($id);
619            if ($disc) $ret[] = '<span class="comment">'.$disc.'</span>';
620        }
621
622        // linkbacks - let Linkback Plugin do the work for us
623        if (!$page['section'] && $this->getConf('showlinkbacks')
624                && (!plugin_isdisabled('linkback'))
625                && ($linkback =& plugin_load('helper', 'linkback'))) {
626            $link = $linkback->td($id);
627            if ($link) $ret[] = '<span class="linkback">'.$link.'</span>';
628        }
629
630        $ret = implode(DOKU_LF.DOKU_TAB.'&middot; ', $ret);
631
632        // tags - let Tag Plugin do the work for us
633        if (!$page['section'] && $this->getConf('showtags')
634                && (!plugin_isdisabled('tag'))
635                && ($tag =& plugin_load('helper', 'tag'))) {
636            $page['tags'] = '<div class="tags"><span>'.DOKU_LF.
637                DOKU_TAB.$tag->td($id).DOKU_LF.
638                DOKU_TAB.'</span></div>'.DOKU_LF;
639            $ret = $page['tags'].DOKU_TAB.$ret;
640        }
641
642        if (!$ret) $ret = '&nbsp;';
643        $class = 'inclmeta';
644        if ($this->header && $this->clevel && ($this->mode == 'section'))
645            $class .= ' level'.$this->clevel;
646        return '<div class="'.$class.'">'.DOKU_LF.DOKU_TAB.$ret.DOKU_LF.'</div>'.DOKU_LF;
647    }
648
649    /**
650     * Builds the ODT to embed the page to include
651     */
652    function renderODT(&$renderer) {
653        global $ID;
654
655        if (!$this->page['id']) return ''; // page must be set first
656        if (!$this->page['exists'] && ($this->page['perm'] < AUTH_CREATE)) return '';
657
658        // prepare variable
659        $this->renderer =& $renderer;
660
661        // get instructions and render them on the fly
662        $this->ins = p_cached_instructions($this->page['file']);
663
664        // show only a given section?
665        if ($this->page['section'] && $this->page['exists']) $this->_getSection();
666
667        // convert relative links
668        $this->_convertInstructions();
669
670        // render the included page
671        $backupID = $ID;               // store the current ID
672        $ID       = $this->page['id']; // change ID to the included page
673        // remove document_start and document_end to avoid zipping
674        $this->ins = array_slice($this->ins, 1, -1);
675        p_render('odt', $this->ins, $info);
676        $ID = $backupID;               // restore ID
677        // reset defaults
678        $this->helper_plugin_include();
679    }
680}
681//vim:ts=4:sw=4:et:enc=utf-8:
682