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