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