dokuwiki-indexmenu-plugin – Blame information for rev 1
?pathlinks?
Rev | Author | Line No. | Line |
---|---|---|---|
1 | office | 1 | <?php |
2 | /** |
||
3 | * Info Indexmenu: Show a customizable and sortable index for a namespace. |
||
4 | * |
||
5 | * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) |
||
6 | * @author Samuele Tognini <samuele@samuele.netsons.org> |
||
7 | * |
||
8 | */ |
||
9 | |||
10 | if(!defined('DOKU_INC')) die(); |
||
11 | if(!defined('INDEXMENU_IMG_ABSDIR')) define('INDEXMENU_IMG_ABSDIR', DOKU_PLUGIN."indexmenu/images"); |
||
12 | |||
13 | require_once(DOKU_INC.'inc/search.php'); |
||
14 | |||
15 | /** |
||
16 | * All DokuWiki plugins to extend the parser/rendering mechanism |
||
17 | * need to inherit from this class |
||
18 | */ |
||
19 | class syntax_plugin_indexmenu_indexmenu extends DokuWiki_Syntax_Plugin { |
||
20 | |||
21 | var $sort = false; |
||
22 | var $msort = false; |
||
23 | var $rsort = false; |
||
24 | var $nsort = false; |
||
25 | var $hsort = false; |
||
26 | |||
27 | /** |
||
28 | * What kind of syntax are we? |
||
29 | */ |
||
30 | public function getType() { |
||
31 | return 'substition'; |
||
32 | } |
||
33 | |||
34 | /** |
||
35 | * Behavior regarding the paragraph |
||
36 | */ |
||
37 | public function getPType() { |
||
38 | return 'block'; |
||
39 | } |
||
40 | |||
41 | /** |
||
42 | * Where to sort in? |
||
43 | */ |
||
44 | public function getSort() { |
||
45 | return 138; |
||
46 | } |
||
47 | |||
48 | /** |
||
49 | * Connect pattern to lexer |
||
50 | */ |
||
51 | public function connectTo($mode) { |
||
52 | $this->Lexer->addSpecialPattern('{{indexmenu>.+?}}', $mode, 'plugin_indexmenu_indexmenu'); |
||
53 | } |
||
54 | |||
55 | /** |
||
56 | * Handler to prepare matched data for the rendering process |
||
57 | * |
||
58 | * @param string $match The text matched by the patterns |
||
59 | * @param int $state The lexer state for the match |
||
60 | * @param int $pos The character position of the matched text |
||
61 | * @param Doku_Handler $handler The Doku_Handler object |
||
62 | * @return array Return an array with all data you want to use in render |
||
63 | */ |
||
64 | public function handle($match, $state, $pos, Doku_Handler $handler) { |
||
65 | $theme = 'default'; |
||
66 | $level = -1; |
||
67 | $gen_id = 'random'; |
||
68 | $maxjs = 0; |
||
69 | $max = 0; |
||
70 | $jsajax = ''; |
||
71 | $nss = array(); |
||
72 | $skipns = array(); |
||
73 | $skipfile = array(); |
||
74 | |||
75 | $defaultsstr = $this->getConf('defaultoptions'); |
||
76 | $defaults = explode(' ', $defaultsstr); |
||
77 | |||
78 | $match = substr($match, 12, -2); |
||
79 | //split namespace,level,theme |
||
80 | list($nsstr, $optsstr) = explode('|', $match, 2); |
||
81 | //split options |
||
82 | $opts = explode(' ', $optsstr); |
||
83 | |||
84 | //Context option |
||
85 | $context = $this->hasOption($defaults, $opts, 'context'); |
||
86 | |||
87 | //split optional namespaces |
||
88 | $nss_temp = preg_split("/ /u", $nsstr, -1, PREG_SPLIT_NO_EMPTY); |
||
89 | //Array optional namespace => level |
||
90 | for($i = 1; $i < count($nss_temp); $i++) { |
||
91 | $nsss = preg_split("/#/u", $nss_temp[$i]); |
||
92 | if(!$context) { |
||
93 | $nsss[0] = $this->_parse_ns($nsss[0]); |
||
94 | } |
||
95 | $nss[] = array($nsss[0], (is_numeric($nsss[1])) ? $nsss[1] : $level); |
||
96 | } |
||
97 | //split main requested namespace |
||
98 | if(preg_match('/(.*)#(\S*)/u', $nss_temp[0], $ns_opt)) { |
||
99 | //split level |
||
100 | $ns = $ns_opt[1]; |
||
101 | if(is_numeric($ns_opt[2])) $level = $ns_opt[2]; |
||
102 | } else { |
||
103 | $ns = $nss_temp[0]; |
||
104 | } |
||
105 | if(!$context) { |
||
106 | $ns = $this->_parse_ns($ns); |
||
107 | } |
||
108 | |||
109 | //nocookie option (disable for uncached pages) |
||
110 | $nocookie = $context || $this->hasOption($defaults, $opts, 'nocookie'); |
||
111 | //noscroll option |
||
112 | $noscroll = $this->hasOption($defaults, $opts, 'noscroll'); |
||
113 | //Open at current namespace option |
||
114 | $navbar = $this->hasOption($defaults, $opts, 'navbar'); |
||
115 | //no namespaces options |
||
116 | $nons = $this->hasOption($defaults, $opts, 'nons'); |
||
117 | //no pages option |
||
118 | $nopg = $this->hasOption($defaults, $opts, 'nopg'); |
||
119 | //disable toc preview |
||
120 | $notoc = $this->hasOption($defaults, $opts, 'notoc'); |
||
121 | //disable underscore to space |
||
122 | $scorespace = $this->hasOption($defaults, $opts, 'scorespace'); |
||
123 | //disable the right context menu |
||
124 | $nomenu = $this->hasOption($defaults, $opts, 'nomenu'); |
||
125 | //Main sort method |
||
126 | $tsort = $this->hasOption($defaults, $opts, 'tsort'); |
||
127 | $dsort = $this->hasOption($defaults, $opts, 'dsort'); |
||
128 | if($tsort) { |
||
129 | $sort = 't'; |
||
130 | } elseif($dsort) { |
||
131 | $sort = 'd'; |
||
132 | } else $sort = 0; |
||
133 | //sort directories in the same way as files |
||
134 | $nsort = $this->hasOption($defaults, $opts, 'nsort'); |
||
135 | //sort headpages up |
||
136 | $hsort = $this->hasOption($defaults, $opts, 'hsort'); |
||
137 | //Metadata sort method |
||
138 | if($msort = $this->hasOption($defaults, $opts, 'msort')) { |
||
139 | $msort = 'indexmenu_n'; |
||
140 | } elseif($value = $this->getOption($defaultsstr, $optsstr, '/msort#(\S+)/u')) { |
||
141 | $msort = str_replace(':', ' ', $value); |
||
142 | } |
||
143 | //reverse sort |
||
144 | $rsort = $this->hasOption($defaults, $opts, 'rsort'); |
||
145 | |||
146 | if($sort) $jsajax .= "&sort=" . $sort; |
||
147 | if($msort) $jsajax .= "&msort=" . $msort; |
||
148 | if($rsort) $jsajax .= "&rsort=1"; |
||
149 | if($nsort) $jsajax .= "&nsort=1"; |
||
150 | if($hsort) $jsajax .= "&hsort=1"; |
||
151 | if($nopg) $jsajax .= "&nopg=1"; |
||
152 | |||
153 | //javascript option |
||
154 | $dir = ''; |
||
155 | //check defaults for js,js#theme, #theme |
||
156 | if(!$js = in_array('js', $defaults)) { |
||
157 | if(preg_match('/(?:^|\s)(js)?#(\S*)/u', $defaultsstr, $match_djs) > 0) { |
||
158 | if(!empty($match_djs[1])) $js = true; |
||
159 | if(isset($match_djs[2])) $dir = $match_djs[2]; |
||
160 | } |
||
161 | } |
||
162 | //check opts for nojs,#theme or js,js#theme |
||
163 | if($js) { |
||
164 | if(in_array('nojs', $opts)) { |
||
165 | $js = false; |
||
166 | } else { |
||
167 | if(preg_match('/(?:^|\s)(?:js)?#(\S*)/u', $optsstr, $match_ojs) > 0) { |
||
168 | if(isset($match_ojs[1])) $dir = $match_ojs[1]; |
||
169 | } |
||
170 | } |
||
171 | } else { |
||
172 | if($js = in_array('js', $opts)) { |
||
173 | //use theme from the defaults |
||
174 | } else { |
||
175 | if(preg_match('/(?:^|\s)js#(\S*)/u', $optsstr, $match_ojs) > 0) { |
||
176 | $js = true; |
||
177 | if(isset($match_ojs[1])) $dir = $match_ojs[1]; |
||
178 | } |
||
179 | } |
||
180 | } |
||
181 | |||
182 | if($js) { |
||
183 | //exist theme? |
||
184 | if(!empty($dir) && is_dir(INDEXMENU_IMG_ABSDIR . "/" . $dir)) { |
||
185 | $theme = $dir; |
||
186 | } |
||
187 | |||
188 | //id generation method |
||
189 | $gen_id = $this->getOption($defaultsstr, $optsstr, '/id#(\S+)/u'); |
||
190 | |||
191 | //max option |
||
192 | if($maxmatches = $this->getOption($defaultsstr, $optsstr, '/max#(\d+)($|\s+|#(\d+))/u', true)) { |
||
193 | $max = $maxmatches[1]; |
||
194 | if($maxmatches[3]) { |
||
195 | $jsajax .= "&max=" . $maxmatches[3]; |
||
196 | } |
||
197 | //disable cookie to avoid javascript errors |
||
198 | $nocookie = true; |
||
199 | } else { |
||
200 | $max = 0; |
||
201 | } |
||
202 | |||
203 | //max js option |
||
204 | if($maxjsvalue = $this->getOption($defaultsstr, $optsstr, '/maxjs#(\d+)/u')) { |
||
205 | $maxjs = $maxjsvalue; |
||
206 | } |
||
207 | } |
||
208 | if(is_numeric($gen_id)) { |
||
209 | $identifier = $gen_id; |
||
210 | } elseif($gen_id == 'ns') { |
||
211 | $identifier = sprintf("%u", crc32($ns)); |
||
212 | } else { |
||
213 | $identifier = uniqid(rand()); |
||
214 | } |
||
215 | |||
216 | //skip namespaces in index |
||
217 | $skipns[] = $this->getConf('skip_index'); |
||
218 | if(preg_match('/skipns[\+=](\S+)/u', $optsstr, $sns) > 0) { |
||
219 | //first sign is: '+' (parallel to conf) or '=' (replace conf) |
||
220 | $action = $sns[0][6]; |
||
221 | $index = 0; |
||
222 | if($action == '+') { |
||
223 | $index = 1; |
||
224 | } |
||
225 | $skipns[$index] = $sns[1]; |
||
226 | $jsajax .= "&skipns=" . utf8_encodeFN(($action == '+' ? '+' : '=') . $sns[1]); |
||
227 | } |
||
228 | //skip file |
||
229 | $skipfile[] = $this->getConf('skip_file'); |
||
230 | if(preg_match('/skipfile[\+=](\S+)/u', $optsstr, $sf) > 0) { |
||
231 | //first sign is: '+' (parallel to conf) or '=' (replace conf) |
||
232 | $action = $sf[0][8]; |
||
233 | $index = 0; |
||
234 | if($action == '+') { |
||
235 | $index = 1; |
||
236 | } |
||
237 | $skipfile[$index] = $sf[1]; |
||
238 | $jsajax .= "&skipfile=" . utf8_encodeFN(($action == '+' ? '+' : '=') . $sf[1]); |
||
239 | } |
||
240 | |||
241 | //js options |
||
242 | $js_opts = compact('theme', 'identifier', 'nocookie', 'navbar', 'noscroll', 'maxjs', 'notoc', 'scorespace', 'jsajax', 'context', 'nomenu'); |
||
243 | |||
244 | return array( |
||
245 | $ns, |
||
246 | $js_opts, |
||
247 | $sort, |
||
248 | $msort, |
||
249 | $rsort, |
||
250 | $nsort, |
||
251 | array( |
||
252 | 'level' => $level, |
||
253 | 'nons' => $nons, |
||
254 | 'nopg' => $nopg, |
||
255 | 'nss' => $nss, |
||
256 | 'max' => $max, |
||
257 | 'js' => $js, |
||
258 | 'skip_index' => $skipns, |
||
259 | 'skip_file' => $skipfile, |
||
260 | 'headpage' => $this->getConf('headpage'), |
||
261 | 'hide_headpage' => $this->getConf('hide_headpage') |
||
262 | ), |
||
263 | $hsort |
||
264 | ); |
||
265 | } |
||
266 | |||
267 | |||
268 | /** |
||
269 | * Looks if the default options and syntax options has the requested option |
||
270 | * |
||
271 | * @param array $defaultsopts array of default options |
||
272 | * @param array $opts array of options provided via syntax |
||
273 | * @param string $optionname name of requested option |
||
274 | * @return bool has optionname? |
||
275 | */ |
||
276 | private function hasOption($defaultsopts, $opts, $optionname) { |
||
277 | $name = $optionname; |
||
278 | if(substr($optionname, 0, 2) == 'no') { |
||
279 | $inversename = substr($optionname, 2); |
||
280 | } else { |
||
281 | $inversename = 'no' . $optionname; |
||
282 | } |
||
283 | |||
284 | if(in_array($name, $defaultsopts)) { |
||
285 | return !in_array($inversename, $opts); |
||
286 | } else { |
||
287 | return in_array($name, $opts); |
||
288 | } |
||
289 | } |
||
290 | |||
291 | /** |
||
292 | * Looks for the value of the requested option in the default options and syntax options |
||
293 | * |
||
294 | * @param string $defaultsstr default options string |
||
295 | * @param string $optsstr syntax options string |
||
296 | * @param string $matchpattern pattern to search for |
||
297 | * @param bool $multiplematches if multiple returns array, otherwise the first match |
||
298 | * @return string|array |
||
299 | */ |
||
300 | private function getOption($defaultsstr, $optsstr, $matchpattern, $multiplematches = false) { |
||
301 | if(preg_match($matchpattern, $optsstr, $match_o) > 0) { |
||
302 | if($multiplematches) { |
||
303 | return $match_o; |
||
304 | } else { |
||
305 | return $match_o[1]; |
||
306 | } |
||
307 | } elseif(preg_match($matchpattern, $defaultsstr, $match_d) > 0) { |
||
308 | if($multiplematches) { |
||
309 | return $match_d; |
||
310 | } else { |
||
311 | return $match_d[1]; |
||
312 | } |
||
313 | } |
||
314 | return false; |
||
315 | } |
||
316 | |||
317 | /** |
||
318 | * Handles the actual output creation. |
||
319 | * |
||
320 | * @param $mode string output format being rendered |
||
321 | * @param $renderer Doku_Renderer the current renderer object |
||
322 | * @param $data array data created by handler() |
||
323 | * @return boolean rendered correctly? |
||
324 | */ |
||
325 | public function render($mode, Doku_Renderer $renderer, $data) { |
||
326 | global $ACT; |
||
327 | global $conf; |
||
328 | global $INFO; |
||
329 | if($mode == 'xhtml') { |
||
330 | /** @var Doku_Renderer_xhtml $renderer */ |
||
331 | if($ACT == 'preview') { |
||
332 | //Check user permission to display indexmenu in a preview page |
||
333 | if($this->getConf('only_admins') && |
||
334 | $conf['useacl'] && |
||
335 | $INFO['perm'] < AUTH_ADMIN |
||
336 | ) |
||
337 | return false; |
||
338 | //disable cookies |
||
339 | $data[1]['nocookie'] = true; |
||
340 | } |
||
341 | //Navbar with nojs |
||
342 | if($data[1]['navbar'] && !$data[6]['js']) { |
||
343 | if(!isset($data[0])) $data[0] = '..'; |
||
344 | $data[6]['nss'][] = array(getNS($INFO['id'])); |
||
345 | $renderer->info['cache'] = FALSE; |
||
346 | } |
||
347 | |||
348 | if($data[1]['context']) { |
||
349 | //resolve current id relative namespaces |
||
350 | $data[0] = $this->_parse_ns($data[0], $INFO['id']); |
||
351 | foreach($data[6]['nss'] as $key=> $value) { |
||
352 | $data[6]['nss'][$key][0] = $this->_parse_ns($value[0], $INFO['id']); |
||
353 | } |
||
354 | $renderer->info['cache'] = FALSE; |
||
355 | } |
||
356 | $n = $this->_indexmenu($data); |
||
357 | if(!@$n) { |
||
358 | $n = $this->getConf('empty_msg'); |
||
359 | $n = str_replace('{{ns}}', cleanID($data[0]), $n); |
||
360 | $n = p_render('xhtml', p_get_instructions($n), $info); |
||
361 | } |
||
362 | $renderer->doc .= $n; |
||
363 | return true; |
||
364 | } else if($mode == 'metadata') { |
||
365 | /** @var Doku_Renderer_metadata $renderer */ |
||
366 | if(!($data[1]['navbar'] && !$data[6]['js']) && !$data[1]['context']) { |
||
367 | //this is an indexmenu page that needs the PARSER_CACHE_USE event trigger; |
||
368 | $renderer->meta['indexmenu'] = TRUE; |
||
369 | } |
||
370 | $renderer->doc .= ((empty($data[0])) ? $conf['title'] : nons($data[0]))." index\n\n"; |
||
371 | unset($renderer->persistent['indexmenu']); |
||
372 | return true; |
||
373 | } else { |
||
374 | return false; |
||
375 | } |
||
376 | } |
||
377 | |||
378 | /** |
||
379 | * Return the index |
||
380 | * |
||
381 | * @author Samuele Tognini <samuele@samuele.netsons.org> |
||
382 | * |
||
383 | * This function is a simple hack of Dokuwiki @see html_index($ns) |
||
384 | * @author Andreas Gohr <andi@splitbrain.org> |
||
385 | * |
||
386 | * @param array $myns the options for indexmenu |
||
387 | * @return bool|string return html for a nojs index and when enabled the js rendered index, otherwise false |
||
388 | */ |
||
389 | private function _indexmenu($myns) { |
||
390 | global $conf; |
||
391 | $ns = $myns[0]; |
||
392 | $js_opts = $myns[1]; //theme, identifier, nocookie, navbar, noscroll, maxjs, notoc, scorespace, jsajax, context, nomenu |
||
393 | $this->sort = $myns[2]; |
||
394 | $this->msort = $myns[3]; |
||
395 | $this->rsort = $myns[4]; |
||
396 | $this->nsort = $myns[5]; |
||
397 | $opts = $myns[6]; //level, nons, nopg, nss, max, js, skip_index, skip_file, headpage, hide_headpage |
||
398 | $this->hsort = $myns[7]; |
||
399 | $data = array(); |
||
400 | $js_name = "indexmenu_".$js_opts['identifier']; |
||
401 | $fsdir = "/".utf8_encodeFN(str_replace(':', '/', $ns)); |
||
402 | if($this->sort || $this->msort || $this->rsort || $this->hsort) { |
||
403 | $this->_search($data, $conf['datadir'], array($this, '_search_index'), $opts, $fsdir); |
||
404 | } else { |
||
405 | search($data, $conf['datadir'], array($this, '_search_index'), $opts, $fsdir); |
||
406 | } |
||
407 | if(!$data) return false; |
||
408 | |||
409 | // javascript index |
||
410 | $output_tmp = ""; |
||
411 | if($opts['js']) { |
||
412 | $ns = str_replace('/', ':', $ns); |
||
413 | $output_tmp = $this->_jstree($data, $ns, $js_opts, $js_name, $opts['max']); |
||
414 | |||
415 | //remove unwanted nodes from standard index |
||
416 | $this->_clean_data($data); |
||
417 | } |
||
418 | |||
419 | // Nojs dokuwiki index |
||
420 | // extra div needed when index is first element in sidebar of dokuwiki template, template uses this to toggle sidebar |
||
421 | // the toggle interacts with hide needed for js option. |
||
422 | $output = "\n"; |
||
423 | $output .= '<div><div id="nojs_'.$js_name.'" data-jsajax="'.utf8_encodeFN($js_opts['jsajax']).'" class="indexmenu_nojs">'."\n"; |
||
424 | $output .= html_buildlist($data, 'idx', array($this, "_html_list_index"), "html_li_index"); |
||
425 | $output .= "</div></div>\n"; |
||
426 | $output .= $output_tmp; |
||
427 | return $output; |
||
428 | } |
||
429 | |||
430 | /** |
||
431 | * Build the browsable index of pages using javascript |
||
432 | * |
||
433 | * @author Samuele Tognini <samuele@samuele.netsons.org> |
||
434 | * @author Rene Hadler |
||
435 | * |
||
436 | * @param array $data array with items of the tree |
||
437 | * @param string $ns requested namespace |
||
438 | * @param array $js_opts options for javascript renderer |
||
439 | * @param string $js_name identifier for this index |
||
440 | * @param int $max the node at $max level will retrieve all its child nodes through the AJAX mechanism |
||
441 | * @return bool|string returns inline javascript or false |
||
442 | */ |
||
443 | private function _jstree($data, $ns, $js_opts, $js_name, $max) { |
||
444 | global $conf; |
||
445 | $hns = false; |
||
446 | if(empty($data)) return false; |
||
447 | |||
448 | //Render requested ns as root |
||
449 | $headpage = $this->getConf('headpage'); |
||
450 | //if rootnamespace and headpage, then add startpage as headpage - TODO seems not logic, when desired use $conf[headpage]=:start: ?? |
||
451 | if(empty($ns) && !empty($headpage)) $headpage .= ','.$conf['start']; |
||
452 | $title = $this->_getTitle($ns, $headpage, $hns); |
||
453 | if(empty($title)) { |
||
454 | if(empty($ns)){ |
||
455 | $title = htmlspecialchars($conf['title'], ENT_QUOTES); |
||
456 | } else{ |
||
457 | $title = $ns; |
||
458 | } |
||
459 | } |
||
460 | // inline javascript |
||
461 | $out = "<script type='text/javascript' charset='utf-8'>\n"; |
||
462 | $out .= "<!--//--><![CDATA[//><!--\n"; |
||
463 | $out .= "var $js_name = new dTree('".$js_name."','".$js_opts['theme']."');\n"; |
||
464 | //javascript config options |
||
465 | $sepchar = idfilter(':', false); |
||
466 | $out .= "$js_name.config.urlbase='".substr(wl(":"), 0, -1)."';\n"; |
||
467 | $out .= "$js_name.config.sepchar='".$sepchar."';\n"; |
||
468 | if($js_opts['notoc']) $out .= "$js_name.config.toc=false;\n"; |
||
469 | if($js_opts['scorespace']) $out .= "$js_name.config.scorespace=true;\n"; |
||
470 | if($js_opts['nocookie']) $out .= "$js_name.config.useCookies=false;\n"; |
||
471 | if($js_opts['noscroll']) $out .= "$js_name.config.scroll=false;\n"; |
||
472 | if($js_opts['maxjs'] > 0) $out .= "$js_name.config.maxjs=".$js_opts['maxjs'].";\n"; |
||
473 | if(!empty($js_opts['jsajax'])) $out .= "$js_name.config.jsajax='".utf8_encodeFN($js_opts['jsajax'])."';\n"; |
||
474 | //add root node |
||
475 | $json = new JSON(); |
||
476 | $out .= $js_name.".add('".idfilter(cleanID($ns), false)."',0,-1,".$json->encode($title); |
||
477 | if($hns) $out .= ",'".idfilter(cleanID($hns), false)."'"; |
||
478 | $out .= ");\n"; |
||
479 | //add nodes |
||
480 | $anodes = $this->_jsnodes($data, $js_name); |
||
481 | $out .= $anodes[0]; |
||
482 | //write to document |
||
483 | $out .= "document.write(".$js_name.");\n"; |
||
484 | //initialize index |
||
485 | $out .= "jQuery(function(){".$js_name.".init("; |
||
486 | $out .= (int) is_file(INDEXMENU_IMG_ABSDIR.'/'.$js_opts['theme'].'/style.css').","; |
||
487 | $out .= (int) $js_opts['nocookie'].","; |
||
488 | $out .= '"'.$anodes[1].'",'; |
||
489 | $out .= (int) $js_opts['navbar'].","; |
||
490 | $out .= (int) $max; |
||
491 | if($js_opts['nomenu']) $out .= ",1"; |
||
492 | $out .= ");});\n"; |
||
493 | |||
494 | $out .= "//--><!]]>\n"; |
||
495 | $out .= "</script>\n"; |
||
496 | return $out; |
||
497 | } |
||
498 | |||
499 | /** |
||
500 | * Return array of javascript nodes and nodes to open. |
||
501 | * |
||
502 | * @author Samuele Tognini <samuele@samuele.netsons.org> |
||
503 | * @param array $data array with items of the tree |
||
504 | * @param string $js_name identifier for this index |
||
505 | * @param int $noajax return as inline js (=1) or array for ajax response (=0) |
||
506 | * @return array|bool returns array with |
||
507 | * - a string of the javascript nodes |
||
508 | * - and a string of space separated numbers of the opened nodes |
||
509 | * or false when no data provided |
||
510 | */ |
||
511 | public function _jsnodes($data, $js_name, $noajax = 1) { |
||
512 | if(empty($data)) return false; |
||
513 | //Array of nodes to check |
||
514 | $q = array('0'); |
||
515 | //Current open node |
||
516 | $node = 0; |
||
517 | $out = ''; |
||
518 | $extra = ''; |
||
519 | if($noajax) { |
||
520 | $jscmd = $js_name.".add"; |
||
521 | $separator = ";\n"; |
||
522 | } else { |
||
523 | $jscmd = "new Array "; |
||
524 | $separator = ","; |
||
525 | } |
||
526 | $json = new JSON(); |
||
527 | foreach($data as $i=> $item) { |
||
528 | $i++; |
||
529 | //Remove already processed nodes (greater level = lower level) |
||
530 | while($item['level'] <= $data[end($q) - 1]['level']) { |
||
531 | array_pop($q); |
||
532 | } |
||
533 | |||
534 | //till i found its father node |
||
535 | if($item['level'] == 1) { |
||
536 | //root node |
||
537 | $father = '0'; |
||
538 | } else { |
||
539 | //Father node |
||
540 | $father = end($q); |
||
541 | } |
||
542 | //add node and its options |
||
543 | if($item['type'] == 'd') { |
||
544 | //Search the lowest open node of a tree branch in order to open it. |
||
545 | if($item['open']) ($item['level'] < $data[$node]['level']) ? $node = $i : $extra .= "$i "; |
||
546 | //insert node in last position |
||
547 | array_push($q, $i); |
||
548 | } |
||
549 | $out .= $jscmd."('".idfilter($item['id'], false)."',$i,".$father.",".$json->encode($item['title']); |
||
550 | //hns |
||
551 | ($item['hns']) ? $out .= ",'".idfilter($item['hns'], false)."'" : $out .= ",0"; |
||
552 | ($item['type'] == 'd' || $item['type'] == 'l') ? $out .= ",1" : $out .= ",0"; |
||
553 | //MAX option |
||
554 | ($item['type'] == 'l') ? $out .= ",1" : $out .= ",0"; |
||
555 | $out .= ")".$separator; |
||
556 | } |
||
557 | $extra = rtrim($extra, ' '); |
||
558 | return array($out, $extra); |
||
559 | } |
||
560 | |||
561 | /** |
||
562 | * Get namespace title, checking for headpages |
||
563 | * |
||
564 | * @author Samuele Tognini <samuele@samuele.netsons.org> |
||
565 | * @param string $ns namespace |
||
566 | * @param string $headpage commaseparated headpages options and headpages |
||
567 | * @param string $hns reference pageid of headpage, false when not existing |
||
568 | * @return string when headpage & heading on: title of headpage, otherwise: namespace name |
||
569 | */ |
||
570 | private function _getTitle($ns, $headpage, &$hns) { |
||
571 | global $conf; |
||
572 | $hns = false; |
||
573 | $title = noNS($ns); |
||
574 | if(empty($headpage)) return $title; |
||
575 | $ahp = explode(",", $headpage); |
||
576 | foreach($ahp as $hp) { |
||
577 | switch($hp) { |
||
578 | case ":inside:": |
||
579 | $page = $ns.":".noNS($ns); |
||
580 | break; |
||
581 | case ":same:": |
||
582 | $page = $ns; |
||
583 | break; |
||
584 | //it's an inside start |
||
585 | case ":start:": |
||
586 | $page = ltrim($ns.":".$conf['start'], ":"); |
||
587 | break; |
||
588 | //inside pages |
||
589 | default: |
||
590 | $page = $ns.":".$hp; |
||
591 | } |
||
592 | //check headpage |
||
593 | if(@file_exists(wikiFN($page)) && auth_quickaclcheck($page) >= AUTH_READ) { |
||
594 | if($conf['useheading'] == 1 || $conf['useheading'] === 'navigation') { |
||
595 | $title_tmp = p_get_first_heading($page, FALSE); |
||
596 | if(!is_null($title_tmp)) $title = $title_tmp; |
||
597 | } |
||
598 | $title = htmlspecialchars($title, ENT_QUOTES); |
||
599 | $hns = $page; |
||
600 | //headpage found, exit for |
||
601 | break; |
||
602 | } |
||
603 | } |
||
604 | return $title; |
||
605 | } |
||
606 | |||
607 | /** |
||
608 | * Parse namespace request |
||
609 | * |
||
610 | * @author Samuele Tognini <samuele@samuele.netsons.org> |
||
611 | * @param string $ns namespaceid |
||
612 | * @param bool $id page id to resolve $ns relative to. |
||
613 | * @return string id of namespace |
||
614 | */ |
||
615 | public function _parse_ns($ns, $id = FALSE) { |
||
616 | if(!$id) { |
||
617 | global $ID; |
||
618 | $id = $ID; |
||
619 | } |
||
620 | //Just for old reelases compatibility |
||
621 | if(empty($ns) || $ns == '..') $ns = ":.."; |
||
622 | return resolve_id(getNS($id), $ns); |
||
623 | } |
||
624 | |||
625 | /** |
||
626 | * Clean index data from unwanted nodes in nojs mode. |
||
627 | * |
||
628 | * @author Samuele Tognini <samuele@samuele.netsons.org> |
||
629 | * @param array $data nodes of the tree |
||
630 | * @return void |
||
631 | */ |
||
632 | private function _clean_data(&$data) { |
||
633 | foreach($data as $i=> $item) { |
||
634 | //closed node |
||
635 | if($item['type'] == "d" && !$item['open']) { |
||
636 | $a = $i + 1; |
||
637 | $level = $data[$i]['level']; |
||
638 | //search and remove every lower and closed nodes |
||
639 | while($data[$a]['level'] > $level && !$data[$a]['open']) { |
||
640 | unset($data[$a]); |
||
641 | $a++; |
||
642 | } |
||
643 | } |
||
644 | } |
||
645 | } |
||
646 | |||
647 | /** |
||
648 | * Callback that adds an item of namespace/page to the browsable index, if it fits in the specified options |
||
649 | * |
||
650 | * $opts['skip_index'] string regexp matching namespaceids to skip |
||
651 | * $opts['skip_file'] string regexp matching pageids to skip |
||
652 | * $opts['headpage'] string headpages options or pageids |
||
653 | * $opts['level'] int desired depth of main namespace, -1 = all levels |
||
654 | * $opts['nss'] array with entries: array(namespaceid,level) specifying namespaces with their own level |
||
655 | * $opts['nons'] bool exclude namespace nodes |
||
656 | * $opts['max'] int If initially closed, the node at max level will retrieve all its child nodes through the AJAX mechanism |
||
657 | * $opts['nopg'] bool exclude page nodes |
||
658 | * $opts['hide_headpage'] int don't hide (0) or hide (1) |
||
659 | * $opts['js'] bool use js-render |
||
660 | * |
||
661 | * @author Andreas Gohr <andi@splitbrain.org> |
||
662 | * modified by Samuele Tognini <samuele@samuele.netsons.org> |
||
663 | * @param array $data Already collected nodes |
||
664 | * @param string $base Where to start the search, usually this is $conf['datadir'] |
||
665 | * @param string $file Current file or directory relative to $base |
||
666 | * @param string $type Type either 'd' for directory or 'f' for file |
||
667 | * @param int $lvl Current recursion depht |
||
668 | * @param array $opts Option array as given to search(), see above. |
||
669 | * @return bool if this directory should be traversed (true) or not (false) |
||
670 | */ |
||
671 | public function _search_index(&$data, $base, $file, $type, $lvl, $opts) { |
||
672 | global $conf; |
||
673 | $hns = false; |
||
674 | $isopen = false; |
||
675 | $title = null; |
||
676 | $skip_index = $opts['skip_index']; |
||
677 | $skip_file = $opts['skip_file']; |
||
678 | $headpage = $opts['headpage']; |
||
679 | $id = pathID($file); |
||
680 | if($type == 'd') { |
||
681 | // Skip folders in plugin conf |
||
682 | foreach($skip_index as $skipi) { |
||
683 | if(!empty($skipi) && preg_match($skipi, $id)) |
||
684 | return false; |
||
685 | } |
||
686 | //check ACL (for sneaky_index namespaces too). |
||
687 | if($conf['sneaky_index'] && auth_quickaclcheck($id.':') < AUTH_READ) return false; |
||
688 | //Open requested level |
||
689 | if($opts['level'] > $lvl || $opts['level'] == -1) $isopen = true; |
||
690 | //Search optional namespaces |
||
691 | if(!empty($opts['nss'])) { |
||
692 | $nss = $opts['nss']; |
||
693 | for($a = 0; $a < count($nss); $a++) { |
||
694 | if(preg_match("/^".$id."($|:.+)/i", $nss[$a][0], $match)) { |
||
695 | //It contains an optional namespace |
||
696 | $isopen = true; |
||
697 | } elseif(preg_match("/^".$nss[$a][0]."(:.*)/i", $id, $match)) { |
||
698 | //It's inside an optional namespace |
||
699 | if($nss[$a][1] == -1 || substr_count($match[1], ":") < $nss[$a][1]) { |
||
700 | $isopen = true; |
||
701 | } else { |
||
702 | $isopen = false; |
||
703 | } |
||
704 | } |
||
705 | } |
||
706 | } |
||
707 | if($opts['nons']) { |
||
708 | return $isopen; |
||
709 | } elseif($opts['max'] > 0 && !$isopen && $lvl >= $opts['max']) { |
||
710 | $isopen = false; |
||
711 | //Stop recursive searching |
||
712 | $return = false; |
||
713 | //change type |
||
714 | $type = "l"; |
||
715 | } elseif($opts['js']) { |
||
716 | $return = true; |
||
717 | } else { |
||
718 | $return = $isopen; |
||
719 | } |
||
720 | //Set title and headpage |
||
721 | $title = $this->_getTitle($id, $headpage, $hns); |
||
722 | //link namespace nodes to start pages when excluding page nodes |
||
723 | if(!$hns && $opts['nopg']) $hns = $id.":".$conf['start']; |
||
724 | } else { |
||
725 | //Nopg.Dont show pages |
||
726 | if($opts['nopg']) return false; |
||
727 | $return = true; |
||
728 | //Nons.Set all pages at first level |
||
729 | if($opts['nons']) $lvl = 1; |
||
730 | //don't add |
||
731 | if(substr($file, -4) != '.txt') return false; |
||
732 | //check hiddens and acl |
||
733 | if(isHiddenPage($id) || auth_quickaclcheck($id) < AUTH_READ) return false; |
||
734 | //Skip files in plugin conf |
||
735 | foreach($skip_file as $skipf) { |
||
736 | if(!empty($skipf) && preg_match($skipf, $id)) |
||
737 | return false; |
||
738 | } |
||
739 | //Skip headpages to hide |
||
740 | if(!$opts['nons'] && !empty($headpage) && $opts['hide_headpage']) { |
||
741 | //start page is in root |
||
742 | if($id == $conf['start']) return false; |
||
743 | $ahp = explode(",", $headpage); |
||
744 | foreach($ahp as $hp) { |
||
745 | switch($hp) { |
||
746 | case ":inside:": |
||
747 | if(noNS($id) == noNS(getNS($id))) return false; |
||
748 | break; |
||
749 | case ":same:": |
||
750 | if(@is_dir(dirname(wikiFN($id))."/".utf8_encodeFN(noNS($id)))) return false; |
||
751 | break; |
||
752 | //it' s an inside start |
||
753 | case ":start:": |
||
754 | if(noNS($id) == $conf['start']) return false; |
||
755 | break; |
||
756 | default: |
||
757 | if(noNS($id) == cleanID($hp)) return false; |
||
758 | } |
||
759 | } |
||
760 | } |
||
761 | |||
762 | //Set title |
||
763 | if($conf['useheading'] == 1 || $conf['useheading'] === 'navigation') { |
||
764 | $title = p_get_first_heading($id, FALSE); |
||
765 | } |
||
766 | if(is_null($title)) $title = noNS($id); |
||
767 | $title = htmlspecialchars($title, ENT_QUOTES); |
||
768 | } |
||
769 | |||
770 | $item = array( |
||
771 | 'id' => $id, |
||
772 | 'type' => $type, |
||
773 | 'level' => $lvl, |
||
774 | 'open' => $isopen, |
||
775 | 'title' => $title, |
||
776 | 'hns' => $hns, |
||
777 | 'file' => $file, |
||
778 | 'return' => $return |
||
779 | ); |
||
780 | $item['sort'] = $this->_setorder($item); |
||
781 | $data[] = $item; |
||
782 | return $return; |
||
783 | } |
||
784 | |||
785 | /** |
||
786 | * Callback Index item formatter |
||
787 | * |
||
788 | * User function for @see html_buildlist() |
||
789 | * |
||
790 | * @author Andreas Gohr <andi@splitbrain.org> |
||
791 | * @author Samuele Tognini <samuele@samuele.netsons.org> |
||
792 | * @author Rik Blok |
||
793 | * |
||
794 | * @param array $item item described by array with at least the entries |
||
795 | * - id page id/namespace id |
||
796 | * - type 'd', 'l'(directory which is not yet opened) or 'f' |
||
797 | * - open is node open |
||
798 | * - title title of link |
||
799 | * - hns page id of headpage of the namespace or false |
||
800 | * @return string html of the content of a list item |
||
801 | */ |
||
802 | public function _html_list_index($item) { |
||
803 | global $INFO; |
||
804 | $ret = ''; |
||
805 | |||
806 | //namespace |
||
807 | if($item['type'] == 'd' || $item['type'] == 'l') { |
||
808 | $markCurrentPage = false; |
||
809 | |||
810 | $link = $item['id']; |
||
811 | $more = 'idx='.$item['id']; |
||
812 | //namespace link |
||
813 | if($item['hns']) { |
||
814 | $link = $item['hns']; |
||
815 | $tagid = "indexmenu_idx_head"; |
||
816 | $more = ''; |
||
817 | //current page is shown? |
||
818 | $markCurrentPage = $this->getConf('hide_headpage') && $item['hns'] == $INFO['id']; |
||
819 | } else { |
||
820 | //namespace without headpage |
||
821 | $tagid = "indexmenu_idx"; |
||
822 | if($item['open']) $tagid .= ' open'; |
||
823 | } |
||
824 | |||
825 | if($markCurrentPage) $ret .= '<span class="curid">'; |
||
826 | $ret .= '<a href="'.wl($link, $more).'" class="'.$tagid.'">'; |
||
827 | $ret .= $item['title']; |
||
828 | $ret .= '</a>'; |
||
829 | if($markCurrentPage) $ret .= '</span>'; |
||
830 | } else { |
||
831 | //page link |
||
832 | $ret .= html_wikilink(':'.$item['id']); |
||
833 | } |
||
834 | return $ret; |
||
835 | } |
||
836 | |||
837 | /** |
||
838 | * callback that recurse directory |
||
839 | * |
||
840 | * This function recurses into a given base directory |
||
841 | * and calls the supplied function for each file and directory |
||
842 | * |
||
843 | * Similar to search() of inc/search.php, but has extended sorting options |
||
844 | * |
||
845 | * @param array $data The results of the search are stored here |
||
846 | * @param string $base Where to start the search |
||
847 | * @param callback $func Callback (function name or array with object,method) |
||
848 | * @param array $opts List of indexmenu options |
||
849 | * @param string $dir Current directory beyond $base |
||
850 | * @param int $lvl Recursion Level |
||
851 | * |
||
852 | * @author Andreas Gohr <andi@splitbrain.org> |
||
853 | * @author modified by Samuele Tognini <samuele@samuele.netsons.org> |
||
854 | */ |
||
855 | public function _search(&$data, $base, $func, $opts, $dir = '', $lvl = 1) { |
||
856 | $dirs = array(); |
||
857 | $files = array(); |
||
858 | $files_tmp = array(); |
||
859 | $dirs_tmp = array(); |
||
860 | $count = count($data); |
||
861 | |||
862 | //read in directories and files |
||
863 | $dh = @opendir($base.'/'.$dir); |
||
864 | if(!$dh) return; |
||
865 | while(($file = readdir($dh)) !== false) { |
||
866 | //skip hidden files and upper dirs |
||
867 | if(preg_match('/^[\._]/', $file)) continue; |
||
868 | if(is_dir($base.'/'.$dir.'/'.$file)) { |
||
869 | $dirs[] = $dir.'/'.$file; |
||
870 | continue; |
||
871 | } |
||
872 | $files[] = $dir.'/'.$file; |
||
873 | } |
||
874 | closedir($dh); |
||
875 | |||
876 | //Collect and sort dirs |
||
877 | if($this->nsort) { |
||
878 | //collect the wanted directories in dirs_tmp |
||
879 | foreach($dirs as $dir) { |
||
880 | call_user_func_array($func, array(&$dirs_tmp, $base, $dir, 'd', $lvl, $opts)); |
||
881 | } |
||
882 | //sort directories |
||
883 | usort($dirs_tmp, array($this, "_cmp")); |
||
884 | //add and search each directory |
||
885 | foreach($dirs_tmp as $dir) { |
||
886 | $data[] = $dir; |
||
887 | if($dir['return']) { |
||
888 | $this->_search($data, $base, $func, $opts, $dir['file'], $lvl + 1); |
||
889 | } |
||
890 | } |
||
891 | } else { |
||
892 | //sort by page name |
||
893 | sort($dirs); |
||
894 | //collect directories |
||
895 | foreach($dirs as $dir) { |
||
896 | if(call_user_func_array($func, array(&$data, $base, $dir, 'd', $lvl, $opts))) { |
||
897 | $this->_search($data, $base, $func, $opts, $dir, $lvl + 1); |
||
898 | } |
||
899 | } |
||
900 | } |
||
901 | |||
902 | //Collect and sort files |
||
903 | foreach($files as $file) { |
||
904 | call_user_func_array($func, array(&$files_tmp, $base, $file, 'f', $lvl, $opts)); |
||
905 | } |
||
906 | usort($files_tmp, array($this, "_cmp")); |
||
907 | |||
908 | //count added items |
||
909 | $added = count($data) - $count; |
||
910 | |||
911 | if($added === 0 && empty($files_tmp)) { |
||
912 | //remove empty directory again, only if it has not a headpage associated |
||
913 | $v = end($data); |
||
914 | if(!$v['hns']) array_pop($data); |
||
915 | } else { |
||
916 | //add files to index |
||
917 | $data = array_merge($data, $files_tmp); |
||
918 | } |
||
919 | } |
||
920 | |||
921 | /** |
||
922 | * callback that sorts nodes |
||
923 | * |
||
924 | * @param array $a first node as array with 'sort' entry |
||
925 | * @param array $b second node as array with 'sort' entry |
||
926 | * @return int if less than zero 1st node is less than 2nd, otherwise equal respectively larger |
||
927 | */ |
||
928 | private function _cmp($a, $b) { |
||
929 | if($this->rsort) { |
||
930 | return strnatcasecmp($b['sort'], $a['sort']); |
||
931 | } else { |
||
932 | return strnatcasecmp($a['sort'], $b['sort']); |
||
933 | } |
||
934 | } |
||
935 | |||
936 | /** |
||
937 | * Add sort information to item. |
||
938 | * |
||
939 | * @author Samuele Tognini <samuele@samuele.netsons.org> |
||
940 | * |
||
941 | * @param array $item |
||
942 | * @return bool|int|mixed|string |
||
943 | */ |
||
944 | private function _setorder($item) { |
||
945 | global $conf; |
||
946 | |||
947 | $sort = false; |
||
948 | $page = false; |
||
949 | if($item['type'] == 'd' || $item['type'] == 'l') { |
||
950 | //Fake order info when nsort is not requested |
||
951 | ($this->nsort) ? $page = $item['hns'] : $sort = 0; |
||
952 | } |
||
953 | if($item['type'] == 'f') $page = $item['id']; |
||
954 | if($page) { |
||
955 | if($this->hsort && noNS($item['id']) == $conf['start']) $sort = 1; |
||
956 | if($this->msort) $sort = p_get_metadata($page, $this->msort); |
||
957 | if(!$sort && $this->sort) { |
||
958 | switch($this->sort) { |
||
959 | case 't': |
||
960 | $sort = $item['title']; |
||
961 | break; |
||
962 | case 'd': |
||
963 | $sort = @filectime(wikiFN($page)); |
||
964 | break; |
||
965 | } |
||
966 | } |
||
967 | } |
||
968 | if($sort === false) $sort = noNS($item['id']); |
||
969 | return $sort; |
||
970 | } |
||
971 | } //Indexmenu class end |