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