1<?php
2/**
3 * Plugin catlist : Displays a list of the pages of a namespace recursively
4 *
5 * @license   MIT
6 * @author    Félix Faisant <xcodexif@xif.fr>
7 *
8 */
9
10if (!defined('DOKU_INC')) die('meh.');
11
12if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC.'lib/plugins/');
13require_once(DOKU_PLUGIN.'syntax.php');
14require_once(DOKU_INC.'inc/search.php');
15require_once(DOKU_INC.'inc/pageutils.php');
16require_once(DOKU_INC.'inc/parserutils.php');
17
18define('CATLIST_DISPLAY_LIST', 1);
19define('CATLIST_DISPLAY_LINE', 2);
20
21define('CATLIST_NSLINK_AUTO', 0);
22define('CATLIST_NSLINK_NONE', 1);
23define('CATLIST_NSLINK_FORCE', 2);
24
25define('CATLIST_INDEX_START', 0);
26define('CATLIST_INDEX_OUTSIDE', 1);
27define('CATLIST_INDEX_INSIDE', 2);
28
29define('CATLIST_SORT_NONE', 0);
30define('CATLIST_SORT_ASCENDING', 1);
31define('CATLIST_SORT_DESCENDING', 2);
32
33class syntax_plugin_catlist extends DokuWiki_Syntax_Plugin {
34
35	function connectTo ($aMode) {
36		$this->Lexer->addSpecialPattern('<catlist[^>]*>', $aMode, 'plugin_catlist');
37	}
38
39	function getSort () {
40		return 189;
41	}
42
43	function getType () {
44		return 'substition';
45	}
46
47	/*********************************************************************************************/
48	/************************************ <catlist> directive ************************************/
49
50	function _checkOption(&$match, $option, &$varAffected, $valIfFound){
51		if (preg_match('/-'.$option.' /i', $match, $found)) {
52			$varAffected = $valIfFound;
53			$match = str_replace($found[0], '', $match);
54		}
55	}
56	function _checkOptionParam(&$match, $option, &$varAffected, $varAssoc){
57		if (preg_match('/-'.$option.':('.implode('|',array_keys($varAssoc)).') /i', $match, $found)) {
58			$varAffected = $varAssoc[$found[1]];
59			$match = str_replace($found[0], '', $match);
60		}
61	}
62
63	function handle ($match, $state, $pos, Doku_Handler $handler) {
64		global $conf;
65
66		$_default_sort_map = array("none" => CATLIST_SORT_NONE,
67		                           "ascending" => CATLIST_SORT_ASCENDING,
68		                           "descending" => CATLIST_SORT_DESCENDING);
69		$_index_priority_map = array("start" => CATLIST_INDEX_START,
70		                             "outside" => CATLIST_INDEX_OUTSIDE,
71		                             "inside" => CATLIST_INDEX_INSIDE);
72
73		$data = array('displayType' => CATLIST_DISPLAY_LIST, 'nsInBold' => true, 'expand' => 6,
74		              'exclupage' => array(), 'excluns' => array(), 'exclunsall' => array(), 'exclunspages' => array(), 'exclunsns' => array(),
75		              'exclutype' => 'id',
76		              'createPageButtonNs' => true, 'createPageButtonSubs' => false, 'pagename_sanitize' => (boolean)$this->getConf('pagename_sanitize'),
77		              'head' => (boolean)$this->getConf('showhead'),
78		              'headTitle' => NULL, 'smallHead' => false, 'linkStartHead' => true, 'hn' => 'h1',
79		              'useheading' => (boolean)$this->getConf('useheading'),
80		              'nsuseheading' => NULL, 'nsLinks' => CATLIST_NSLINK_AUTO,
81		              'columns' => 0, 'maxdepth' => 0,
82		              'sort_order' => $_default_sort_map[$this->getConf('default_sort')],
83		              'sort_by_title' => false, 'sort_by_type' => false, 'sort_by_date' => false, 'sort_collator' => $this->getConf('sort_collator_locale'),
84		              'hide_index' => (boolean)$this->getConf('hide_index'),
85		              'index_priority' => array(),
86		              'nocache' => (boolean)$this->getConf('nocache'),
87		              'hide_nsnotr' => (boolean)$this->getConf('hide_acl_nsnotr'), 'show_pgnoread' => false, 'show_perms' => (boolean)$this->getConf('show_acl'),
88		              'show_leading_ns' => (boolean)$this->getConf('show_leading_ns'),
89		              'show_notfound_error' => true );
90
91		$index_priority = explode(',', $this->getConf('index_priority'));
92		foreach ($index_priority as $index_type) {
93			if (!array_key_exists($index_type, $_index_priority_map)) {
94				msg("catlist: invalid index type in index_priority", -1);
95				return false;
96			}
97			$data['index_priority'][] = $_index_priority_map[$index_type];
98		}
99		$match = utf8_substr($match, 9, -1).' ';
100
101		// Display options
102		$this->_checkOption($match, "displayList", $data['displayType'], CATLIST_DISPLAY_LIST);
103		$this->_checkOption($match, "displayLine", $data['displayType'], CATLIST_DISPLAY_LINE);
104		$this->_checkOption($match, "noNSInBold", $data['nsInBold'], false);
105		if (preg_match("/-expandButton:([0-9]+)/i", $match, $found)) {
106			$data['expand'] = intval($found[1]);
107			$match = str_replace($found[0], '', $match);
108		}
109		$this->_checkOption($match, "noHeadTitle", $data['useheading'], false);
110		$this->_checkOption($match, "forceHeadTitle", $data['useheading'], true);
111		$data['nsuseheading'] = $data['useheading'];
112		$this->_checkOption($match, "noNSHeadTitle", $data['nsuseheading'], false);
113		$this->_checkOption($match, "hideNotFoundMsg", $data['show_notfound_error'], false);
114
115		// Namespace options
116		$this->_checkOption($match, "forceLinks", $data['nsLinks'], CATLIST_NSLINK_FORCE); // /!\ Deprecated
117		$this->_checkOptionParam($match, "nsLinks", $data['nsLinks'], array( "none" => CATLIST_NSLINK_NONE,
118		                                                                     "auto" => CATLIST_NSLINK_AUTO,
119		                                                                     "force" => CATLIST_NSLINK_FORCE ));
120
121		// Exclude options
122		for ($found; preg_match("/-(exclu(page|ns|nsall|nspages|nsns)!?):\"([^\\/\"]+)\" /i", $match, $found); ) {
123			$option = strtolower($found[1]);
124			// is regex negated ?
125			if (substr($option,-1) == "!") {
126				$data[substr($option,0,-1)][] = array('regex' => $found[3], 'neg' => true);
127			} else {
128				$data[$option][] = array('regex' => $found[3], 'neg' => false);
129			}
130			$match = str_replace($found[0], '', $match);
131		}
132		for ($found; preg_match("/-(exclu(page|ns|nsall|nspages|nsns)) /i", $match, $found); ) {
133			$data[strtolower($found[1])] = true;
134			$match = str_replace($found[0], '', $match);
135		}
136		// Exclude type (exclude based on id, name, or title)
137		$this->_checkOption($match, "excludeOnID", $data['exclutype'], 'id');
138		$this->_checkOption($match, "excludeOnName", $data['exclutype'], 'name');
139		$this->_checkOption($match, "excludeOnTitle", $data['exclutype'], 'title');
140		// Exclude page/namespace id list
141		$data['excludelist'] = array();
142		for ($found; preg_match("/-exclude:\\{([^\\}]*)\\} /", $match, $found); ) {
143			$list = explode(' ', $found[1]);
144			$data['excludelist'] = array_merge($data['excludelist'], $list);
145			$match = str_replace($found[0], '', $match);
146		}
147
148		// Max depth
149		if (preg_match("/-maxDepth:([0-9]+)/i", $match, $found)) {
150			$data['maxdepth'] = intval($found[1]);
151			$match = str_replace($found[0], '', $match);
152		}
153
154		// Columns
155		if (preg_match("/-columns:([0-9]+)/i", $match, $found)) {
156			$data['columns'] = intval($found[1]);
157			$match = str_replace($found[0], '', $match);
158		}
159
160		// Head options
161		$this->_checkOption($match, "noHead", $data['head'], false);
162		$this->_checkOption($match, "showHead", $data['head'], true);
163		$this->_checkOption($match, "smallHead", $data['smallHead'], true);
164		$this->_checkOption($match, "noLinkStartHead", $data['linkStartHead'], false);
165		if (preg_match("/-(h[1-5])/i", $match, $found)) {
166			$data['hn'] = $found[1];
167			$match = str_replace($found[0], '', $match);
168		}
169		if (preg_match("/-titleHead:\"([^\"]*)\"/i", $match, $found)) {
170			$data['headTitle'] = $found[1];
171			$match = str_replace($found[0], '', $match);
172		}
173
174		// Create page button options
175		$this->_checkOption($match, "noAddPageButton", $data['createPageButtonNs'], false);
176		$this->_checkOption($match, "addPageButtonEach", $data['createPageButtonSubs'], true);
177
178		// Sorting options
179		$this->_checkOption($match, "sortAscending", $data['sort_order'], CATLIST_SORT_ASCENDING);
180		$this->_checkOption($match, "sortDescending", $data['sort_order'], CATLIST_SORT_DESCENDING);
181		$this->_checkOption($match, "sortByTitle", $data['sort_by_title'], true);
182		$this->_checkOption($match, "sortByType", $data['sort_by_type'], true);
183		$this->_checkOption($match, "sortByCreationDate", $data['sort_by_date'], 'created');
184		$this->_checkOption($match, "sortByModifDate", $data['sort_by_date'], 'modified');
185
186		// ACL options
187		$this->_checkOption($match, "ACLshowPage", $data['show_pgnoread'], true);
188		$this->_checkOption($match, "ACLhideNs", $data['hide_nsnotr'], true);
189
190		// Remove other options and warn about
191		for ($found; preg_match("/ (-.*)/", $match, $found); ) {
192			msg(sprintf($this->getLang('unknownoption'), htmlspecialchars($found[1])), -1);
193			$match = str_replace($found[0], '', $match);
194		}
195
196		// Looking for the wanted namespace. Now, only the wanted namespace remains in $match. Then clean the namespace id
197		$ns = trim($match);
198		if ((boolean)$this->getConf('nswildcards')) {
199			global $ID;
200			$parsepagetemplate_data = array('id' => $ID, 'tpl' => $ns, 'doreplace' => true);
201			$ns = parsePageTemplate($parsepagetemplate_data);
202		}
203		if ($ns == '') $ns = '.'; // If there is nothing, we take the current namespace
204		global $ID;
205		if ($ns[0] == '.') $ns = getNS($ID).':'.$ns; // If it start with a '.', it is a relative path
206		$split = explode(':', $ns);
207		for ($i = 0; $i < count($split); $i++) {
208			if ($split[$i] === '' || $split[$i] === '.') {
209				array_splice($split, $i, 1);
210				$i--;
211			} else if ($split[$i] == '..') {
212				if ($i != 0) {
213					array_splice($split, $i-1, 2);
214					$i -= 2;
215				} else break;
216			}
217		}
218		if (count($split) > 0 && $split[0] == '..') {
219			// Path would be outside the 'pages' directory
220			msg($this->getLang('outofpages'), -1);
221			return false;
222		}
223		$data['ns'] = implode(':', $split);
224		return $data;
225	}
226
227	/**************************************************************************************/
228	/************************************ Tree walking ************************************/
229
230		/* Utility function to check is a given page/namespace ($item) is excluded
231		 * based on the relevant list of blacklisting/whitelisting regexes $arrayRegex
232		 * ( array of array('regex'=>the_regex,'neg'=>false/true) ). The exclusion
233		 * is based on item title, full id or name ($exclutype).
234		 */
235	function _isExcluded ($item, $exclutype, $arrayRegex) {
236		if ($arrayRegex === true) return true;
237		global $conf;
238		if ((strlen($conf['hidepages']) != 0) && preg_match('/'.$conf['hidepages'].'/i', $item['id'])) return true;
239		foreach($arrayRegex as $regex) {
240			if (!is_array($regex)) // temporary, for transitioning to v2021-07-21
241				$regex = array('regex' => $regex, 'neg' => false);
242			$match = preg_match('/'.$regex['regex'].(($exclutype=='title')?'/':'/i'), $item[$exclutype]);
243			if ($regex['neg']) {
244				if ($match === 0)
245					return true;
246			} else {
247				if ($match === 1)
248					return true;
249			}
250		}
251		return false;
252	}
253
254	function _getStartPage ($index_priority, $parid, $parpath, $name, $force) {
255		$exists = false;
256		if ($parid != '') $parid .= ':';
257		global $conf;
258		$index_path_map = array( CATLIST_INDEX_START => $parpath.'/'.$name.'/'.$conf['start'].'.txt',
259		                         CATLIST_INDEX_OUTSIDE => $parpath.'/'.$name.'.txt',
260		                         CATLIST_INDEX_INSIDE => $parpath.'/'.$name.'/'.$name.'.txt' );
261		$index_id_map = array( CATLIST_INDEX_START => $parid .$name.':'.$conf['start'],
262		                       CATLIST_INDEX_OUTSIDE => $parid .$name,
263		                       CATLIST_INDEX_INSIDE => $parid .$name.':'.$name );
264		foreach ($index_priority as $index_type) {
265			if (is_file($index_path_map[$index_type])) {
266				$exists = true;
267				return array(true, $index_id_map[$index_type], $index_path_map[$index_type]);
268			}
269		}
270		if ($force && isset($index_priority[0]))
271			return array(false, $index_id_map[0], null);
272		else
273			return array(false, false, null);
274		// returns ($index_exists, $index_id, $index_filepath)
275	}
276
277	function _getMetadata ($id, $filepath) {
278		$meta = p_get_metadata($id, $key='', $render=METADATA_RENDER_USING_SIMPLE_CACHE);
279		if (!isset($meta['date']['modified']))
280			$meta['date']['modified'] = @filemtime($filepath);
281		if (!isset($meta['contributor']))
282			$meta['contributor'] = $meta['creator'];
283		return $meta;
284	}
285
286		/* Entry function for tree walking, called in render()
287		 *
288		 * $data contains the various options initialized and parsed in handle(), and will be passed along
289		 * the tree walking. Moreover, $data['tree'] is filled by the pages found by _walk_recurse(), and
290		 * will contain the full tree, minus the excluded pages (however, permissions are only evaluated at
291		 * rendering time) and up to the max depth. _walk() prepares and start the tree walking.
292		 */
293	function _walk (&$data) {
294		global $conf;
295
296			// Get the directory path from namespace id, and check if it exists
297		$ns = $data['ns'];
298		if($ns == '%%CURRENT_NAMESPACE%%')
299			$ns = getNS(cleanID(getID())); // update namespace to the one currently displayed
300		$path = str_replace(':', '/', $ns);
301		$path = $conf['datadir'].'/'.utf8_encodeFN($path);
302		if (!is_dir($path)) {
303			if ($data['show_notfound_error'])
304				msg(sprintf($this->getLang('dontexist'), $ns), -1);
305			return false;
306		}
307
308			// Info on the main page (the "header" page)
309		$main = array( 'id' => $ns.':',
310		               'exist' => false,
311		               'title' => NULL );
312		resolve_pageid('', $main['id'], $main['exist']);
313		if ($data['headTitle'] !== NULL)
314			$main['title'] = $data['headTitle'];
315		else {
316			if ($data['useheading'] && $main['exist'])
317				$main['title'] = p_get_first_heading($main['id'], true);
318			if (is_null($main['title'])) {
319				$ex = explode(':', $ns);
320				$main['title'] = end($ex);
321			}
322		}
323		$data['main'] = $main;
324
325			// Preparing other stuff
326		if (!isset($data['sort_collator']) || $data['sort_collator'] == "")
327			$data['sort_collator'] = NULL;
328		else {
329			$locale = $data['sort_collator'];
330			$coll = collator_create($locale);
331			if (!isset($coll)) {
332				msg("catlist sorting: can't create Collator object: ".intl_get_error_message(), -1);
333				$data['sort_collator'] = NULL;
334			} else {
335				$coll->setAttribute(Collator::CASE_FIRST, Collator::UPPER_FIRST);
336				$coll->setAttribute(Collator::NUMERIC_COLLATION, Collator::ON);
337				$data['sort_collator'] = $coll;
338			}
339		}
340
341			// Start the recursion
342		if (!isset($data['excludelist'])) // temporary, for transitioning to v2021-07-21
343			$data['excludelist'] = array();
344		$data['tree'] = array();
345		$data['index_pages'] = array( $main['id'] );
346		$this->_walk_recurse($data, $path, $ns, "", false, false, 1/*root depth is 1*/, $data['tree']/*root*/);
347		return true;
348	}
349
350		/* Recursive function for tree walking.
351		 *
352		 * Scans the current namespace by looking directly at the filesystem directory
353		 * for .txt files (pages) and sub-directories (namespaces). Excludes pages/namespaces
354		 * based on the various exclusion options. The current/local directory path, namesapce
355		 * ID and relative namespace ID are respectively $path, $ns and $relns.
356		 * $data is described above. $data['tree'] is not modified directly, but only through
357		 * $_TREE which is the *local* tree view (ie. a reference of a $data['tree'] node) and
358		 * where found children are added. Optionally sorts this list of children.
359		 * The local tree depth is $depth. $excluPages, $excluNS are flags indicates if the
360		 * sub-pages/namespaces should be excluded. Fills $data['index_pages'] with all
361		 * namespace IDs where an index has been found.
362		 */
363	function _walk_recurse (&$data, $path, $ns, $relns, $excluPages, $excluNS, $depth, &$_TREE) {
364		$scanDirs = @scandir($path, SCANDIR_SORT_NONE);
365		if ($scanDirs === false) {
366			msg("catlist: can't open directory of namespace ".$ns, -1);
367			return;
368		}
369		foreach ($scanDirs as $file) {
370			if ($file[0] == '.' || $file[0] == '_') continue;
371			$name = utf8_decodeFN(str_replace('.txt', '', $file));
372			$id = ($ns == '') ? $name : $ns.':'.$name;
373			$rel_id = ($relns == '') ? $name : $relns.':'.$name;
374			$item = array('id' => $id, 'rel_id' => $rel_id, 'name' => $name, 'title' => NULL);
375
376				// ID exclusion
377			if (in_array($rel_id, $data['excludelist'])) continue;
378
379				// It's a namespace
380			if (is_dir($path.'/'.$file)) {
381					// Index page of the namespace
382				list($index_exists, $index_id, $index_filepath) = $this->_getStartPage($data['index_priority'], $ns, $path, $name, ($data['nsLinks']==CATLIST_NSLINK_FORCE));
383				if ($index_exists)
384					$data['index_pages'][] = $index_id;
385					// Exclusion
386				if ($excluNS) continue;
387				if ($this->_isExcluded($item, $data['exclutype'], $data['excluns'])) continue;
388					// Namespace
389				if ($index_exists) {
390					$item['metadata'] = $this->_getMetadata($index_id, $index_filepath);
391					if ($data['nsuseheading'] && isset($item['metadata']['title']))
392						$item['title'] = $item['metadata']['title'];
393				}
394				if (is_null($item['title']))
395					$item['title'] = $name;
396				$item['linkdisp'] = ($index_exists && ($data['nsLinks']==CATLIST_NSLINK_AUTO)) || ($data['nsLinks']==CATLIST_NSLINK_FORCE);
397				$item['linkid'] = $index_id;
398					// Button
399				$item['buttonid'] = $data['createPageButtonSubs'] ? $id.':' : NULL;
400					// Recursion if wanted
401				$item['_'] = array();
402				$okdepth = ($depth < $data['maxdepth']) || ($data['maxdepth'] == 0);
403				$exclude_content = $this->_isExcluded($item, $data['exclutype'], $data['exclunsall'])
404				                   || in_array($rel_id.':', $data['excludelist']);
405				if (!$exclude_content && $okdepth) {
406					$exclunspages = $this->_isExcluded($item, $data['exclutype'], $data['exclunspages']);
407					$exclunsns = $this->_isExcluded($item, $data['exclutype'], $data['exclunsns']);
408					$this->_walk_recurse($data, $path.'/'.$file, $id, $rel_id, $exclunspages, $exclunsns, $depth+1, $item['_']);
409				}
410					// Tree
411				$_TREE[] = $item;
412			} else
413
414				// It's a page
415			if (!$excluPages) {
416				if (substr($file, -4) != ".txt") continue;
417					// Page title
418				$item['metadata'] = $this->_getMetadata($id, $file);
419				if ($data['useheading'] && isset($item['metadata']['title'])) {
420					$item['title'] = $item['metadata']['title'];
421				}
422				if (is_null($item['title']))
423					$item['title'] = $name;
424					// Exclusion
425				if ($this->_isExcluded($item, $data['exclutype'], $data['exclupage'])) continue;
426					// Tree
427				$_TREE[] = $item;
428			}
429
430				// Sorting
431			if ($data['sort_order'] != CATLIST_SORT_NONE) {
432				usort($_TREE, function ($a, $b) use ($data) {
433					$a_is_folder = isset($a['_']);
434					$b_is_folder = isset($b['_']);
435					// if one or the other is folder, comparison is done
436					if ($data['sort_by_type'] && ($a_is_folder xor $b_is_folder ))
437						return $b_is_folder;
438					// else, compare date or name
439					if ($data['sort_by_date'] === false) {
440						// by name
441						$a_title = ($data['sort_by_title'] ? $a['title'] : $a['name']);
442						$b_title = ($data['sort_by_title'] ? $b['title'] : $b['name']);
443						if (!is_null($data['sort_collator']))
444							$r = $data['sort_collator']->compare($a_title, $b_title);
445						else
446							$r = strnatcasecmp($a_title, $b_title);
447					} else {
448						// by date
449						$field = $data['sort_by_date'];
450						$a_date = (isset($a['metadata']['date'][$field]) ? $a['metadata']['date'][$field] : 0);
451						$b_date = (isset($b['metadata']['date'][$field]) ? $b['metadata']['date'][$field] : 0);
452						$r = $a_date <=> $b_date;
453					}
454					if ($data['sort_order'] == CATLIST_SORT_DESCENDING)
455						$r *= -1;
456					return $r;
457				});
458			}
459		}
460	}
461
462	/***********************************************************************************/
463	/************************************ Rendering ************************************/
464
465	function render ($mode, Doku_Renderer $renderer, $data) {
466		if (!is_array($data)) return false;
467		$ns = $data['ns'];
468
469			// Disabling cache
470		if ($data['nocache'])
471			$renderer->nocache();
472
473			// Walk namespace tree
474		$r = $this->_walk($data);
475		if ($r == false) return false;
476
477			// Write params for the add page button
478		global $conf;
479		if (!isset($data['pagename_sanitize'])) // temporary, for transitioning to v2022-06-25
480			$data['pagename_sanitize'] = true;
481		$renderer->doc .= '<script type="text/javascript"> catlist_baseurl = "'.DOKU_URL.'"; catlist_basescript = "'.DOKU_SCRIPT.'"; catlist_useslash = '.$conf['useslash'].'; catlist_userewrite = '.$conf['userewrite'].'; catlist_sepchar = "'.$conf['sepchar'].'"; catlist_deaccent = '.$conf['deaccent'].'; catlist_pagename_sanitize = '.$data['pagename_sanitize'].'; </script>';
482
483			// Display headline
484		if ($data['head']) {
485			$html_tag_small = ($data['nsInBold']) ? 'strong' : 'span';
486			$html_tag = ($data['smallHead']) ? $html_tag_small : $data['hn'];
487			$renderer->doc .= '<'.$html_tag.' class="catlist-head">';
488			$main = $data['main'];
489			if (($main['exist'] && $data['linkStartHead'] && !($data['nsLinks']==CATLIST_NSLINK_NONE)) || ($data['nsLinks']==CATLIST_NSLINK_FORCE))
490				$renderer->internallink(':'.$main['id'], $main['title']);
491			else
492				$renderer->doc .= htmlspecialchars($main['title']);
493			$renderer->doc .= '</'.$html_tag.'>';
494		}
495
496			// Recurse and display
497		$global_ul_attr = "";
498		if ($data['columns'] != 0) {
499			$global_ul_attr = 'column-count: '.$data['columns'].';';
500			$global_ul_attr = 'style="-webkit-'.$global_ul_attr.' -moz-'.$global_ul_attr.' '.$global_ul_attr.'" ';
501			$global_ul_attr .= 'class="catlist_columns catlist-nslist" ';
502		} else {
503			$global_ul_attr = 'class="catlist-nslist" ';
504		}
505		if ($data['displayType'] == CATLIST_DISPLAY_LIST) $renderer->doc .= '<ul '.$global_ul_attr.'>';
506		$this->_recurse($renderer, $data, $data['tree']);
507		$perm_create = $this->_cached_quickaclcheck($ns.':*') >= AUTH_CREATE;
508		$ns_button = ($ns == '') ? '' : $ns.':';
509		if ($data['createPageButtonNs'] && $perm_create) $this->_displayAddPageButton($renderer, $ns_button, $data['displayType']);
510		if ($data['displayType'] == CATLIST_DISPLAY_LIST) $renderer->doc .= '</ul>';
511
512		return true;
513	}
514
515		/* Just cache the calls to auth_quickaclcheck, mainly for _any_child_perms */
516	function _cached_quickaclcheck($id) {
517		static $cache = array();
518		if (!isset($cache[$id]))
519			$cache[$id] = auth_quickaclcheck($id);
520		return $cache[$id];
521	}
522
523		/* Walk the tree to see if any page/namespace below this has read access access, for show_leading_ns option */
524	function _any_child_perms ($data, $_TREE) {
525		foreach ($_TREE as $item) {
526			if (isset($item['_'])) {
527				$perms = $this->_cached_quickaclcheck($item['id'].':*');
528				if ($perms >= AUTH_READ || $this->_any_child_perms($data, $item['_']))
529					return true;
530			} else {
531				$perms = $this->_cached_quickaclcheck($item['id']);
532				if ($perms >= AUTH_READ)
533					return true;
534			}
535		}
536		return false;
537	}
538
539	function _recurse (&$renderer, $data, $_TREE) {
540		foreach ($_TREE as $item) {
541			if (isset($item['_'])) {
542				// It's a namespace
543				$perms = $this->_cached_quickaclcheck($item['id'].':*');
544				$perms_exemption = $data['show_perms'];
545				// If we actually care about not showing the namespace because of permissions :
546				if ($perms < AUTH_READ && !$perms_exemption) {
547					// If show_leading_ns activated, walk the tree below this, see if any page/namespace below this has access
548					if ($data['show_leading_ns'] && $this->_any_child_perms($data, $item['_'])) {
549						$perms_exemption = true;
550					} else {
551						if ($data['hide_nsnotr']) continue;
552						if ($data['show_pgnoread'])
553							$perms_exemption = true; // Add exception if show_pgnoread enabled, but hide_nsnotr prevails
554					}
555				}
556				$linkdisp = $item['linkdisp'] && ($perms >= AUTH_READ);
557				$item['buttonid'] = ($perms >= AUTH_CREATE) ? $item['buttonid'] : NULL;
558				$this->_displayNSBegin($renderer, $data, $item['title'], $linkdisp, $item['linkid'], ($data['show_perms'] ? $perms : NULL));
559				if ($perms >= AUTH_READ || $perms_exemption)
560					$this->_recurse($renderer, $data, $item['_']);
561				$this->_displayNSEnd($renderer, $data['displayType'], $item['buttonid']);
562			} else {
563				// It's a page
564				$perms = $this->_cached_quickaclcheck($item['id']);
565				if ($perms < AUTH_READ && !$data['show_perms'] && !$data['show_pgnoread'])
566					continue;
567				if ($data['hide_index'] && in_array($item['id'], $data['index_pages']))
568					continue;
569				$displayLink = $perms >= AUTH_READ || $data['show_perms'];
570				$this->_displayPage($renderer, $item, $data['displayType'], ($data['show_perms'] ? $perms : NULL), $displayLink);
571			}
572		}
573	}
574
575	function _displayNSBegin (&$renderer, $data, $title, $displayLink, $idLink, $perms) {
576		if ($data['displayType'] == CATLIST_DISPLAY_LIST) {
577			$warper_ns = ($data['nsInBold']) ? 'strong' : 'span';
578			$renderer->doc .= '<li class="catlist-ns"><'.$warper_ns.' class="li catlist-nshead">';
579			if ($displayLink) $renderer->internallink($idLink, $title);
580			else $renderer->doc .= htmlspecialchars($title);
581			if ($perms !== NULL) $renderer->doc .= ' [ns, perm='.$perms.']';
582			$renderer->doc .= '</'.$warper_ns.'>';
583			$renderer->doc .= '<ul class="catlist-nslist">';
584		}
585		else if ($data['displayType'] == CATLIST_DISPLAY_LINE) {
586			if ($data['nsInBold']) $renderer->doc .= '<strong>';
587			if ($displayLink) $renderer->internallink($idLink, $title);
588			else $renderer->doc .= htmlspecialchars($title);
589			if ($data['nsInBold']) $renderer->doc .= '</strong>';
590			$renderer->doc .= '[ ';
591		}
592	}
593
594	function _displayNSEnd (&$renderer, $displayType, $nsAddButton) {
595		if (!is_null($nsAddButton)) $this->_displayAddPageButton($renderer, $nsAddButton, $displayType);
596		if ($displayType == CATLIST_DISPLAY_LIST) $renderer->doc .= '</ul></li>';
597		else if ($displayType == CATLIST_DISPLAY_LINE) $renderer->doc .= '] ';
598	}
599
600	function _displayPage (&$renderer, $item, $displayType, $perms, $displayLink) {
601		if ($displayType == CATLIST_DISPLAY_LIST) {
602			$renderer->doc .= '<li class="catlist-page">';
603			if ($displayLink) $renderer->internallink(':'.$item['id'], $item['title']);
604			else $renderer->doc .= htmlspecialchars($item['title']);
605			if ($perms !== NULL) $renderer->doc .= ' [page, perm='.$perms.']';
606			$renderer->doc .= '</li>';
607		} else if ($displayType == CATLIST_DISPLAY_LINE) {
608			$renderer->internallink(':'.$item['id'], $item['title']);
609			$renderer->doc .= ' ';
610		}
611	}
612
613	function _displayAddPageButton (&$renderer, $ns, $displayType) {
614		$html = ($displayType == CATLIST_DISPLAY_LIST) ? 'li' : 'span';
615		$renderer->doc .= '<'.$html.' class="catlist_addpage"><button class="button" onclick="catlist_button_add_page(this,\''.$ns.'\')">'.$this->getLang('addpage').'</button></'.$html.'>';
616	}
617
618}
619