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