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,
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')], 'sort_by_title' => false, 'sort_by_type' => false,
83		              'hide_index' => (boolean)$this->getConf('hide_index'),
84		              'index_priority' => array(),
85		              'nocache' => (boolean)$this->getConf('nocache'),
86		              'hide_nsnotr' => (boolean)$this->getConf('hide_acl_nsnotr'), 'show_pgnoread' => false, 'show_perms' => (boolean)$this->getConf('show_acl'),
87		              'show_leading_ns' => (boolean)$this->getConf('show_leading_ns'),
88		              'show_notfound_error' => true );
89
90		$index_priority = explode(',', $this->getConf('index_priority'));
91		foreach ($index_priority as $index_type) {
92			if (!array_key_exists($index_type, $_index_priority_map)) {
93				msg("catlist: invalid index type in index_priority", -1);
94				return false;
95			}
96			$data['index_priority'][] = $_index_priority_map[$index_type];
97		}
98		$match = utf8_substr($match, 9, -1).' ';
99
100		// Display options
101		$this->_checkOption($match, "displayList", $data['displayType'], CATLIST_DISPLAY_LIST);
102		$this->_checkOption($match, "displayLine", $data['displayType'], CATLIST_DISPLAY_LINE);
103		$this->_checkOption($match, "noNSInBold", $data['nsInBold'], false);
104		if (preg_match("/-expandButton:([0-9]+)/i", $match, $found)) {
105			$data['expand'] = intval($found[1]);
106			$match = str_replace($found[0], '', $match);
107		}
108		$this->_checkOption($match, "noHeadTitle", $data['useheading'], false);
109		$this->_checkOption($match, "forceHeadTitle", $data['useheading'], true);
110		$data['nsuseheading'] = $data['useheading'];
111		$this->_checkOption($match, "noNSHeadTitle", $data['nsuseheading'], false);
112		$this->_checkOption($match, "hideNotFoundMsg", $data['show_notfound_error'], false);
113
114		// Namespace options
115		$this->_checkOption($match, "forceLinks", $data['nsLinks'], CATLIST_NSLINK_FORCE); // /!\ Deprecated
116		$this->_checkOptionParam($match, "nsLinks", $data['nsLinks'], array( "none" => CATLIST_NSLINK_NONE,
117		                                                                     "auto" => CATLIST_NSLINK_AUTO,
118		                                                                     "force" => CATLIST_NSLINK_FORCE ));
119
120		// Exclude options
121		for ($found; preg_match("/-(exclu(page|ns|nsall|nspages|nsns)!?):\"([^\\/\"]+)\" /i", $match, $found); ) {
122			$option = strtolower($found[1]);
123			// is regex negated ?
124			if (substr($option,-1) == "!") {
125				$data[substr($option,0,-1)][] = array('regex' => $found[3], 'neg' => true);
126			} else {
127				$data[$option][] = array('regex' => $found[3], 'neg' => false);
128			}
129			$match = str_replace($found[0], '', $match);
130		}
131		for ($found; preg_match("/-(exclu(page|ns|nsall|nspages|nsns)) /i", $match, $found); ) {
132			$data[strtolower($found[1])] = true;
133			$match = str_replace($found[0], '', $match);
134		}
135		// Exclude type (exclude based on id, name, or title)
136		$this->_checkOption($match, "excludeOnID", $data['exclutype'], 'id');
137		$this->_checkOption($match, "excludeOnName", $data['exclutype'], 'name');
138		$this->_checkOption($match, "excludeOnTitle", $data['exclutype'], 'title');
139		// Exclude page/namespace id list
140		$data['excludelist'] = array();
141		for ($found; preg_match("/-exclude:\\{([^\\}]*)\\} /", $match, $found); ) {
142			$list = explode(' ', $found[1]);
143			$data['excludelist'] = array_merge($data['excludelist'], $list);
144			$match = str_replace($found[0], '', $match);
145		}
146
147		// Max depth
148		if (preg_match("/-maxDepth:([0-9]+)/i", $match, $found)) {
149			$data['maxdepth'] = intval($found[1]);
150			$match = str_replace($found[0], '', $match);
151		}
152
153		// Columns
154		if (preg_match("/-columns:([0-9]+)/i", $match, $found)) {
155			$data['columns'] = intval($found[1]);
156			$match = str_replace($found[0], '', $match);
157		}
158
159		// Head options
160		$this->_checkOption($match, "noHead", $data['head'], false);
161		$this->_checkOption($match, "showHead", $data['head'], true);
162		$this->_checkOption($match, "smallHead", $data['smallHead'], true);
163		$this->_checkOption($match, "noLinkStartHead", $data['linkStartHead'], false);
164		if (preg_match("/-(h[1-5])/i", $match, $found)) {
165			$data['hn'] = $found[1];
166			$match = str_replace($found[0], '', $match);
167		}
168		if (preg_match("/-titleHead:\"([^\"]*)\"/i", $match, $found)) {
169			$data['headTitle'] = $found[1];
170			$match = str_replace($found[0], '', $match);
171		}
172
173		// Create page button options
174		$this->_checkOption($match, "noAddPageButton", $data['createPageButtonNs'], false);
175		$this->_checkOption($match, "addPageButtonEach", $data['createPageButtonSubs'], true);
176
177		// Sorting options
178		$this->_checkOption($match, "sortAscending", $data['sort_order'], CATLIST_SORT_ASCENDING);
179		$this->_checkOption($match, "sortDescending", $data['sort_order'], CATLIST_SORT_DESCENDING);
180		$this->_checkOption($match, "sortByTitle", $data['sort_by_title'], true);
181		$this->_checkOption($match, "sortByType", $data['sort_by_type'], true);
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 ($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, &$exists) {
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 $index_id_map[$index_type];
265			}
266		}
267		if ($force && isset($index_priority[0]))
268			return $index_id_map[0];
269		else
270			return false;
271	}
272
273		/* Entry function for tree walking, called in render()
274		 *
275		 * $data contains the various options initialized and parsed in handle(), and will be passed along
276		 * the tree walking. Moreover, $data['tree'] is filled by the pages found by _walk_recurse(), and
277		 * will contain the full tree, minus the excluded pages (however, permissions are only evaluated at
278		 * rendering time) and up to the max depth. _walk() prepares and start the tree walking.
279		 */
280	function _walk (&$data) {
281		global $conf;
282
283			// Get the directory path from namespace id, and check if it exists
284		$ns = $data['ns'];
285		if($ns == '%%CURRENT_NAMESPACE%%')
286			$ns = getNS(cleanID(getID())); // update namespace to the one currently displayed
287		$path = str_replace(':', '/', $ns);
288		$path = $conf['datadir'].'/'.utf8_encodeFN($path);
289		if (!is_dir($path)) {
290			if ($data['show_notfound_error'])
291				msg(sprintf($this->getLang('dontexist'), $ns), -1);
292			return false;
293		}
294
295			// Info on the main page (the "header" page)
296		$main = array( 'id' => $ns.':',
297		               'exist' => false,
298		               'title' => NULL );
299		resolve_pageid('', $main['id'], $main['exist']);
300		if ($data['headTitle'] !== NULL)
301			$main['title'] = $data['headTitle'];
302		else {
303			if ($data['useheading'] && $main['exist'])
304				$main['title'] = p_get_first_heading($main['id'], true);
305			if (is_null($main['title'])) {
306				$ex = explode(':', $ns);
307				$main['title'] = end($ex);
308			}
309		}
310		$data['main'] = $main;
311
312			// Start the recursion
313		if (!isset($data['excludelist'])) // temporary, for transitioning to v2021-07-21
314			$data['excludelist'] = array();
315		$data['tree'] = array();
316		$data['index_pages'] = array( $main['id'] );
317		$this->_walk_recurse($data, $path, $ns, "", false, false, 1/*root depth is 1*/, $data['tree']/*root*/);
318		return true;
319	}
320
321		/* Recursive function for tree walking.
322		 *
323		 * Scans the current namespace by looking directly at the filesystem directory
324		 * for .txt files (pages) and sub-directories (namespaces). Excludes pages/namespaces
325		 * based on the various exclusion options. The current/local directory path, namesapce
326		 * ID and relative namespace ID are respectively $path, $ns and $relns.
327		 * $data is described above. $data['tree'] is not modified directly, but only through
328		 * $_TREE which is the *local* tree view (ie. a reference of a $data['tree'] node) and
329		 * where found children are added. Optionally sorts this list of children.
330		 * The local tree depth is $depth. $excluPages, $excluNS are flags indicates if the
331		 * sub-pages/namespaces should be excluded. Fills $data['index_pages'] with all
332		 * namespace IDs where an index has been found.
333		 */
334	function _walk_recurse (&$data, $path, $ns, $relns, $excluPages, $excluNS, $depth, &$_TREE) {
335		$scanDirs = @scandir($path, SCANDIR_SORT_NONE);
336		if ($scanDirs === false) {
337			msg("catlist: can't open directory of namespace ".$ns, -1);
338			return;
339		}
340		foreach ($scanDirs as $file) {
341			if ($file[0] == '.' || $file[0] == '_') continue;
342			$name = utf8_decodeFN(str_replace('.txt', '', $file));
343			$id = ($ns == '') ? $name : $ns.':'.$name;
344			$rel_id = ($relns == '') ? $name : $relns.':'.$name;
345			$item = array('id' => $id, 'rel_id' => $rel_id, 'name' => $name, 'title' => NULL);
346
347				// ID exclusion
348			if (in_array($rel_id, $data['excludelist'])) continue;
349
350				// It's a namespace
351			if (is_dir($path.'/'.$file)) {
352					// Index page of the namespace
353				$index_exists = false;
354				$index_id = $this->_getStartPage($data['index_priority'], $ns, $path, $name, ($data['nsLinks']==CATLIST_NSLINK_FORCE), $index_exists);
355				if ($index_exists)
356					$data['index_pages'][] = $index_id;
357					// Exclusion
358				if ($excluNS) continue;
359				if ($this->_isExcluded($item, $data['exclutype'], $data['excluns'])) continue;
360					// Namespace
361				if ($index_exists && $data['nsuseheading'])
362					$item['title'] = p_get_first_heading($index_id, true);
363				if (is_null($item['title']))
364					$item['title'] = $name;
365				$item['linkdisp'] = ($index_exists && ($data['nsLinks']==CATLIST_NSLINK_AUTO)) || ($data['nsLinks']==CATLIST_NSLINK_FORCE);
366				$item['linkid'] = $index_id;
367					// Button
368				$item['buttonid'] = $data['createPageButtonSubs'] ? $id.':' : NULL;
369					// Recursion if wanted
370				$item['_'] = array();
371				$okdepth = ($depth < $data['maxdepth']) || ($data['maxdepth'] == 0);
372				$exclude_content = $this->_isExcluded($item, $data['exclutype'], $data['exclunsall'])
373				                   || in_array($rel_id.':', $data['excludelist']);
374				if (!$exclude_content && $okdepth) {
375					$exclunspages = $this->_isExcluded($item, $data['exclutype'], $data['exclunspages']);
376					$exclunsns = $this->_isExcluded($item, $data['exclutype'], $data['exclunsns']);
377					$this->_walk_recurse($data, $path.'/'.$file, $id, $rel_id, $exclunspages, $exclunsns, $depth+1, $item['_']);
378				}
379					// Tree
380				$_TREE[] = $item;
381			} else
382
383				// It's a page
384			if (!$excluPages) {
385				if (substr($file, -4) != ".txt") continue;
386					// Page title
387				if ($data['useheading']) {
388					$title = p_get_first_heading($id, true);
389					if (!is_null($title))
390						$item['title'] = $title;
391				}
392				if (is_null($item['title']))
393					$item['title'] = $name;
394					// Exclusion
395				if ($this->_isExcluded($item, $data['exclutype'], $data['exclupage'])) continue;
396					// Tree
397				$_TREE[] = $item;
398			}
399
400				// Sorting
401			if ($data['sort_order'] != CATLIST_SORT_NONE) {
402				usort($_TREE, function ($a, $b) use ($data) {
403					if ($data['sort_by_type'] && ( isset($a['_']) xor isset($b['_']) ))
404						return isset($b['_']);
405					$a_title = ($data['sort_by_title'] ? $a['title'] : $a['name']);
406					$b_title = ($data['sort_by_title'] ? $b['title'] : $b['name']);
407					$r = strnatcasecmp($a_title, $b_title);
408					if ($data['sort_order'] == CATLIST_SORT_DESCENDING)
409						$r *= -1;
410					return $r;
411				});
412			}
413		}
414	}
415
416	/***********************************************************************************/
417	/************************************ Rendering ************************************/
418
419	function render ($mode, Doku_Renderer $renderer, $data) {
420		if (!is_array($data)) return false;
421		$ns = $data['ns'];
422
423			// Disabling cache
424		if ($data['nocache'])
425			$renderer->nocache();
426
427			// Walk namespace tree
428		$r = $this->_walk($data);
429		if ($r == false) return false;
430
431			// Write params for the add page button
432		global $conf;
433		$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'].'; </script>';
434
435			// Display headline
436		if ($data['head']) {
437			$html_tag_small = ($data['nsInBold']) ? 'strong' : 'span';
438			$html_tag = ($data['smallHead']) ? $html_tag_small : $data['hn'];
439			$renderer->doc .= '<'.$html_tag.' class="catlist-head">';
440			$main = $data['main'];
441			if (($main['exist'] && $data['linkStartHead'] && !($data['nsLinks']==CATLIST_NSLINK_NONE)) || ($data['nsLinks']==CATLIST_NSLINK_FORCE))
442				$renderer->internallink(':'.$main['id'], $main['title']);
443			else
444				$renderer->doc .= htmlspecialchars($main['title']);
445			$renderer->doc .= '</'.$html_tag.'>';
446		}
447
448			// Recurse and display
449		$global_ul_attr = "";
450		if ($data['columns'] != 0) {
451			$global_ul_attr = 'column-count: '.$data['columns'].';';
452			$global_ul_attr = 'style="-webkit-'.$global_ul_attr.' -moz-'.$global_ul_attr.' '.$global_ul_attr.'" ';
453			$global_ul_attr .= 'class="catlist_columns catlist-nslist" ';
454		} else {
455			$global_ul_attr = 'class="catlist-nslist" ';
456		}
457		if ($data['displayType'] == CATLIST_DISPLAY_LIST) $renderer->doc .= '<ul '.$global_ul_attr.'>';
458		$this->_recurse($renderer, $data, $data['tree']);
459		$perm_create = $this->_cached_quickaclcheck($ns.':*') >= AUTH_CREATE;
460		$ns_button = ($ns == '') ? '' : $ns.':';
461		if ($data['createPageButtonNs'] && $perm_create) $this->_displayAddPageButton($renderer, $ns_button, $data['displayType']);
462		if ($data['displayType'] == CATLIST_DISPLAY_LIST) $renderer->doc .= '</ul>';
463
464		return true;
465	}
466
467		/* Just cache the calls to auth_quickaclcheck, mainly for _any_child_perms */
468	function _cached_quickaclcheck($id) {
469		static $cache = array();
470		if (!isset($cache[$id]))
471			$cache[$id] = auth_quickaclcheck($id);
472		return $cache[$id];
473	}
474
475		/* Walk the tree to see if any page/namespace below this has read access access, for show_leading_ns option */
476	function _any_child_perms ($data, $_TREE) {
477		foreach ($_TREE as $item) {
478			if (isset($item['_'])) {
479				$perms = $this->_cached_quickaclcheck($item['id'].':*');
480				if ($perms >= AUTH_READ || $this->_any_child_perms($data, $item['_']))
481					return true;
482			} else {
483				$perms = $this->_cached_quickaclcheck($item['id']);
484				if ($perms >= AUTH_READ)
485					return true;
486			}
487		}
488		return false;
489	}
490
491	function _recurse (&$renderer, $data, $_TREE) {
492		foreach ($_TREE as $item) {
493			if (isset($item['_'])) {
494				// It's a namespace
495				$perms = $this->_cached_quickaclcheck($item['id'].':*');
496				$perms_exemption = $data['show_perms'];
497				// If we actually care about not showing the namespace because of permissions :
498				if ($perms < AUTH_READ && !$perms_exemption) {
499					// If show_leading_ns activated, walk the tree below this, see if any page/namespace below this has access
500					if ($data['show_leading_ns'] && $this->_any_child_perms($data, $item['_'])) {
501						$perms_exemption = true;
502					} else {
503						if ($data['hide_nsnotr']) continue;
504						if ($data['show_pgnoread'])
505							$perms_exemption = true; // Add exception if show_pgnoread enabled, but hide_nsnotr prevails
506					}
507				}
508				$linkdisp = $item['linkdisp'] && ($perms >= AUTH_READ);
509				$item['buttonid'] = ($perms >= AUTH_CREATE) ? $item['buttonid'] : NULL;
510				$this->_displayNSBegin($renderer, $data, $item['title'], $linkdisp, $item['linkid'], ($data['show_perms'] ? $perms : NULL));
511				if ($perms >= AUTH_READ || $perms_exemption)
512					$this->_recurse($renderer, $data, $item['_']);
513				$this->_displayNSEnd($renderer, $data['displayType'], $item['buttonid']);
514			} else {
515				// It's a page
516				$perms = $this->_cached_quickaclcheck($item['id']);
517				if ($perms < AUTH_READ && !$data['show_perms'] && !$data['show_pgnoread'])
518					continue;
519				if ($data['hide_index'] && in_array($item['id'], $data['index_pages']))
520					continue;
521				$displayLink = $perms >= AUTH_READ || $data['show_perms'];
522				$this->_displayPage($renderer, $item, $data['displayType'], ($data['show_perms'] ? $perms : NULL), $displayLink);
523			}
524		}
525	}
526
527	function _displayNSBegin (&$renderer, $data, $title, $displayLink, $idLink, $perms) {
528		if ($data['displayType'] == CATLIST_DISPLAY_LIST) {
529			$warper_ns = ($data['nsInBold']) ? 'strong' : 'span';
530			$renderer->doc .= '<li class="catlist-ns"><'.$warper_ns.' class="li catlist-nshead">';
531			if ($displayLink) $renderer->internallink($idLink, $title);
532			else $renderer->doc .= htmlspecialchars($title);
533			if ($perms !== NULL) $renderer->doc .= ' [ns, perm='.$perms.']';
534			$renderer->doc .= '</'.$warper_ns.'>';
535			$renderer->doc .= '<ul class="catlist-nslist">';
536		}
537		else if ($data['displayType'] == CATLIST_DISPLAY_LINE) {
538			if ($data['nsInBold']) $renderer->doc .= '<strong>';
539			if ($displayLink) $renderer->internallink($idLink, $title);
540			else $renderer->doc .= htmlspecialchars($title);
541			if ($data['nsInBold']) $renderer->doc .= '</strong>';
542			$renderer->doc .= '[ ';
543		}
544	}
545
546	function _displayNSEnd (&$renderer, $displayType, $nsAddButton) {
547		if (!is_null($nsAddButton)) $this->_displayAddPageButton($renderer, $nsAddButton, $displayType);
548		if ($displayType == CATLIST_DISPLAY_LIST) $renderer->doc .= '</ul></li>';
549		else if ($displayType == CATLIST_DISPLAY_LINE) $renderer->doc .= '] ';
550	}
551
552	function _displayPage (&$renderer, $item, $displayType, $perms, $displayLink) {
553		if ($displayType == CATLIST_DISPLAY_LIST) {
554			$renderer->doc .= '<li class="catlist-page">';
555			if ($displayLink) $renderer->internallink(':'.$item['id'], $item['title']);
556			else $renderer->doc .= htmlspecialchars($item['title']);
557			if ($perms !== NULL) $renderer->doc .= ' [page, perm='.$perms.']';
558			$renderer->doc .= '</li>';
559		} else if ($displayType == CATLIST_DISPLAY_LINE) {
560			$renderer->internallink(':'.$item['id'], $item['title']);
561			$renderer->doc .= ' ';
562		}
563	}
564
565	function _displayAddPageButton (&$renderer, $ns, $displayType) {
566		$html = ($displayType == CATLIST_DISPLAY_LIST) ? 'li' : 'span';
567		$renderer->doc .= '<'.$html.' class="catlist_addpage"><button class="button" onclick="catlist_button_add_page(this,\''.$ns.'\')">'.$this->getLang('addpage').'</button></'.$html.'>';
568	}
569
570}
571