xref: /plugin/visualindex/syntax/visualindex.php (revision 7f5290b8d6d83542fd5b939a9c568e40224c0ec8)
1<?php
2/**
3 * Plugin visualindex
4 * Affiche les pages d'un namespace donné
5 * Auteur: Choimetg, Lortetv
6 */
7
8use dokuwiki\Extension\SyntaxPlugin;
9use dokuwiki\File\PageResolver;
10use dokuwiki\Ui\Index;
11
12class syntax_plugin_visualindex_visualindex extends SyntaxPlugin {
13    /** @var helper_plugin_pagesicon|null|false */
14    private $pagesiconHelper = false;
15
16	private function getMediaLinkTargetAttr() {
17		global $conf;
18		$target = (string)($conf['target']['media'] ?? '');
19		if($target === '') return '';
20		return ' target="' . hsc($target) . '"';
21	}
22
23    private function renderInfoMessage(Doku_Renderer $renderer, $langKey) {
24        $message = $this->getLang($langKey);
25        if(!$message) {
26            $message = 'Nothing to display.';
27        }
28        $renderer->doc .= '<div class="visualindex_info">' . hsc($message) . '</div>';
29    }
30
31    public function getType() {
32        return 'substition'; // substition = remplacer la balise par du contenu (orthographe figée dans l'API DokuWiki)
33    }
34
35    public function getPType() {
36        return 'block';
37    }
38
39    public function getSort() { // priorité du plugin par rapport à d'autres
40        return 10;
41    }
42
43    /**
44	 * Reconnaît la syntaxe {{visualindex>[namespace]}}
45     */
46    public function connectTo($mode) { // reconnait la syntaxe utilisé par l'utilisateur
47        $this->Lexer->addSpecialPattern('{{visualindex>.*?}}', $mode, 'plugin_visualindex_visualindex');
48    }
49
50    /**
51     * Nettoie  {{visualindex>[namespace]}}
52     */
53    public function handle($match, $state, $pos, Doku_Handler $handler) {
54		$paramsString = trim(substr($match, 14, -2));
55		$params = explode(';', $paramsString);
56    	$namespace = trim(array_shift($params));
57
58		$result = ['namespace' => $namespace];
59
60		foreach ($params as $param) {
61			$param = trim($param);
62			$paramParts = explode('=', $param, 2);
63			$paramName = $paramParts[0];
64			$paramValue = isset($paramParts[1])? $paramParts[1] : true;
65			$result[$paramName] = $paramValue;
66		}
67
68		return $result;
69	}
70
71	private function getCurrentNamespace($ID, $getMedias = false) {
72		if(!is_dir($this->namespaceDir($ID, $getMedias))) {
73			$pageNamespaceInfo = $this->getNamespaceInfo($ID);
74			if($this->isHomepage($pageNamespaceInfo['pageID'], $pageNamespaceInfo['parentID'])) {
75				return $pageNamespaceInfo['parentNamespace'];
76			}
77		}
78
79		return $ID;
80	}
81
82    public function render($mode, Doku_Renderer $renderer, $data) {
83		if($mode !== 'xhtml' && $mode !== 'wikiedit') return false;
84
85		global $ID;
86
87		$getMedias = isset($data['medias']) && $data['medias'] || false;
88		$filter = isset($data['filter'])? $data['filter'] : null;
89		$desc = isset($data['desc']) && $data['desc'] || false;
90
91		if($data['namespace'] === '.') { // Récupération du namespace courant
92			$namespace = $this->getCurrentNamespace($ID, $getMedias);
93		}
94		elseif(strpos($data['namespace'], '~') === 0) {
95			$relativeNamespace = cleanID(ltrim($data['namespace'], '~'));
96			$currentNamespace = $this->getCurrentNamespace($ID, $getMedias);
97        	$namespace = $currentNamespace . ':' . $relativeNamespace;
98		}
99		else {
100			$namespace = cleanID($data['namespace']);
101		}
102
103		$items = $this->getItemsAndSubfoldersItems($namespace, $getMedias, $filter, $desc);
104		if($items === false) {
105			$this->renderInfoMessage($renderer, 'namespace_not_found');
106			return true;
107		}
108		if(empty($items)) {
109			$this->renderInfoMessage($renderer, 'empty');
110			return true;
111		}
112
113		// Tri stable : homepages en premier, ordre de scan préservé pour les égaux
114		$idx = 0;
115		foreach ($items as &$item) { $item['_idx'] = $idx++; }
116		unset($item);
117		usort($items, function($a, $b) {
118			$diff = $b['sortID'] - $a['sortID'];
119			return $diff !== 0 ? $diff : ($a['_idx'] - $b['_idx']);
120		});
121
122		$tileWidth = $this->getConf('tile_width');
123		$iconSize  = $this->getConf('icon_size');
124		$textSize  = $this->getConf('text_size');
125		$textColor = $this->getConf('text_color');
126
127		// Styles inline dérivés de la configuration
128		$tileStyle = 'width:' . hsc($tileWidth) . ';';
129		$imgStyle  = 'max-width:' . hsc($iconSize) . ';max-height:' . hsc($iconSize) . ';';
130
131		// -----------------------------
132		// ProseMirror / HTML wrapper
133		// -----------------------------
134		$renderer->doc .= '<span class="plugin_visualindex" '
135			.'data-namespace="'.htmlspecialchars($namespace).'" '
136			.'data-filter="'.htmlspecialchars($filter).'" '
137			.'data-desc="'.($desc ? '1' : '0').'">';
138
139		// -----------------------------
140		// HTML classique pour le rendu visuel
141		// -----------------------------
142		$renderer->doc .= '<div class="visualindex">';
143
144		$renderedItems = 0;
145		foreach ($items as $item) {
146			$pageID        = $item['pageID'];
147			$itemNamespace = $item['namespace'];
148			$pageNamespace = $item['pageNamespace'];
149			$isHomepage    = $item['isHomepage'];
150
151			if($pageNamespace == $ID) {
152				continue;
153			}
154
155			$permission = auth_quickaclcheck($pageNamespace);
156			if($permission < AUTH_READ) {
157				continue;
158			}
159
160			$logoUrl = null;
161			if(!$getMedias) {
162				$title = p_get_first_heading($pageNamespace);
163				if(empty($title)) {
164					continue;
165				}
166			}
167			else {
168				$title = str_replace('_', ' ', $pageID);
169				$logoUrl = $this->getMediaItemImage($pageNamespace);
170			}
171
172			if(!$logoUrl) {
173				$logoUrl = $this->getPageImage($itemNamespace, $pageID);
174			}
175
176			// Afficher le lien de la page ou du sous-dossier
177			$targetAttr = $getMedias ? $this->getMediaLinkTargetAttr() : '';
178			$renderer->doc .= '<a class="vi_tile' . ($isHomepage? ' homepage' : '') . '" style="' . $tileStyle . 'color:' . $textColor . ';font-size:' . $textSize . '" href="'. ($getMedias? ml($pageNamespace) : wl($pageNamespace)) . '"' . $targetAttr . '>';
179				$renderer->doc .= '<div class="vi_content"><img loading="lazy" src="' . $logoUrl . '" style="' . $imgStyle . '" alt="" /><br />' . $title . '</div>';
180				$renderer->doc .= '<div class="vi_vertical_align"></div>';
181			$renderer->doc .= '</a>';
182			$renderedItems++;
183		}
184
185		$renderer->doc .= '</div>';
186
187		if($renderedItems === 0) {
188			$this->renderInfoMessage($renderer, 'empty');
189		}
190
191		$renderer->doc .= '</span>';
192		// -----------------------------
193		// Fin du node ProseMirror
194		// -----------------------------
195
196		return true;
197	}
198
199	private function getPagesiconHelper() {
200		if($this->pagesiconHelper === false) {
201			$this->pagesiconHelper = plugin_load('helper', 'pagesicon');
202		}
203		return $this->pagesiconHelper ?: null;
204	}
205
206	private function getDefaultImageUrl() {
207		$defaultImage = cleanID((string)$this->getConf('default_image'));
208		if($defaultImage !== '' && @file_exists(mediaFN($defaultImage))) {
209			return ml($defaultImage, ['width' => 55]);
210		}
211
212		return '/lib/plugins/visualindex/images/default_image.png';
213	}
214
215	private function getMediaItemImage($mediaID) {
216		$mediaID = cleanID((string)$mediaID);
217		if($mediaID === '') {
218			return $this->getDefaultImageUrl();
219		}
220
221		$helper = $this->getPagesiconHelper();
222		if((bool)$this->getConf('use_pagesicon') && $helper) {
223			if(method_exists($helper, 'getMediaIconUrl')) {
224				$mtime = null;
225				$iconUrl = $helper->getMediaIconUrl($mediaID, 'bigorsmall', ['width' => 55], $mtime, false);
226				if($iconUrl) return $iconUrl;
227			} else if(method_exists($helper, 'getMediaIcon')) {
228				$mtime = null;
229				$withDefaultSupported = false;
230				try {
231					$method = new ReflectionMethod($helper, 'getMediaIcon');
232					$withDefaultSupported = $method->getNumberOfParameters() >= 5;
233				} catch (ReflectionException $e) {
234					$withDefaultSupported = false;
235				}
236
237				if($withDefaultSupported) {
238					$iconUrl = $helper->getMediaIcon($mediaID, 'bigorsmall', ['width' => 55], $mtime, false);
239				} else {
240					$iconUrl = $helper->getMediaIcon($mediaID, 'bigorsmall', ['width' => 55], $mtime);
241				}
242				if($iconUrl) return $iconUrl;
243			}
244		}
245
246		$childPathInfo = pathinfo(noNS($mediaID));
247		$imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg'];
248		if(isset($childPathInfo['extension']) && in_array(strtolower((string)$childPathInfo['extension']), $imageExtensions, true)) {
249			return ml($mediaID);
250		}
251
252		return $this->getDefaultImageUrl();
253	}
254
255	/**
256	 * Renvoie l'URL de l'icone de la page via pagesicon, sinon image par defaut.
257	 */
258	private function getPageImage($namespace, $pageID = null) {
259		if(!$pageID) {
260			$pageNamespaceInfo = $this->getNamespaceInfo($namespace);
261			$namespace = $pageNamespaceInfo['parentNamespace'];
262			$pageID = $pageNamespaceInfo['pageID'];
263		}
264
265		$helper = $this->getPagesiconHelper();
266		if((bool)$this->getConf('use_pagesicon') && $helper) {
267			if(method_exists($helper, 'getPageIconUrl')) {
268				$mtime = null;
269				$iconUrl = $helper->getPageIconUrl((string)$namespace, (string)$pageID, 'bigorsmall', ['width' => 55], $mtime, false);
270				if($iconUrl) return $iconUrl;
271			} else if(method_exists($helper, 'getImageIcon')) {
272				$mtime = null;
273				$withDefaultSupported = false;
274				try {
275					$method = new ReflectionMethod($helper, 'getImageIcon');
276					$withDefaultSupported = $method->getNumberOfParameters() >= 6;
277				} catch (ReflectionException $e) {
278					$withDefaultSupported = false;
279				}
280
281				if($withDefaultSupported) {
282					$iconUrl = $helper->getImageIcon((string)$namespace, (string)$pageID, 'bigorsmall', ['width' => 55], $mtime, false);
283				} else {
284					$iconUrl = $helper->getImageIcon((string)$namespace, (string)$pageID, 'bigorsmall', ['width' => 55], $mtime);
285				}
286				if($iconUrl) return $iconUrl;
287			}
288		}
289
290		return $this->getDefaultImageUrl();
291	}
292
293
294	private function createListItem($parentNamespace, $pageID, $isHomepage = false) {
295		return array(
296			'pageID' => $pageID,
297			'namespace' => $parentNamespace,
298			'pageNamespace' => cleanID("$parentNamespace:$pageID"),
299			'sortID' => ($isHomepage? 100 : 0),
300			'isHomepage' => $isHomepage
301		);
302	}
303
304	/**
305	 * Récupère à la fois les pages et les sous-dossiers d'un namespace
306	 */
307	private function getItemsAndSubfoldersItems($namespace, $getMedias = false, $filter = null, $desc = false) {
308		global $conf;
309
310		$childrens = @scandir($this->namespaceDir($namespace, $getMedias), $desc? SCANDIR_SORT_DESCENDING : SCANDIR_SORT_ASCENDING);
311		if($childrens === false) {
312			if($getMedias) {
313				$childrens = @scandir($this->namespaceDir($namespace));
314				if($childrens != false) {
315					return [];
316				}
317			}
318
319			return false;
320		}
321
322		$start = $conf['start']; // page d'accueil du namespace
323
324		$finalPattern = null;
325		if($filter) {
326			$parts = explode('|', $filter);
327			$regexParts = [];
328			foreach ($parts as $part) {
329				$pattern = preg_quote($part, '/');
330				$pattern = str_replace('\*', '.*', $pattern);
331				$regexParts[] = '^' . $pattern . '$';
332			}
333
334			$finalPattern = '/(' . implode('|', $regexParts) . ')/i';
335		}
336
337		$items = [];
338		foreach($childrens as $child) {
339			if($child[0] == '.' ) {
340				continue;
341			}
342
343			if($finalPattern && !preg_match($finalPattern, $child)) {
344				continue;
345			}
346
347			$childPathInfo = pathinfo($child);
348			$childID = cleanID($childPathInfo['filename']);
349			$childNamespace = cleanID("$namespace:$childID");
350
351			$childHasExtension = isset($childPathInfo['extension']) && $childPathInfo['extension'] !== '';
352			$isDirNamespace = is_dir($this->namespaceDir($childNamespace, $getMedias));
353			$isPageNamespace = page_exists($childNamespace);
354
355			if($getMedias) {
356				if($childHasExtension) {
357					$items[] = $this->createListItem($namespace, $child);
358				}
359				continue;
360			}
361
362			if(!$childHasExtension && $isDirNamespace) { // Si dossier
363				if(page_exists("$childNamespace:$start")) { // S'il y a une page d'accueil
364					$items[] = $this->createListItem($childNamespace, $start);
365				}
366				else if(page_exists("$childNamespace:$childID")) { // S'il y a une page du même nom que le dossier dans le dossier
367					$items[] = $this->createListItem($childNamespace, $childID);
368				}
369				else if($isPageNamespace) { // S'il y a une page du même nom que le dossier au même niveau que le dossier
370					$items[] = $this->createListItem($namespace, $childID);
371				}
372
373				continue;
374			}
375
376			if(!$isDirNamespace && $isPageNamespace) {
377				$skipRegex = $this->getConf('skip_file');
378				if (!empty($skipRegex) && preg_match($skipRegex, $childNamespace)) {
379					continue;
380				}
381
382				$isHomepage = false;
383				$pageNamespaceInfo = $this->getNamespaceInfo("$namespace:$childID");
384				if($this->isHomepage($childID, $pageNamespaceInfo['parentID'])) {
385					$isHomepage = true;
386				}
387
388				$items[] = $this->createListItem($namespace, $childID, $isHomepage);
389			}
390		}
391
392		return $items;
393	}
394
395	private function isHomepage($pageID, $parentID) {
396		global $conf;
397		$startPageID = $conf['start'];
398
399		return $pageID == $startPageID || $pageID == $parentID;
400	}
401
402	private function namespaceDir($namespace, $getMedias = false) {
403		global $conf;
404
405		// Choix du dossier selon le mode
406		$baseDir = $getMedias ? $conf['mediadir'] : $conf['datadir'];
407
408		// Remplacement des deux-points par des slashs et encodage UTF-8
409		return $baseDir . '/' . utf8_encodeFN(str_replace(':', '/', $namespace));
410	}
411
412	private function getNamespaceInfo($namespace) {
413		$namespaces = explode(':', $namespace);
414
415		return array(
416			'pageNamespace' => $namespace,
417			'pageID' => array_pop($namespaces),
418			'parentNamespace' => implode(':', $namespaces),
419			'parentID' => array_pop($namespaces)
420		);
421	}
422}
423