1<?php
2/**
3 *  Info Indexmenu: Displays the index of a specified namespace.
4 *
5 *  $Id: indexmenu.php 93 2007-05-07 11:56:33Z wingedfox $
6 *  $HeadURL: https://svn.debugger.ru/repos/common/DokuWiki/Indexmenu2/tags/Indexmenu2.v2.1.2/syntax/indexmenu.php $
7 *
8 *  @lastmodified $Date: 2007-05-07 15:56:33 +0400 (Пнд, 07 Май 2007) $
9 *  @license      LGPL 2 (http://www.gnu.org/licenses/lgpl.html)
10 *  @author       Ilya Lebedev <ilya@lebedev.net>
11 *  @version      $Rev: 93 $
12 *  @copyright    (c) 2005-2007, Ilya Lebedev
13 */
14
15if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/');
16if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
17if(!defined('INDEXMENU_FS_IMAGES')) define('INDEXMENU_FS_IMAGES',realpath(dirname(__FILE__)."/../templates")."/");
18require_once(DOKU_PLUGIN.'syntax.php');
19require_once(DOKU_INC.'inc/search.php');
20
21/**
22 * All DokuWiki plugins to extend the parser/rendering mechanism
23 * need to inherit from this class
24 */
25class syntax_plugin_indexmenu_indexmenu extends DokuWiki_Syntax_Plugin {
26
27    /**
28     *  sorting target and reverse flag
29     *
30     */
31    var $s_target = array('fn' => 'target', 'title' => 'title' , 'date' => 'date');
32    var $s_rev = false;
33  /**
34   * return some info
35   */
36  function getInfo() {
37      preg_match("#^.+Indexmenu2[/.]([^\\/]+)#"," $HeadURL: https://svn.debugger.ru/repos/common/DokuWiki/Indexmenu2/tags/Indexmenu2.v2.1.2/syntax/indexmenu.php $ ", $v);
38      $v = preg_replace("#.*?((trunk|\.v)[\d.]+)#","\\1",$v[1]);
39      $b = preg_replace("/\\D/","", " $Rev: 93 $ ");
40
41      return array( 'author' => "Ilya Lebedev"
42                   ,'email'  => 'ilya@lebedev.net'
43                   ,'date'   => preg_replace("#.*?(\d{4}-\d{2}-\d{2}).*#","\\1",'$Date: 2007-05-07 15:56:33 +0400 (Пнд, 07 Май 2007) $')
44                   ,'name'   => "Indexmenu 2 {$v}.$b"
45                   ,'desc'   => "Insert the index of a specified namespace.\nJavascript code: http://cms.debugger.ru by Ilya Lebedev."
46                   ,'url'    => 'https://www.dokuwiki.org/plugin:indexmenu2'
47                  );
48  }
49
50  /**
51   * What kind of syntax are we?
52   */
53  function getType(){
54    return 'substition';
55  }
56
57  function getPType(){
58    return 'block';
59  }
60  /**
61   * Where to sort in?
62   */
63  function getSort(){
64    return 138;
65  }
66  /**
67   *  Emulates getConf for DW releases prior to current RCs
68   *
69   *  @param string $var variable to get
70   *  @return string value
71   *  @access protected
72   */
73  function getConf($var) {
74    global $conf;
75    if (method_exists(DokuWiki_Syntax_Plugin,'getConf')) {
76      if (!$this) {
77        $tmp = & new syntax_plugin_indexmenu_indexmenu();
78        $res = $tmp->getConf($var);
79        unset ($tmp);
80        return $res;
81      } else {
82        return parent::getConf($var);
83      }
84    } else {
85      return @$conf['plugin_indexmenu'][$var];
86    }
87  }
88
89  /**
90   * Connect pattern to lexer
91   */
92  function connectTo($mode) {
93    $this->Lexer->addSpecialPattern('{{indexmenu>.+?}}',$mode,'plugin_indexmenu_indexmenu');
94    $this->Lexer->addSpecialPattern('{{indexmenu>.+?}}',$mode,'plugin_indexmenu_indexmenu');
95  }
96
97  /**
98   * Handle the match
99   */
100  function handle($match, $state, $pos, &$handler){
101
102    return $this->parseOptions(substr($match,12,-2));
103
104  }
105
106  /**
107   *  Render output
108   */
109  function render($mode, &$renderer, $data) {
110      switch ($mode) {
111          case 'xhtml' :
112              $n = $this->_indexmenu($data);
113              if (!$n && ($n = $this->getConf('empty_msg'))) {
114                  $exists = false;
115                  resolve_pageid(getNS(getID()),$data[0],$exists);
116                  $n = str_replace('{{ns}}'," ".$data[0]." ",$n);
117              }
118              $renderer->doc .= $n ;
119              return true;
120              break;
121          case 'metadata' :
122              /*
123              *  used to purge the cache, if path to the current ID is used
124              */
125              if ("." == $data[0]) $renderer->meta['indexmenu'] = true;
126              return true;
127              break;
128    }
129    return false;
130  }
131
132  /**
133   *  Parse plugin options and return array with them
134   *
135   *  @author Ilya Lebedev <ilya@lebedev.net>
136   *  @param $opts string unparsed options list
137   *  @return array options values
138   *  @access public
139   */
140  function parseOptions ($opts) {
141    $theme="DokuWiki";
142
143    $level = 0;
144
145    $nons = true;
146    /*
147    *  split namespace,level,theme
148    *  array will have
149    *   0 => namespace name and options
150    *   1 => optional js settings
151    */
152    $options = explode('|', $opts, 2);
153
154    /*
155    *  split namespace
156    *  array will have
157    *   0 => namespace name
158    *   1 => options
159    *   2 => sort mode
160    */
161    $options[0] = explode("#", trim($options[0]));
162    $ns = $options[0][0];
163    /*
164    *  split namespace options
165    *  i.e. 1+nons => array(1,'nons')
166    */
167    @$options[0][1] = explode("+",$options[0][1]);
168    /*
169    *  split sort options
170    *  i.e. sort+type+rev => array('sort','nons','rev')
171    */
172    @$options[0][2] = explode("+",$options[0][2]);
173
174    /*
175    *  does not matter, if level is not defined
176    */
177    $level = intval(@$options[0][1][0]);
178    $nons = in_array('nons',$options[0][1]);
179
180    /*
181    *  now, parse the JS options
182    */
183    if (!isset($options[1])) {
184      $js = false;
185      $ajax = false;
186      $theme = '';
187    } else {
188      $js = true;
189      /*
190      *  split js part
191      *  array will have
192      *   0 => 'js'
193      *   1 => options
194      */
195      $options[1] = explode("#", $options[1]);
196      /*
197      *  split js options
198      *  i.e. IndexMenu+ajax => array('IndexMenu','nons')
199      */
200      $options[1][1] = explode('+',$options[1][1]);
201      $ajax = in_array('ajax',$options[1][1]);
202      /*
203      *  change the default theme name only if it really exists
204      */
205      if (@file_exists(INDEXMENU_FS_IMAGES.$theme."/".$options[1][1][0]."/design.css")) {
206        $theme .= "/".$options[1][1][0];
207      }
208    }
209    return array($ns,array( 'level' => $level
210                           ,'theme' => $theme
211                           ,'nons'  => $nons
212                           ,'ajax'  => $ajax
213                           ,'js'    => $js
214                           ,'sort'  => $options[0][2][1]
215                          )
216                );
217  }
218  /**
219   *  Return the index
220   *
221   *  @author Ilya Lebedev <ilya@lebedev.net>
222   */
223  function _indexmenu($myns) {
224    global $conf;
225    global $ID;
226
227    $ns = $myns[0];
228    $opts = $myns[1];
229
230    $exists = false;
231    $id = resolve_pageid(getNS(getID()),$ns,$exists);
232    if ($ns == $conf['start']) $ns = "";
233    /*
234    *  if 'nons' is set or NS is root, no need to adjust settings
235    */
236    if (($opts['js'] || $opts['navigation']) && !$opts['nons'] && $ns) {
237      $opts['root'] = $ns;
238      $ns = (string)getNS($ns);
239      /*
240      *  we've moved index root 1 level lower, adjust max level than
241      */
242      if ($opts['level']) $opts['level']++;
243    }
244    $data = array();
245    search($data,$conf['datadir'],"indexmenu_search_index",$opts,"/".utf8_encodeFN(str_replace(':','/',$ns)));
246    if ($opts['sort']) {
247      $this->s_rev = substr($opts['sort'],0,1)=='!';
248      $this->s_target = $this->s_target[preg_replace("#^!#","",$opts['sort'])];
249
250      if ($this->s_target) {
251        usort($data,array($this,sortCallback));
252      }
253    }
254    /*
255    *  prepare array to convert into nested one
256    */
257    foreach ($data as $k => $v) {
258      $data[$k]['parent_id'] = (string)getNS($v['id']);
259    }
260    /*
261    *  convert array, w/o skipping NSs
262    */
263    if (!$opts['nons'])
264      $data = array2tree($data,$ns);
265
266    /*
267    *  indicate empty tree
268    */
269    if (empty($data)) return false;
270    /*
271    *  if user want to draw complete wiki index, we need to make one more fake level
272    */
273    if ($opts['js'] && !$opts['nons'] && !isset($opts['root']) && !$ns) {
274      $data = array(array('level'  =>0,
275                          'child_nodes' => $data,
276                          'type'   => 'd',
277                          'open'   => 'true',
278                          'id'     => $conf['start'],
279                          'target' => $conf['start'],
280                          'title' => ($conf['useheading'] && ($title=p_get_first_heading($conf['start'])))?$title:"",
281                   ));
282
283    }
284    /*
285    *  get the list tree
286    */
287    return syntax_plugin_indexmenu_indexmenu::getHTML ($opts, $opts['navigation']?$this->html_buildlist($data,$opts)
288                                                                                 :"<ul>".$this->html_buildlist($data,$opts)."</ul>");
289  }
290  /**
291   *  returns complete HTML for the menu
292   *
293   *  @param $opts mixed array of the options
294   *  @param $html string html layout to be wrapped with javascript code, if needed
295   *  @return string
296   *  @access public
297   */
298
299  function getHTML ($opts, $html) {
300      /*
301      *  make unique id for current menu
302      */
303      $idx = 'indexmenu'.str_replace(".","",join(explode(" ",microtime())));
304      $add = $opts['js']?"id=\"$idx\"":"class=\"idx\"";
305      $html = preg_replace("#<ul[^>]*>#i", "<ul $add>", $html, 1);
306
307      /*
308      *  if 'nons' is set, then there's no need in JS
309      */
310      if ($opts['js'] && !$opts['nons']) {
311          /*
312          *  create ajax callback, if needed
313          */
314          if ($opts['ajax']) {
315              $ajax = <<<EOL
316                        ,modifiers : ['ajaxum']
317                        ,ajaxum : {
318                              fetcher : function (s, callback) {
319                                  if ('undefined' == typeof RemoteScript) {
320                                      callback ({'state' : false,
321                                                 'response' : 'Plugin <a href="http://wiki.splitbrain.org/plugin:remotescript" _target="blank">RemoteScript</a> is not available'});
322                                      return;
323                                  }
324
325                                  RemoteScript.query( ['indexmenu','getsubmenu']
326                                                     ,{ 'src'  : s
327                                                       ,'sort' : '{$opts['sort']}'}
328                                                     ,function(js, txt) {
329                                                          callback({'state' : !!js,
330                                                                    'response' : js||txt});
331                                                      }
332                                                     ,true);
333                              }
334                          }
335EOL;
336          } else {
337              $ajax = '';
338          }
339          $themeRoot = DOKU_BASE.'lib/plugins/indexmenu/templates/';
340          $html .= <<<EOL
341              <script type="text/javascript"><!--//--><![CDATA[//><!--
342                 var cms = new CompleteMenuSolution()
343                 cms.initMenu('$idx',{ 'theme': {'name': '{$opts["theme"]}'}
344                                      ,'themeRootPath' : '$themeRoot'
345                                      ,closeSiblings : false
346                                       $ajax
347                                     });
348              //--><!]]></script>
349EOL;
350      }
351      return $html;
352  }
353  /**
354  * Build an unordered list
355  *
356  * Build an unordered list from the given $data array
357  * Each item in the array has to have a 'level' property
358  * the item itself gets printed by the given $func user
359  * function. The second and optional function is used to
360  * print the <li> tag. Both user function need to accept
361  * a single item.
362  *
363  * Both user functions can be given as array to point to
364  * a member of an object.
365  *
366  * @author Andreas Gohr <andi@splitbrain.org>
367  * @author Ilya Lebedev <ilya@lebedev.net>
368  */
369  function html_buildlist(&$data,&$opts){
370      $ret   = array();
371
372      foreach ($data as $item) {
373          $ret[] = "<li".(($item['type']=='d')?(" class=\"".($item['open']?'open':'closed')."\" "):'').">";
374          $ret[] = preg_replace("#^<span[^>]+>(.+)</span>$#i","$1",html_wikilink(":".$item['target'],null));
375          /*
376          *  append child nodes, if exists
377          */
378          if ($item['type']=='d') { //isset($item['child_nodes'])) {
379              if ($opts['level'] != 0 && ($opts['level'] <= $item['level'])) {
380                  /*
381                  *  for closed nodes add mark for Ajaxum plugin
382                  */
383                  if ($opts['ajax'])
384                      $ret[] = "<ul ".(!$opts['js']&&!$opts['navigation']?"style=\"display: none\""
385                                                                         :"")
386                                     ." title=\"{$item['id']}\"><!-- {$item['id']} --></ul>";
387              } else {
388                  /*
389                  *  open nodes process as usual
390                  */
391                  if (isset($item['child_nodes'])) {
392                      $ret[] = "<ul>";
393                      /*
394                      *  static method used to be able to make menu w/o make class object
395                      */
396                      $ret[] = syntax_plugin_indexmenu_indexmenu::html_buildlist($item['child_nodes'],$opts);
397                      $ret[] = "</ul>";
398                  }
399              }
400          }
401          $ret[] = "</li>";
402      }
403      return join("\n",$ret);
404  }
405  /**
406   *  Sorting callback function
407   *
408   *  @param array $a first matching
409   *  @param array $b second matching
410   *  @return int -1,0,1 sorting result
411   *  @access protected
412   */
413  function sortCallback ($a, $b) {
414    $t1 = $this->s_rev?$b[$this->s_target]:$a[$this->s_target];
415    $t2 = $this->s_rev?$a[$this->s_target]:$b[$this->s_target];
416    if ($t1>$t2) return 1;
417    if ($t1<$t2) return -1;
418    return 0;
419  }
420} //Indexmenu class end
421  /**
422  * Build the browsable index of pages
423  *
424  * $opts['ns'] is the current namespace
425  *
426  * @author  Andreas Gohr <andi@splitbrain.org>
427  * @author  Ilya Lebedev <ilya@lebedev.net>
428  */
429  function indexmenu_search_index(&$data,$base,$file,$type,$lvl,$opts){
430    global $conf;
431    $ret = true;
432
433    $item = array();
434    if($type == 'd'){
435      if ($opts['level']!=0 && $lvl >= $opts['level']) $ret=false;
436      if ($opts['nons']) return $ret;
437    } elseif($type == 'f' && !preg_match('#\.txt$#',$file)) {
438      //don't add
439      return false;
440    }
441
442    /*
443    *  get page id by filename
444    */
445    $id = pathID($file);
446
447    /*
448    *  index only 'root' namespace, if requested
449    */
450    if ($lvl == 1 && isset($opts['root']) && $id != $opts['root']) return false;
451
452    /*
453    * check for files/folders to skip
454    */
455    if (syntax_plugin_indexmenu_indexmenu::getConf('skip_index') && preg_match(syntax_plugin_indexmenu_indexmenu::getConf('skip_index'), $file))
456      return false;
457
458    //check hiddens
459    if($type=='f' && isHiddenPage($id)){
460      return false;
461    }
462
463    //check ACL (for namespaces too)
464    if(auth_quickaclcheck($id) < AUTH_READ){
465      return false;
466    }
467
468    //check if it's a headpage (acrobatic check)
469    if(!$opts['nons'] && $type=='f' && $conf['useheading'] && syntax_plugin_indexmenu_indexmenu::getConf('hide_headpage')) {
470      if (noNS(getNS($id))==noNS($id) ||                       // /<ns>/<ns>.txt
471          $id==$conf['start'] ||                               // <ns> == <start_page>
472          $id==getNS($id).":".$conf['start'] ||                // /<ns>/<start_page>.txt
473          @file_exists(dirname(wikiFN($id.":".noNS($id))))     // /<ns>/
474                                                               // /<ns>.txt
475         ){
476        return false;
477      }
478    }
479    /*
480    *  bugfix for the
481    *  /ns/
482    *  /<ns>.txt
483    *  case, need to force the 'directory' type
484    */
485    if ($type == 'f' && file_exists(dirname(wikiFN($id.":".noNS($id))))) $type = 'd';
486
487
488    /*
489    *  page target id = global id
490    */
491    $target = $id;
492    if ($type == 'd') {
493      /*
494      *  this will check 3 kinds of headpage:
495      *  1. /<ns>/<ns>.txt
496      *  2. /<ns>/
497      *     /<ns>.txt
498      *  3. /<ns>/
499      *     /<ns>/<start_page>
500      */
501      $nsa = array( $id.":".noNS($id),
502                    $id,
503                    $id.":".$conf['start']
504                  );
505      $nspage = false;
506      foreach ($nsa as $nsp) {
507        if (@file_exists(wikiFN($nsp)) && auth_quickaclcheck($nsp) >= AUTH_READ) {
508          $nspage = $nsp;
509          break;
510        }
511      }
512      //headpage exists
513      if ($nspage) {
514        $target = $nspage;
515      } else {
516        /*
517        *  open namespace index, if headpage does not exists
518        */
519        $target = $target.':';
520      }
521    }
522
523    //Set all pages at first level
524    if ($opts['nons']) {
525      $lvl=1;
526    }
527
528    $data[]=array( 'id'     => $id
529                  ,'date'   => @filectime(wikiFN($target))
530                  ,'type'   => $type
531                  ,'target' => $target  // id to be used in the menu
532                  ,'title'  => ($conf['useheading'] && ($title=p_get_first_heading($target)))?$title:$id // NS title
533                  ,'level'  => $lvl
534                  ,'open'   => $ret );
535
536    return $ret|$opts['ajax'];
537
538  }
539
540
541
542/**
543 * Converts an associative array into tree
544 * @author Anton Makarenko php[at]ripfolio[dot]com
545 * @copyright GPL
546 *
547 * @param array $source_arr
548 * @param mixed $parent_id
549 * @param string $key_children
550 * @param string $key_id
551 * @param string $key_parent_id
552 * @return array $tree
553 *
554 * @example :
555 * $source = array(
556 *         array('id'=>1, 'parent_id'=>0, 'foo'=>'bar'),
557 *         array('id'=>2, 'parent_id'=>1, 'foo'=>'barr'),
558 *         array('id'=>3, 'parent_id'=>1, 'foo'=>'barrr')
559 *         );
560 * $tree = array2tree($source, 0);
561 */
562function array2tree($source_arr, $parent_id, $key_children='child_nodes', $key_id='id', $key_parent_id='parent_id')
563{
564        $tree=array();
565        if (empty($source_arr))
566                return $tree;
567        _array2treer($source_arr, $tree, $parent_id, $parent_id, $key_children, $key_id, $key_parent_id);
568        return $tree;
569}
570/**
571 * A private function. Background for array2tree. It is unnecessarily to use this function directly
572 * @author Anton Makarenko php[at]ripfolio[dot]com
573 * @copyright GPL
574 *
575 * @param array $source_arr
576 * @param array &$_this
577 * @param mixed $parent_id
578 * @param mixed $_this_id
579 * @param string $key_children
580 * @param string $key_id
581 * @param string $key_parent_id
582 * @return null
583 */
584function _array2treer($source_arr, &$_this, $parent_id, $_this_id, $key_children, $key_id, $key_parent_id)
585{
586        // populate current children
587        foreach ($source_arr as $value)
588                if ($value[$key_parent_id]===$_this_id)
589                        $_this[$key_children][$value[$key_id]]=$value;
590        if (isset($_this[$key_children]))
591        {
592                // populate children of the current children
593                foreach ($_this[$key_children] as $value)
594                        _array2treer($source_arr, $_this[$key_children][$value[$key_id]], $parent_id, $value[$key_id], $key_children, $key_id, $key_parent_id);
595                // make the tree root look pretty (more convenient to use such tree)
596                if ($_this_id===$parent_id)
597                        $_this=$_this[$key_children];
598        }
599}
600
601//Setup VIM: ex: et ts=4 enc=utf-8 :
602