xref: /plugin/include/helper.php (revision 9d22b4288c52220bca6d26e75d1630af788904d0)
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 * @author     Michael Hamann <michael@content-space.de>
8 */
9
10// must be run within Dokuwiki
11if (!defined('DOKU_INC')) die();
12
13if (!defined('DOKU_LF')) define('DOKU_LF', "\n");
14if (!defined('DOKU_TAB')) define('DOKU_TAB', "\t");
15if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC.'lib/plugins/');
16
17/**
18 * Helper functions for the include plugin and other plugins that want to include pages.
19 */
20class helper_plugin_include extends DokuWiki_Plugin { // DokuWiki_Helper_Plugin
21
22    var $defaults  = array();
23    var $sec_close = true;
24    /** @var helper_plugin_tag $taghelper */
25    var $taghelper = null;
26    var $includes  = array(); // deprecated - compatibility code for the blog plugin
27
28    /**
29     * Constructor loads default config settings once
30     */
31    function __construct() {
32        $this->defaults['noheader']  = $this->getConf('noheader');
33        $this->defaults['firstsec']  = $this->getConf('firstseconly');
34        $this->defaults['editbtn']   = $this->getConf('showeditbtn');
35        $this->defaults['taglogos']  = $this->getConf('showtaglogos');
36        $this->defaults['footer']    = $this->getConf('showfooter');
37        $this->defaults['redirect']  = $this->getConf('doredirect');
38        $this->defaults['date']      = $this->getConf('showdate');
39        $this->defaults['mdate']     = $this->getConf('showmdate');
40        $this->defaults['user']      = $this->getConf('showuser');
41        $this->defaults['comments']  = $this->getConf('showcomments');
42        $this->defaults['linkbacks'] = $this->getConf('showlinkbacks');
43        $this->defaults['tags']      = $this->getConf('showtags');
44        $this->defaults['link']      = $this->getConf('showlink');
45        $this->defaults['permalink'] = $this->getConf('showpermalink');
46        $this->defaults['indent']    = $this->getConf('doindent');
47        $this->defaults['linkonly']  = $this->getConf('linkonly');
48        $this->defaults['title']     = $this->getConf('title');
49        $this->defaults['pageexists']  = $this->getConf('pageexists');
50        $this->defaults['parlink']   = $this->getConf('parlink');
51        $this->defaults['inline']    = false;
52        $this->defaults['order']     = $this->getConf('order');
53        $this->defaults['rsort']     = $this->getConf('rsort');
54        $this->defaults['depth']     = $this->getConf('depth');
55        $this->defaults['readmore']  = $this->getConf('readmore');
56    }
57
58    /**
59     * Available methods for other plugins
60     */
61    function getMethods() {
62        $result = array();
63        $result[] = array(
64                'name'   => 'get_flags',
65                'desc'   => 'overrides standard values for showfooter and firstseconly settings',
66                'params' => array('flags' => 'array'),
67                );
68        return $result;
69    }
70
71    /**
72     * Overrides standard values for showfooter and firstseconly settings
73     */
74    function get_flags($setflags) {
75        // load defaults
76        $flags = $this->defaults;
77        foreach ($setflags as $flag) {
78            $value = '';
79            if (strpos($flag, '=') !== false) {
80                list($flag, $value) = explode('=', $flag, 2);
81            }
82            switch ($flag) {
83                case 'footer':
84                    $flags['footer'] = 1;
85                    break;
86                case 'nofooter':
87                    $flags['footer'] = 0;
88                    break;
89                case 'firstseconly':
90                case 'firstsectiononly':
91                    $flags['firstsec'] = 1;
92                    break;
93                case 'fullpage':
94                    $flags['firstsec'] = 0;
95                    break;
96                case 'showheader':
97                case 'header':
98                    $flags['noheader'] = 0;
99                    break;
100                case 'noheader':
101                    $flags['noheader'] = 1;
102                    break;
103                case 'editbtn':
104                case 'editbutton':
105                    $flags['editbtn'] = 1;
106                    break;
107                case 'noeditbtn':
108                case 'noeditbutton':
109                    $flags['editbtn'] = 0;
110                    break;
111                case 'permalink':
112                    $flags['permalink'] = 1;
113                    break;
114                case 'nopermalink':
115                    $flags['permalink'] = 0;
116                    break;
117                case 'redirect':
118                    $flags['redirect'] = 1;
119                    break;
120                case 'noredirect':
121                    $flags['redirect'] = 0;
122                    break;
123                case 'link':
124                    $flags['link'] = 1;
125                    break;
126                case 'nolink':
127                    $flags['link'] = 0;
128                    break;
129                case 'user':
130                    $flags['user'] = 1;
131                    break;
132                case 'nouser':
133                    $flags['user'] = 0;
134                    break;
135                case 'comments':
136                    $flags['comments'] = 1;
137                    break;
138                case 'nocomments':
139                    $flags['comments'] = 0;
140                    break;
141                case 'linkbacks':
142                    $flags['linkbacks'] = 1;
143                    break;
144                case 'nolinkbacks':
145                    $flags['linkbacks'] = 0;
146                    break;
147                case 'tags':
148                    $flags['tags'] = 1;
149                    break;
150                case 'notags':
151                    $flags['tags'] = 0;
152                    break;
153                case 'date':
154                    $flags['date'] = 1;
155                    break;
156                case 'nodate':
157                    $flags['date'] = 0;
158                    break;
159                case 'mdate':
160                    $flags['mdate'] = 1;
161                    break;
162                case 'nomdate':
163                    $flags['mdate'] = 0;
164                    break;
165                case 'indent':
166                    $flags['indent'] = 1;
167                    break;
168                case 'noindent':
169                    $flags['indent'] = 0;
170                    break;
171                case 'linkonly':
172                    $flags['linkonly'] = 1;
173                    break;
174                case 'nolinkonly':
175                case 'include_content':
176                    $flags['linkonly'] = 0;
177                    break;
178                case 'inline':
179                    $flags['inline'] = 1;
180                    break;
181                case 'title':
182                    $flags['title'] = 1;
183                    break;
184                case 'notitle':
185                    $flags['title'] = 0;
186                    break;
187                case 'pageexists':
188                    $flags['pageexists'] = 1;
189                    break;
190                case 'nopageexists':
191                    $flags['pageexists'] = 0;
192                    break;
193                case 'existlink':
194                    $flags['pageexists'] = 1;
195                    $flags['linkonly'] = 1;
196                    break;
197                case 'parlink':
198                    $flags['parlink'] = 1;
199                    break;
200                case 'noparlink':
201                    $flags['parlink'] = 0;
202                    break;
203                case 'order':
204                    $flags['order'] = $value;
205                    break;
206                case 'sort':
207                    $flags['rsort'] = 0;
208                    break;
209                case 'rsort':
210                    $flags['rsort'] = 1;
211                    break;
212                case 'depth':
213                    $flags['depth'] = max(intval($value), 0);
214                    break;
215                case 'beforeeach':
216                    $flags['beforeeach'] = $value;
217                    break;
218                case 'aftereach':
219                    $flags['aftereach'] = $value;
220                    break;
221                case 'readmore':
222                    $flags['readmore'] = 1;
223                    break;
224                case 'noreadmore':
225                    $flags['readmore'] = 0;
226                    break;
227                case 'exclude':
228                    $flags['exclude'] = $value;
229                    break;
230            }
231        }
232        // the include_content URL parameter overrides flags
233        if (isset($_REQUEST['include_content']))
234            $flags['linkonly'] = 0;
235        return $flags;
236    }
237
238    /**
239     * Returns the converted instructions of a give page/section
240     *
241     * @author Michael Klier <chi@chimeric.de>
242     * @author Michael Hamann <michael@content-space.de>
243     */
244    function _get_instructions($page, $sect, $mode, $lvl, $flags, $root_id = null, $included_pages = array()) {
245        $key = ($sect) ? $page . '#' . $sect : $page;
246        $this->includes[$key] = true; // legacy code for keeping compatibility with other plugins
247
248        // keep compatibility with other plugins that don't know the $root_id parameter
249        if (is_null($root_id)) {
250            global $ID;
251            $root_id = $ID;
252        }
253
254        if ($flags['linkonly']) {
255            if (page_exists($page) || $flags['pageexists']  == 0) {
256                $title = '';
257                if ($flags['title'])
258                    $title = p_get_first_heading($page);
259                if($flags['parlink']) {
260                    $ins = array(
261                        array('p_open', array()),
262                        array('internallink', array(':'.$key, $title)),
263                        array('p_close', array()),
264                    );
265                } else {
266                    $ins = array(array('internallink', array(':'.$key,$title)));
267                }
268            }else {
269                $ins = array();
270            }
271        } else {
272            if (page_exists($page)) {
273                global $ID;
274                $backupID = $ID;
275                $ID = $page; // Change the global $ID as otherwise plugins like the discussion plugin will save data for the wrong page
276                $ins = p_cached_instructions(wikiFN($page), false, $page);
277                $ID = $backupID;
278            } else {
279                $ins = array();
280            }
281
282            $this->_convert_instructions($ins, $lvl, $page, $sect, $flags, $root_id, $included_pages);
283        }
284        return $ins;
285    }
286
287    /**
288     * Converts instructions of the included page
289     *
290     * The funcion iterates over the given list of instructions and generates
291     * an index of header and section indicies. It also removes document
292     * start/end instructions, converts links, and removes unwanted
293     * instructions like tags, comments, linkbacks.
294     *
295     * Later all header/section levels are convertet to match the current
296     * inclusion level.
297     *
298     * @author Michael Klier <chi@chimeric.de>
299     */
300    function _convert_instructions(&$ins, $lvl, $page, $sect, $flags, $root_id, $included_pages = array()) {
301        global $conf;
302
303        // filter instructions if needed
304        if(!empty($sect)) {
305            $this->_get_section($ins, $sect);   // section required
306        }
307
308        if($flags['firstsec']) {
309            $this->_get_firstsec($ins, $page, $flags);  // only first section
310        }
311
312        $ns  = getNS($page);
313        $num = count($ins);
314
315        $conv_idx = array(); // conversion index
316        $lvl_max  = false;   // max level
317        $first_header = -1;
318        $no_header  = false;
319        $sect_title = false;
320        $endpos     = null; // end position of the raw wiki text
321
322        $this->adapt_links($ins, $page, $included_pages);
323
324        for($i=0; $i<$num; $i++) {
325            switch($ins[$i][0]) {
326                case 'document_start':
327                case 'document_end':
328                case 'section_edit':
329                    unset($ins[$i]);
330                    break;
331                case 'header':
332                    // get section title of first section
333                    if($sect && !$sect_title) {
334                        $sect_title = $ins[$i][1][0];
335                    }
336                    // check if we need to skip the first header
337                    if((!$no_header) && $flags['noheader']) {
338                        $no_header = true;
339                    }
340
341                    $conv_idx[] = $i;
342                    // get index of first header
343                    if($first_header == -1) $first_header = $i;
344                    // get max level of this instructions set
345                    if(!$lvl_max || ($ins[$i][1][1] < $lvl_max)) {
346                        $lvl_max = $ins[$i][1][1];
347                    }
348                    break;
349                case 'section_open':
350                    if ($flags['inline'])
351                        unset($ins[$i]);
352                    else
353                        $conv_idx[] = $i;
354                    break;
355                case 'section_close':
356                    if ($flags['inline'])
357                        unset($ins[$i]);
358                    break;
359                case 'nest':
360                    $this->adapt_links($ins[$i][1][0], $page, $included_pages);
361                    break;
362                case 'plugin':
363                    // FIXME skip other plugins?
364                    switch($ins[$i][1][0]) {
365                        case 'tag_tag':                 // skip tags
366                        case 'discussion_comments':     // skip comments
367                        case 'linkback':                // skip linkbacks
368                        case 'data_entry':              // skip data plugin
369                        case 'meta':                    // skip meta plugin
370                        case 'indexmenu_tag':           // skip indexmenu sort tag
371                        case 'include_sorttag':         // skip include plugin sort tag
372                            unset($ins[$i]);
373                            break;
374                        // adapt indentation level of nested includes
375                        case 'include_include':
376                            if (!$flags['inline'] && $flags['indent'])
377                                $ins[$i][1][1][4] += $lvl;
378                            break;
379                        /*
380                         * if there is already a closelastsecedit instruction (was added by one of the section
381                         * functions), store its position but delete it as it can't be determined yet if it is needed,
382                         * i.e. if there is a header which generates a section edit (depends on the levels, level
383                         * adjustments, $no_header, ...)
384                         */
385                        case 'include_closelastsecedit':
386                            $endpos = $ins[$i][1][1][0];
387                            unset($ins[$i]);
388                            break;
389                    }
390                    break;
391                default:
392                    break;
393            }
394        }
395
396        // calculate difference between header/section level and include level
397        $diff = 0;
398        if (!isset($lvl_max)) $lvl_max = 0; // if no level found in target, set to 0
399        $diff = $lvl - $lvl_max + 1;
400        if ($no_header) $diff -= 1;  // push up one level if "noheader"
401
402        // convert headers and set footer/permalink
403        $hdr_deleted      = false;
404        $has_permalink    = false;
405        $footer_lvl       = false;
406        $contains_secedit = false;
407        $section_close_at = false;
408        foreach($conv_idx as $idx) {
409            if($ins[$idx][0] == 'header') {
410                if ($section_close_at === false && isset($ins[$idx+1]) && $ins[$idx+1][0] == 'section_open') {
411                    // store the index of the first heading that is followed by a new section
412                    // the wrap plugin creates sections without section_open so the section shouldn't be closed before them
413                    $section_close_at = $idx;
414                }
415
416                if($no_header && !$hdr_deleted) {
417                    unset ($ins[$idx]);
418                    $hdr_deleted = true;
419                    continue;
420                }
421
422                if($flags['indent']) {
423                    $lvl_new = (($ins[$idx][1][1] + $diff) > 5) ? 5 : ($ins[$idx][1][1] + $diff);
424                    $ins[$idx][1][1] = $lvl_new;
425                }
426
427                if($ins[$idx][1][1] <= $conf['maxseclevel'])
428                    $contains_secedit = true;
429
430                // set permalink
431                if($flags['link'] && !$has_permalink && ($idx == $first_header)) {
432                    $this->_permalink($ins[$idx], $page, $sect, $flags);
433                    $has_permalink = true;
434                }
435
436                // set footer level
437                if(!$footer_lvl && ($idx == $first_header) && !$no_header) {
438                    if($flags['indent'] && isset($lvl_new)) {
439                        $footer_lvl = $lvl_new;
440                    } else {
441                        $footer_lvl = $lvl_max;
442                    }
443                }
444            } else {
445                // it's a section
446                if($flags['indent']) {
447                    $lvl_new = (($ins[$idx][1][0] + $diff) > 5) ? 5 : ($ins[$idx][1][0] + $diff);
448                    $ins[$idx][1][0] = $lvl_new;
449                }
450
451                // check if noheader is used and set the footer level to the first section
452                if($no_header && !$footer_lvl) {
453                    if($flags['indent'] && isset($lvl_new)) {
454                        $footer_lvl = $lvl_new;
455                    } else {
456                        $footer_lvl = $lvl_max;
457                    }
458                }
459            }
460        }
461
462        // close last open section of the included page if there is any
463        if ($contains_secedit) {
464            array_push($ins, array('plugin', array('include_closelastsecedit', array($endpos))));
465        }
466
467        // add edit button
468        if($flags['editbtn']) {
469            $this->_editbtn($ins, $page, $sect, $sect_title, ($flags['redirect'] ? $root_id : false));
470        }
471
472        // add footer
473        if($flags['footer']) {
474            $ins[] = $this->_footer($page, $sect, $sect_title, $flags, $footer_lvl, $root_id);
475        }
476
477        // wrap content at the beginning of the include that is not in a section in a section
478        if ($lvl > 0 && $section_close_at !== 0 && $flags['indent'] && !$flags['inline']) {
479            if ($section_close_at === false) {
480                $ins[] = array('section_close', array());
481                array_unshift($ins, array('section_open', array($lvl)));
482            } else {
483                $section_close_idx = array_search($section_close_at, array_keys($ins));
484                if ($section_close_idx > 0) {
485                    $before_ins = array_slice($ins, 0, $section_close_idx);
486                    $after_ins = array_slice($ins, $section_close_idx);
487                    $ins = array_merge($before_ins, array(array('section_close', array())), $after_ins);
488                    array_unshift($ins, array('section_open', array($lvl)));
489                }
490            }
491        }
492
493        // add instructions entry wrapper
494        $include_secid = (isset($flags['include_secid']) ? $flags['include_secid'] : NULL);
495        array_unshift($ins, array('plugin', array('include_wrap', array('open', $page, $flags['redirect'], $include_secid))));
496        if (isset($flags['beforeeach']))
497            array_unshift($ins, array('entity', array($flags['beforeeach'])));
498        array_push($ins, array('plugin', array('include_wrap', array('close'))));
499        if (isset($flags['aftereach']))
500            array_push($ins, array('entity', array($flags['aftereach'])));
501
502        // close previous section if any and re-open after inclusion
503        if($lvl != 0 && $this->sec_close && !$flags['inline']) {
504            array_unshift($ins, array('section_close', array()));
505            $ins[] = array('section_open', array($lvl));
506        }
507    }
508
509    /**
510     * Appends instruction item for the include plugin footer
511     *
512     * @author Michael Klier <chi@chimeric.de>
513     */
514    function _footer($page, $sect, $sect_title, $flags, $footer_lvl, $root_id) {
515        $footer = array();
516        $footer[0] = 'plugin';
517        $footer[1] = array('include_footer', array($page, $sect, $sect_title, $flags, $root_id, $footer_lvl));
518        return $footer;
519    }
520
521    /**
522     * Appends instruction item for an edit button
523     *
524     * @author Michael Klier <chi@chimeric.de>
525     */
526    function _editbtn(&$ins, $page, $sect, $sect_title, $root_id) {
527        $title = ($sect) ? $sect_title : $page;
528        $editbtn = array();
529        $editbtn[0] = 'plugin';
530        $editbtn[1] = array('include_editbtn', array($title));
531        $ins[] = $editbtn;
532    }
533
534    /**
535     * Convert instruction item for a permalink header
536     *
537     * @author Michael Klier <chi@chimeric.de>
538     */
539    function _permalink(&$ins, $page, $sect, $flags) {
540        $ins[0] = 'plugin';
541        $ins[1] = array('include_header', array($ins[1][0], $ins[1][1], $ins[1][2], $page, $sect, $flags));
542    }
543
544    /**
545     * Convert internal and local links depending on the included pages
546     *
547     * @param array  $ins            The instructions that shall be adapted
548     * @param string $page           The included page
549     * @param array  $included_pages The array of pages that are included
550     */
551    private function adapt_links(&$ins, $page, $included_pages = null) {
552        $num = count($ins);
553        $ns  = getNS($page);
554
555        for($i=0; $i<$num; $i++) {
556            // adjust links with image titles
557            if (strpos($ins[$i][0], 'link') !== false && isset($ins[$i][1][1]) && is_array($ins[$i][1][1]) && $ins[$i][1][1]['type'] == 'internalmedia') {
558                // resolve relative ids, but without cleaning in order to preserve the name
559                $media_id = resolve_id($ns, $ins[$i][1][1]['src']);
560                // make sure that after resolving the link again it will be the same link
561                if ($media_id{0} != ':') $media_id = ':'.$media_id;
562                $ins[$i][1][1]['src'] = $media_id;
563            }
564            switch($ins[$i][0]) {
565                case 'internallink':
566                case 'internalmedia':
567                    // make sure parameters aren't touched
568                    $link_params = '';
569                    $link_id = $ins[$i][1][0];
570                    $link_parts = explode('?', $link_id, 2);
571                    if (count($link_parts) === 2) {
572                        $link_id = $link_parts[0];
573                        $link_params = $link_parts[1];
574                    }
575                    // resolve the id without cleaning it
576                    $link_id = resolve_id($ns, $link_id, false);
577                    // this id is internal (i.e. absolute) now, add ':' to make resolve_id work again
578                    if ($link_id{0} != ':') $link_id = ':'.$link_id;
579                    // restore parameters
580                    $ins[$i][1][0] = ($link_params != '') ? $link_id.'?'.$link_params : $link_id;
581
582                    if ($ins[$i][0] == 'internallink' && !empty($included_pages)) {
583                        // change links to included pages into local links
584                        // only adapt links without parameters
585                        $link_id = $ins[$i][1][0];
586                        $link_parts = explode('?', $link_id, 2);
587                        if (count($link_parts) === 1) {
588                            $exists = false;
589                            resolve_pageid($ns, $link_id, $exists);
590
591                            $link_parts = explode('#', $link_id, 2);
592                            $hash = '';
593                            if (count($link_parts) === 2) {
594                                list($link_id, $hash) = $link_parts;
595                            }
596                            if (array_key_exists($link_id, $included_pages)) {
597                                if ($hash) {
598                                    // hopefully the hash is also unique in the including page (otherwise this might be the wrong link target)
599                                    $ins[$i][0] = 'locallink';
600                                    $ins[$i][1][0] = $hash;
601                                } else {
602                                    // the include section ids are different from normal section ids (so they won't conflict) but this
603                                    // also means that the normal locallink function can't be used
604                                    $ins[$i][0] = 'plugin';
605                                    $ins[$i][1] = array('include_locallink', array($included_pages[$link_id]['hid'], $ins[$i][1][1], $ins[$i][1][0]));
606                                }
607                            }
608                        }
609                    }
610                    break;
611                case 'locallink':
612                    /* Convert local links to internal links if the page hasn't been fully included */
613                    if ($included_pages == null || !array_key_exists($page, $included_pages)) {
614                        $ins[$i][0] = 'internallink';
615                        $ins[$i][1][0] = ':'.$page.'#'.$ins[$i][1][0];
616                    }
617                    break;
618            }
619        }
620    }
621
622    /**
623     * Get a section including its subsections
624     *
625     * @author Michael Klier <chi@chimeric.de>
626     */
627    function _get_section(&$ins, $sect) {
628        $num = count($ins);
629        $offset = false;
630        $lvl    = false;
631        $end    = false;
632        $endpos = null; // end position in the input text, needed for section edit buttons
633
634        $check = array(); // used for sectionID() in order to get the same ids as the xhtml renderer
635
636        for($i=0; $i<$num; $i++) {
637            if ($ins[$i][0] == 'header') {
638
639                // found the right header
640                if (sectionID($ins[$i][1][0], $check) == $sect) {
641                    $offset = $i;
642                    $lvl    = $ins[$i][1][1];
643                } elseif ($offset && $lvl && ($ins[$i][1][1] <= $lvl)) {
644                    $end = $i - $offset;
645                    $endpos = $ins[$i][1][2]; // the position directly after the found section, needed for the section edit button
646                    break;
647                }
648            }
649        }
650        $offset = $offset ? $offset : 0;
651        $end = $end ? $end : ($num - 1);
652        if(is_array($ins)) {
653            $ins = array_slice($ins, $offset, $end);
654            // store the end position in the include_closelastsecedit instruction so it can generate a matching button
655            $ins[] = array('plugin', array('include_closelastsecedit', array($endpos)));
656        }
657    }
658
659    /**
660     * Only display the first section of a page and a readmore link
661     *
662     * @author Michael Klier <chi@chimeric.de>
663     */
664    function _get_firstsec(&$ins, $page, $flags) {
665        $num = count($ins);
666        $first_sect = false;
667        $endpos = null; // end position in the input text
668        for($i=0; $i<$num; $i++) {
669            if($ins[$i][0] == 'section_close') {
670                $first_sect = $i;
671            }
672            if ($ins[$i][0] == 'header') {
673                /*
674                 * Store the position of the last header that is encountered. As section_close/open-instruction are
675                 * always (unless some plugin modifies this) around a header instruction this means that the last
676                 * position that is stored here is exactly the position of the section_close/open at which the content
677                 * is truncated.
678                 */
679                $endpos = $ins[$i][1][2];
680            }
681            // only truncate the content and add the read more link when there is really
682            // more than that first section
683            if(($first_sect) && ($ins[$i][0] == 'section_open')) {
684                $ins = array_slice($ins, 0, $first_sect);
685                if ($flags['readmore']) {
686                    $ins[] = array('plugin', array('include_readmore', array($page)));
687                }
688                $ins[] = array('section_close', array());
689                // store the end position in the include_closelastsecedit instruction so it can generate a matching button
690                $ins[] = array('plugin', array('include_closelastsecedit', array($endpos)));
691                return;
692            }
693        }
694    }
695
696    /**
697     * Gives a list of pages for a given include statement
698     *
699     * @author Michael Hamann <michael@content-space.de>
700     */
701    function _get_included_pages($mode, $page, $sect, $parent_id, $flags) {
702        global $conf;
703        $pages = array();
704        switch($mode) {
705        case 'namespace':
706            $page  = cleanID($page);
707            $ns    = utf8_encodeFN(str_replace(':', '/', $page));
708            // depth is absolute depth, not relative depth, but 0 has a special meaning.
709            $depth = $flags['depth'] ? $flags['depth'] + substr_count($page, ':') + ($page ? 1 : 0) : 0;
710            search($pagearrays, $conf['datadir'], 'search_allpages', array('depth' => $depth, 'skipacl' => false), $ns);
711            if (is_array($pagearrays)) {
712                foreach ($pagearrays as $pagearray) {
713                    if (!isHiddenPage($pagearray['id'])) // skip hidden pages
714                        $pages[] = $pagearray['id'];
715                }
716            }
717            break;
718        case 'tagtopic':
719            if (!$this->taghelper)
720                $this->taghelper =& plugin_load('helper', 'tag');
721            if(!$this->taghelper) {
722                msg('You have to install the tag plugin to use this functionality!', -1);
723                return array();
724            }
725            $tag   = $page;
726            $sect  = '';
727            $pagearrays = $this->taghelper->getTopic('', null, $tag);
728            foreach ($pagearrays as $pagearray) {
729                $pages[] = $pagearray['id'];
730            }
731            break;
732        default:
733            $page = $this->_apply_macro($page, $parent_id);
734            resolve_pageid(getNS($parent_id), $page, $exists); // resolve shortcuts and clean ID
735            if (auth_quickaclcheck($page) >= AUTH_READ)
736                $pages[] = $page;
737        }
738
739        if (isset($flags['exclude']))
740            $pages = array_filter($pages, function ($page) use ($flags) {
741                if (@preg_match($flags['exclude'], $page))
742                    return FALSE;
743                return TRUE;
744            });
745
746        if (count($pages) > 1) {
747            if ($flags['order'] === 'id') {
748                if ($flags['rsort']) {
749                    usort($pages, array($this, '_r_strnatcasecmp'));
750                } else {
751                    natcasesort($pages);
752                }
753            } else {
754                $ordered_pages = array();
755                foreach ($pages as $page) {
756                    $key = '';
757                    switch ($flags['order']) {
758                        case 'title':
759                            $key = p_get_first_heading($page);
760                            break;
761                        case 'created':
762                            $key = p_get_metadata($page, 'date created', METADATA_DONT_RENDER);
763                            break;
764                        case 'modified':
765                            $key = p_get_metadata($page, 'date modified', METADATA_DONT_RENDER);
766                            break;
767                        case 'indexmenu':
768                            $key = p_get_metadata($page, 'indexmenu_n', METADATA_RENDER_USING_SIMPLE_CACHE);
769                            if ($key === null)
770                                $key = '';
771                            break;
772                        case 'custom':
773                            $key = p_get_metadata($page, 'include_n', METADATA_RENDER_USING_SIMPLE_CACHE);
774                            if ($key === null)
775                                $key = '';
776                            break;
777                    }
778                    $key .= '_'.$page;
779                    $ordered_pages[$key] = $page;
780                }
781                if ($flags['rsort']) {
782                    uksort($ordered_pages, array($this, '_r_strnatcasecmp'));
783                } else {
784                    uksort($ordered_pages, 'strnatcasecmp');
785                }
786                $pages = $ordered_pages;
787            }
788        }
789
790        $result = array();
791        foreach ($pages as $page) {
792            $exists = page_exists($page);
793            $result[] = array('id' => $page, 'exists' => $exists, 'parent_id' => $parent_id);
794        }
795        return $result;
796    }
797
798    /**
799     * String comparisons using a "natural order" algorithm in reverse order
800     *
801     * @link http://php.net/manual/en/function.strnatcmp.php
802     * @param string $a First string
803     * @param string $b Second string
804     * @return int Similar to other string comparison functions, this one returns &lt; 0 if
805     * str1 is greater than str2; &gt;
806     * 0 if str1 is lesser than
807     * str2, and 0 if they are equal.
808     */
809    function _r_strnatcasecmp($a, $b) {
810        return strnatcasecmp($b, $a);
811    }
812
813    /**
814     * This function generates the list of all included pages from a list of metadata
815     * instructions.
816     */
817    function _get_included_pages_from_meta_instructions($instructions) {
818        $pages = array();
819        foreach ($instructions as $instruction) {
820            $mode      = $instruction['mode'];
821            $page      = $instruction['page'];
822            $sect      = $instruction['sect'];
823            $parent_id = $instruction['parent_id'];
824            $flags     = $instruction['flags'];
825            $pages = array_merge($pages, $this->_get_included_pages($mode, $page, $sect, $parent_id, $flags));
826        }
827        return $pages;
828    }
829
830    /**
831     *  Get wiki language from "HTTP_ACCEPT_LANGUAGE"
832     *  We allow the pattern e.g. "ja,en-US;q=0.7,en;q=0.3"
833     */
834    function _get_language_of_wiki($id, $parent_id) {
835       global $conf;
836       $result = $conf['lang'];
837       if(strpos($id, '@BROWSER_LANG@') !== false){
838           $brlangp = "/([a-zA-Z]{1,8}(-[a-zA-Z]{1,8})*|\*)(;q=(0(.[0-9]{0,3})?|1(.0{0,3})?))?/";
839           if(preg_match_all(
840               $brlangp, $_SERVER["HTTP_ACCEPT_LANGUAGE"],
841               $matches, PREG_SET_ORDER
842           )){
843               $langs = array();
844               foreach($matches as $match){
845                   $langname = $match[1] == '*' ? $conf['lang'] : $match[1];
846                   $qvalue = $match[4] == '' ? 1.0 : $match[4];
847                   $langs[$langname] = $qvalue;
848               }
849               arsort($langs);
850               foreach($langs as $lang => $langq){
851                   $testpage = $this->_apply_macro(str_replace('@BROWSER_LANG@', $lang, $id), $parent_id);
852                   resolve_pageid(getNS($parent_id), $testpage, $exists);
853                   if($exists){
854                       $result = $lang;
855                       break;
856                   }
857               }
858           }
859       }
860       return cleanID($result);
861    }
862
863    /**
864     * Makes user or date dependent includes possible
865     */
866    function _apply_macro($id, $parent_id) {
867        global $INFO;
868        global $auth;
869
870        // if we don't have an auth object, do nothing
871        if (!$auth) return $id;
872
873        $user     = $_SERVER['REMOTE_USER'];
874        $group    = $INFO['userinfo']['grps'][0];
875
876        // set group for unregistered users
877        if (!isset($group)) {
878            $group = 'ALL';
879        }
880
881        $time_stamp = time();
882        if(preg_match('/@DATE(\w+)@/',$id,$matches)) {
883            switch($matches[1]) {
884            case 'PMONTH':
885                $time_stamp = strtotime("-1 month");
886                break;
887            case 'NMONTH':
888                $time_stamp = strtotime("+1 month");
889                break;
890            case 'NWEEK':
891                $time_stamp = strtotime("+1 week");
892                break;
893            case 'PWEEK':
894                $time_stamp = strtotime("-1 week");
895                break;
896            case 'TOMORROW':
897                $time_stamp = strtotime("+1 day");
898                break;
899            case 'YESTERDAY':
900                $time_stamp = strtotime("-1 day");
901                break;
902            case 'NYEAR':
903                $time_stamp = strtotime("+1 year");
904                break;
905            case 'PYEAR':
906                $time_stamp = strtotime("-1 year");
907                break;
908            }
909            $id = preg_replace('/@DATE(\w+)@/','', $id);
910        }
911
912        $replace = array(
913                '@USER@'  => cleanID($user),
914                '@NAME@'  => cleanID($INFO['userinfo']['name']),
915                '@GROUP@' => cleanID($group),
916                '@BROWSER_LANG@'  => $this->_get_language_of_wiki($id, $parent_id),
917                '@YEAR@'  => date('Y',$time_stamp),
918                '@MONTH@' => date('m',$time_stamp),
919                '@WEEK@' => date('W',$time_stamp),
920                '@DAY@'   => date('d',$time_stamp),
921                '@YEARPMONTH@' => date('Ym',strtotime("-1 month")),
922                '@PMONTH@' => date('m',strtotime("-1 month")),
923                '@NMONTH@' => date('m',strtotime("+1 month")),
924                '@YEARNMONTH@' => date('Ym',strtotime("+1 month")),
925                '@YEARPWEEK@' => date('YW',strtotime("-1 week")),
926                '@PWEEK@' => date('W',strtotime("-1 week")),
927                '@NWEEK@' => date('W',strtotime("+1 week")),
928                '@YEARNWEEK@' => date('YW',strtotime("+1 week")),
929                );
930        return str_replace(array_keys($replace), array_values($replace), $id);
931    }
932}
933// vim:ts=4:sw=4:et:
934