xref: /plugin/visualindex/syntax/visualindex.php (revision 3c9c7f3beeea1dce712c368cb507b309c63f5d06)
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'; // substitution = remplacer la balise par du contenu
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		usort($items, function($a, $b) {
114			return $b['sortID'] - $a['sortID'];
115		});
116
117		$textSize = $this->getConf('taille_texte');
118		$textColor = $this->getConf('couleur_texte');
119
120		// -----------------------------
121		// ProseMirror / HTML wrapper
122		// -----------------------------
123		$renderer->doc .= '<span class="plugin_visualindex" '
124			.'data-namespace="'.htmlspecialchars($namespace).'" '
125			.'data-filter="'.htmlspecialchars($filter).'" '
126			.'data-desc="'.($desc ? '1' : '0').'">';
127
128		// -----------------------------
129		// HTML classique pour le rendu visuel
130		// -----------------------------
131		$renderer->doc .= '<div class="visualindex">';
132
133		$renderedItems = 0;
134		foreach ($items as $item) {
135			$pageID = $item['pageID'];
136			$namespace = $item['namespace'];
137			$pageNamespace = $item['pageNamespace'];
138			$sortID = $item['sortID'];
139			$isHomepage = $item['isHomepage'];
140
141			if($pageNamespace == $ID) {
142				continue;
143			}
144
145			$permission = auth_quickaclcheck($pageNamespace);
146			if($permission < AUTH_READ) {
147				continue;
148			}
149
150			$logoUrl = null;
151			if(!$getMedias) {
152				$title = p_get_first_heading($pageNamespace);
153				if(empty($title)) {
154					continue;
155				}
156			}
157			else {
158				$title = str_replace('_', ' ', $pageID);
159				$logoUrl = $this->getMediaItemImage($pageNamespace);
160			}
161
162			if(!$logoUrl) {
163				$logoUrl = $this->getPageImage($namespace, $pageID);
164			}
165
166			// Afficher le lien de la page ou du sous-dossier
167			$targetAttr = $getMedias ? $this->getMediaLinkTargetAttr() : '';
168			$renderer->doc .= '<a class="vi_tile' . ($isHomepage? ' homepage' : '') . '" style="color:' . $textColor . ' ; font-size:' . $textSize . '" href="'. ($getMedias? ml($pageNamespace) : wl($pageNamespace)) . '"' . $targetAttr . '>';
169				$renderer->doc .= '<div class="vi_content"><img loading="lazy" src="' . $logoUrl . '" alt="" /><br />' . $title . '</div>';
170				$renderer->doc .= '<div class="vi_vertical_align"></div>';
171			$renderer->doc .= '</a>';
172			$renderedItems++;
173		}
174
175		$renderer->doc .= '</div>';
176
177		if($renderedItems === 0) {
178			$this->renderInfoMessage($renderer, 'empty');
179		}
180
181		$renderer->doc .= '</span>';
182		// -----------------------------
183		// Fin du node ProseMirror
184		// -----------------------------
185
186		return true;
187	}
188
189	private function getPagesiconHelper() {
190		if($this->pagesiconHelper === false) {
191			$this->pagesiconHelper = plugin_load('helper', 'pagesicon');
192		}
193		return $this->pagesiconHelper ?: null;
194	}
195
196	private function getDefaultImageUrl() {
197		$defaultImage = cleanID((string)$this->getConf('default_image'));
198		if($defaultImage !== '' && @file_exists(mediaFN($defaultImage))) {
199			return ml($defaultImage, ['width' => 55]);
200		}
201
202		return '/lib/plugins/visualindex/images/default_image.png';
203	}
204
205	private function getMediaItemImage($mediaID) {
206		$mediaID = cleanID((string)$mediaID);
207		if($mediaID === '') {
208			return $this->getDefaultImageUrl();
209		}
210
211		$helper = $this->getPagesiconHelper();
212		if((bool)$this->getConf('use_pagesicon') && $helper) {
213			if(method_exists($helper, 'getMediaIconUrl')) {
214				$mtime = null;
215				$iconUrl = $helper->getMediaIconUrl($mediaID, 'bigorsmall', ['width' => 55], $mtime, false);
216				if($iconUrl) return $iconUrl;
217			} else if(method_exists($helper, 'getMediaIcon')) {
218				$mtime = null;
219				$withDefaultSupported = false;
220				try {
221					$method = new ReflectionMethod($helper, 'getMediaIcon');
222					$withDefaultSupported = $method->getNumberOfParameters() >= 5;
223				} catch (ReflectionException $e) {
224					$withDefaultSupported = false;
225				}
226
227				if($withDefaultSupported) {
228					$iconUrl = $helper->getMediaIcon($mediaID, 'bigorsmall', ['width' => 55], $mtime, false);
229				} else {
230					$iconUrl = $helper->getMediaIcon($mediaID, 'bigorsmall', ['width' => 55], $mtime);
231				}
232				if($iconUrl) return $iconUrl;
233			}
234		}
235
236		$childPathInfo = pathinfo(noNS($mediaID));
237		$imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg'];
238		if(isset($childPathInfo['extension']) && in_array(strtolower((string)$childPathInfo['extension']), $imageExtensions, true)) {
239			return ml($mediaID);
240		}
241
242		return $this->getDefaultImageUrl();
243	}
244
245	/**
246	 * Renvoie l'URL de l'icone de la page via pagesicon, sinon image par defaut.
247	 */
248	public function getPageImage($namespace, $pageID = null) {
249		if(!$pageID) {
250			$pageNamespaceInfo = $this->getNamespaceInfo($namespace);
251			$namespace = $pageNamespaceInfo['parentNamespace'];
252			$pageID = $pageNamespaceInfo['pageID'];
253		}
254
255		$helper = $this->getPagesiconHelper();
256		if((bool)$this->getConf('use_pagesicon') && $helper) {
257			if(method_exists($helper, 'getPageIconUrl')) {
258				$mtime = null;
259				$iconUrl = $helper->getPageIconUrl((string)$namespace, (string)$pageID, 'bigorsmall', ['width' => 55], $mtime, false);
260				if($iconUrl) return $iconUrl;
261			} else if(method_exists($helper, 'getImageIcon')) {
262				$mtime = null;
263				$withDefaultSupported = false;
264				try {
265					$method = new ReflectionMethod($helper, 'getImageIcon');
266					$withDefaultSupported = $method->getNumberOfParameters() >= 6;
267				} catch (ReflectionException $e) {
268					$withDefaultSupported = false;
269				}
270
271				if($withDefaultSupported) {
272					$iconUrl = $helper->getImageIcon((string)$namespace, (string)$pageID, 'bigorsmall', ['width' => 55], $mtime, false);
273				} else {
274					$iconUrl = $helper->getImageIcon((string)$namespace, (string)$pageID, 'bigorsmall', ['width' => 55], $mtime);
275				}
276				if($iconUrl) return $iconUrl;
277			}
278		}
279
280		return $this->getDefaultImageUrl();
281	}
282
283
284	public function createListItem($parentNamespace, $pageID, $isHomepage = false) {
285		return array(
286			'pageID' => $pageID,
287			'namespace' => $parentNamespace,
288			'pageNamespace' => cleanID("$parentNamespace:$pageID"),
289			'sortID' => ($isHomepage? 100 : 0),
290			'isHomepage' => $isHomepage
291		);
292	}
293
294	/**
295	 * Récupère à la fois les pages et les sous-dossiers d'un namespace
296	 */
297	public function getItemsAndSubfoldersItems($namespace, $getMedias = false, $filter = null, $desc = false) {
298		global $conf;
299
300		$childrens = @scandir($this->namespaceDir($namespace, $getMedias), $desc? SCANDIR_SORT_DESCENDING : SCANDIR_SORT_ASCENDING);
301		if($childrens === false) {
302			if($getMedias) {
303				$childrens = @scandir($this->namespaceDir($namespace));
304				if($childrens != false) {
305					return [];
306				}
307			}
308
309			return false;
310		}
311
312		$start = $conf['start']; // 'accueil' dans la plupart des temps (dans bpnum:d-s:accueil)
313
314		$finalPattern = null;
315		if($filter) {
316			$parts = explode('|', $filter);
317			$regexParts = [];
318			foreach ($parts as $part) {
319				$pattern = preg_quote($part, '/');
320				$pattern = str_replace('\*', '.*', $pattern);
321				$regexParts[] = '^' . $pattern . '$';
322			}
323
324			$finalPattern = '/(' . implode('|', $regexParts) . ')/i';
325		}
326
327		$items = [];
328		foreach($childrens as $child) {
329			if($child[0] == '.' ) {
330				continue;
331			}
332
333			if($finalPattern && !preg_match($finalPattern, $child)) {
334				continue;
335			}
336
337			$childPathInfo = pathinfo($child);
338			$childID = cleanID($childPathInfo['filename']);
339			$childNamespace = cleanID("$namespace:$childID");
340
341			$childHasExtension = isset($childPathInfo['extension']) && $childPathInfo['extension'] !== '';
342			$isDirNamespace = is_dir($this->namespaceDir($childNamespace, $getMedias));
343			$isPageNamespace = page_exists($childNamespace);
344
345			if($getMedias) {
346				if($childHasExtension) {
347					$items[] = $this->createListItem($namespace, $child);
348				}
349				continue;
350			}
351
352			if(!$childHasExtension && $isDirNamespace) { // Si dossier
353				if(page_exists("$childNamespace:$start")) { // S'il y a une page d'accueil
354					$items[] = $this->createListItem($childNamespace, $start);
355				}
356				else if(page_exists("$childNamespace:$childID")) { // S'il y a une page du même nom que le dossier dans le dossier
357					$items[] = $this->createListItem($childNamespace, $childID);
358				}
359				else if($isPageNamespace) { // S'il y a une page du même nom que le dossier au même niveau que le dossier
360					$items[] = $this->createListItem($namespace, $childID);
361				}
362
363				continue;
364			}
365
366			if(!$isDirNamespace && $isPageNamespace) {
367				$skipRegex = $this->getConf('skip_file');
368				if (!empty($skipRegex) && preg_match($skipRegex, $childNamespace)) {
369					continue;
370				}
371
372				$isHomepage = false;
373				$pageNamespaceInfo = $this->getNamespaceInfo("$namespace:$childID");
374				if($this->isHomepage($childID, $pageNamespaceInfo['parentID'])) {
375					$isHomepage = true;
376				}
377
378				$items[] = $this->createListItem($namespace, $childID, $isHomepage);
379			}
380		}
381
382		return $items;
383	}
384
385	public function isHomepage($pageID, $parentID) {
386		global $conf;
387		$startPageID = $conf['start'];
388
389		return $pageID == $startPageID || $pageID == $parentID;
390	}
391
392	public function namespaceDir($namespace, $getMedias = false) {
393		global $conf;
394
395		// Choix du dossier selon le mode
396		$baseDir = $getMedias ? $conf['mediadir'] : $conf['datadir'];
397
398		// Remplacement des deux-points par des slashs et encodage UTF-8
399		return $baseDir . '/' . utf8_encodeFN(str_replace(':', '/', $namespace));
400	}
401
402	public function getNamespaceInfo($namespace) {
403		$namespaces = explode(':', $namespace);
404
405		return array(
406			'pageNamespace' => $namespace,
407			'pageID' => array_pop($namespaces),
408			'parentNamespace' => implode(':', $namespaces),
409			'parentID' => array_pop($namespaces)
410		);
411	}
412}
413