xref: /plugin/catmenu/syntax/catmenu.php (revision 6983cdfd4483215ff5a1e573925c9c612964e790)
1<?php
2/**
3 * Plugin catmenu
4 * Affiche les pages d’un namespace donné
5 * Auteur: Lortetv
6 */
7
8use dokuwiki\Extension\SyntaxPlugin;
9use dokuwiki\File\PageResolver;
10use dokuwiki\Ui\Index;
11
12class syntax_plugin_catmenu_catmenu extends SyntaxPlugin {
13    /** @var helper_plugin_pagesicon|null|false */
14    private $pagesiconHelper = false;
15
16    public function getType() {
17        return 'substition'; // substitution = remplacer la balise par du contenu
18    }
19
20    public function getPType() {
21        return 'block';
22    }
23
24    public function getSort() { // priorité du plugin par rapport à d'autres
25        return 15;
26    }
27
28    /**
29	 * Reconnaît la syntaxe {{catmenu>[namespace]}}
30     */
31    public function connectTo($mode) { // reconnait la syntaxe utilisé par l'utilisateur
32        $this->Lexer->addSpecialPattern('{{catmenu>.*?}}', $mode, 'plugin_catmenu_catmenu');
33    }
34
35    /**
36     * Nettoie  {{catmenu>[namespace]}}
37     */
38    public function handle($match, $state, $pos, Doku_Handler $handler) {
39        $namespace = trim(substr($match, 10, -2)); // retirer {{visualindex>, }} et les espaces
40        return ['namespace' => $namespace];
41    }
42
43    public function render($mode, Doku_Renderer $renderer, $data) {
44        if ($mode !== 'xhtml') return false;
45
46		global $ID;
47		global $conf;
48
49		$random = uniqid();
50
51		if($data['namespace'] === '.') { // Récupération du namespace courant
52			if(!is_dir($this->namespaceDir($ID))) {
53				$pageNamespaceInfo = $this->getPageNamespaceInfo($ID);
54				if($this->isHomepage($pageNamespaceInfo['pageID'], $pageNamespaceInfo['parentID'])) {
55					$namespace = $pageNamespaceInfo['parentNamespace'];
56				}
57			}
58
59			if(!isset($namespace)) {
60				$namespace = $ID;
61			}
62		}
63		else {
64			$namespace = cleanID($data['namespace']);
65		}
66
67		$pages = $this->getPagesAndSubfoldersItems($namespace);
68		if($pages === false) {
69			$renderer->doc .= '<div>' . hsc($this->getLang('namespace_not_found')) . '</div>';
70			return true;
71		}
72
73		$renderer->doc .= '<div id="catmenu_' . $random . '" class="catmenu" style=""></div>';
74		$renderer->doc .= "<script>
75			let catmenuconf_" . $random . " = { userewrite: '" . $conf['userewrite'] . "', start: '" . $conf['start'] . "' };
76			let catmenuobj_" . $random . " = JSON.parse(`" . htmlspecialchars_decode(json_encode($pages)) . "`);
77			(function initCatmenu_" . $random . "() {
78				const target = document.getElementById('catmenu_" . $random . "');
79				if (!target) return;
80				const renderOnce = function () {
81					if (target.dataset.catmenuRendered === '1') return;
82					if (typeof window.catmenu_generateSectionMenu !== 'function') return;
83					target.dataset.catmenuRendered = '1';
84					target.innerHTML = '';
85					window.catmenu_generateSectionMenu(catmenuconf_" . $random . ", catmenuobj_" . $random . ", target);
86				};
87				if (typeof window.catmenu_generateSectionMenu === 'function') {
88					renderOnce();
89					return;
90				}
91
92				// In edit/preview pages, plugin scripts can load after inline rendering.
93				document.addEventListener('catmenu:ready', function onReady() {
94					renderOnce();
95				}, { once: true });
96
97				setTimeout(function retryCatmenu_" . $random . "() {
98					renderOnce();
99				}, 0);
100			})();
101		</script>";
102
103		return true;
104	}
105
106	/**
107	 * Récupère à la fois les pages et les sous-dossiers d'un namespace
108	 */
109	public function getPagesAndSubfoldersItems($namespace) {
110		global $conf;
111		$skipPageWithoutTitle = (bool)$this->getConf('skip_page_without_title');
112
113		$childrens = @scandir($this->namespaceDir($namespace)); // Récupère les elements
114		if($childrens === false) {
115			return false;
116		}
117
118		$start = $conf['start']; // 'accueil' dans la plupart des temps (dans bpnum:d-s:accueil)
119
120		$items = [];
121		foreach($childrens as $child) { // Boucle sur les elements
122			if ($child[0] == '.' ) { // Remove ., .. and hidden files
123				continue;
124			}
125
126			$childPathInfo = pathinfo($child);
127			$childID = cleanID($childPathInfo['filename']);
128			$childNamespace = cleanID($namespace !== '' ? ($namespace . ':' . $childID) : $childID);
129
130			$childHasExtension = isset($childPathInfo['extension']) && $childPathInfo['extension'] !== '';
131			$isDirNamespace = is_dir($this->namespaceDir($childNamespace));
132			$isPageNamespace = page_exists($childNamespace);
133
134			if(!$childHasExtension && $isDirNamespace) { // Si dossier
135				$pageNamespaceInfo = $this->getPageNamespaceInfo($childNamespace);
136				if($this->isHomepage($childID, (string)$pageNamespaceInfo['parentID'])) {
137					// Flatten namespace "homepage" folders like ns:ns so children stay direct.
138					$subItems = $this->getPagesAndSubfoldersItems($childNamespace);
139					if(is_array($subItems) && $subItems) {
140						$items = array_merge($items, $subItems);
141					}
142					continue;
143				}
144
145				$pageID = null;
146				if(page_exists("$childNamespace:$start")) {	// S'il y a une page d'accueil
147					$pageID = "$childNamespace:$start";
148				}
149				else if(page_exists("$childNamespace:$childID")) { // S'il y a une page du même nom que le dossier dans le dossier
150					$pageID = "$childNamespace:$childID";
151				}
152				else if($isPageNamespace) { // S'il y a une page du même nom que le dossier au même niveau que le dossier
153					$pageID = cleanID($namespace !== '' ? ($namespace . ':' . $childID) : $childID);
154				}
155
156				$permission = auth_quickaclcheck($pageID);
157				if($permission < AUTH_READ) {
158					continue;
159				}
160
161				$title = $pageID ? p_get_first_heading($pageID) : $pageID;
162				if (empty($title)) {
163					if ($skipPageWithoutTitle || empty($pageID)) {
164						continue;
165					}
166					$title = noNS($pageID);
167				}
168
169				$items[] = array(
170					'title' => $title,
171					'url' => $pageID? wl($pageID) : null,
172					'icon' => $this->getPageImage($pageID),
173					'pagesiconUploadUrl' => $this->getPagesiconUploadUrl($pageID ?: $childNamespace),
174					'folderNamespace' => $childNamespace,
175					'namespace' => $childNamespace,
176					'subtree' => $this->getPagesAndSubfoldersItems($childNamespace),
177					'permission' => $permission
178				);
179
180				continue;
181			}
182
183			if(!$isDirNamespace && $isPageNamespace) { // Si page seulement
184				$skipRegex = $this->getConf('skip_file');
185				if (!empty($skipRegex) && preg_match($skipRegex, $childNamespace)) {
186					continue;
187				}
188
189				$pageNamespaceInfo = $this->getPageNamespaceInfo("$namespace:$childID");
190				if($this->isHomepage($childID, $pageNamespaceInfo['parentID'])) {
191					continue;
192				}
193
194				$permission = auth_quickaclcheck($childNamespace);
195				if($permission < AUTH_READ) {
196					continue;
197				}
198
199				$title = p_get_first_heading($childNamespace);
200				if (empty($title)) {
201					if ($skipPageWithoutTitle) {
202						continue;
203					}
204					$title = noNS($childNamespace);
205				}
206
207				$items[] = array(
208					'title' => $title,
209					'url' => $childNamespace? wl($childNamespace) : null,
210					'icon' => $this->getPageImage($childNamespace),
211					'pagesiconUploadUrl' => $this->getPagesiconUploadUrl($childNamespace),
212					'folderNamespace' => $namespace,
213					'namespace' => $childNamespace,
214					'permission' => $permission
215				);
216			}
217		}
218
219		return $items;
220	}
221
222	/**
223	 * Renvoie l'URL de la petite icone (thumbnail) via le helper pagesicon.
224	 * Si aucune icone n'est definie, ne renvoie rien.
225	 */
226	public function getPageImage($page) {
227		if(!$page) return '';
228
229		$page = cleanID((string)$page);
230		if($page === '') return '';
231
232		/** @var helper_plugin_pagesicon|null $helper */
233		$helper = plugin_load('helper', 'pagesicon');
234		if(!$helper) return '';
235
236		$namespace = getNS($page);
237		$pageID = noNS($page);
238		// Prefer new pagesicon API, keep legacy fallback for older versions.
239		if(method_exists($helper, 'getPageIconUrl')) {
240			$mtime = null;
241			$iconUrl = $helper->getPageIconUrl($namespace, $pageID, 'smallorbig', ['width' => 55], $mtime, true);
242			if($iconUrl) return $iconUrl;
243		} else if(method_exists($helper, 'getImageIcon')) {
244			$mtime = null;
245			$withDefaultSupported = false;
246			try {
247				$method = new ReflectionMethod($helper, 'getImageIcon');
248				$withDefaultSupported = $method->getNumberOfParameters() >= 6;
249			} catch (ReflectionException $e) {
250				$withDefaultSupported = false;
251			}
252
253			if($withDefaultSupported) {
254				$iconUrl = $helper->getImageIcon($namespace, $pageID, 'smallorbig', ['width' => 55], $mtime, true);
255			} else {
256				$iconUrl = $helper->getImageIcon($namespace, $pageID, 'smallorbig', ['width' => 55], $mtime);
257			}
258			if($iconUrl) return $iconUrl;
259		}
260
261		$iconMediaID = false;
262		if(method_exists($helper, 'getPageIconId')) {
263			$iconMediaID = $helper->getPageIconId($namespace, $pageID, 'smallorbig');
264		} else if(method_exists($helper, 'getPageImage')) {
265			$withDefaultSupported = false;
266			try {
267				$method = new ReflectionMethod($helper, 'getPageImage');
268				$withDefaultSupported = $method->getNumberOfParameters() >= 4;
269			} catch (ReflectionException $e) {
270				$withDefaultSupported = false;
271			}
272
273			if($withDefaultSupported) {
274				$iconMediaID = $helper->getPageImage($namespace, $pageID, 'smallorbig', true);
275			} else {
276				$iconMediaID = $helper->getPageImage($namespace, $pageID, 'smallorbig');
277			}
278		}
279		if(!$iconMediaID) return '';
280
281		return ml($iconMediaID, ['width' => 55]);
282	}
283
284	private function getPagesiconUploadUrl($namespace) {
285		if ($this->pagesiconHelper === false) {
286			$this->pagesiconHelper = plugin_load('helper', 'pagesicon');
287		}
288		if (!$this->pagesiconHelper) return null;
289		if (!method_exists($this->pagesiconHelper, 'getUploadIconPage')) return null;
290
291		return $this->pagesiconHelper->getUploadIconPage((string)$namespace);
292	}
293
294	public function isHomepage($pageID, $parentID) {
295		global $conf;
296		$startPageID = $conf['start'];
297
298		return $pageID == $startPageID || $pageID == $parentID;
299	}
300
301	public function namespaceDir($namespace) {
302		global $conf;
303		return $conf['datadir'] . '/' . utf8_encodeFN(str_replace(':', '/', $namespace));
304	}
305
306	public function getPageNamespaceInfo($namespace) {
307		$namespaces = explode(':', $namespace);
308
309		return array(
310			'pageNamespace' => $namespace,
311			'pageID' => array_pop($namespaces),
312			'parentNamespace' => implode(':', $namespaces),
313			'parentID' => array_pop($namespaces)
314		);
315	}
316}
317