xref: /plugin/catmenu/syntax/catmenu.php (revision aa591c9040aa9d58df44eaf65df693766613dc9f)
16983cdfdSLORTET<?php
26983cdfdSLORTET/**
36983cdfdSLORTET * Plugin catmenu
4*aa591c90SLORTET * Affiche les pages d'un namespace donné sous forme de menu hiérarchique.
56983cdfdSLORTET * Auteur: Lortetv
66983cdfdSLORTET */
76983cdfdSLORTET
86983cdfdSLORTETuse dokuwiki\Extension\SyntaxPlugin;
96983cdfdSLORTETuse dokuwiki\File\PageResolver;
106983cdfdSLORTETuse dokuwiki\Ui\Index;
116983cdfdSLORTET
126983cdfdSLORTETclass syntax_plugin_catmenu_catmenu extends SyntaxPlugin {
136983cdfdSLORTET    /** @var helper_plugin_pagesicon|null|false */
146983cdfdSLORTET    private $pagesiconHelper = false;
156983cdfdSLORTET
16*aa591c90SLORTET    /** @var helper_plugin_catmenu_namespace|null */
17*aa591c90SLORTET    private $nsHelper = null;
18*aa591c90SLORTET
19*aa591c90SLORTET    /**
20*aa591c90SLORTET     * Retourne le helper namespace (chargé en lazy).
21*aa591c90SLORTET     */
22*aa591c90SLORTET    private function getNsHelper(): helper_plugin_catmenu_namespace
23*aa591c90SLORTET    {
24*aa591c90SLORTET        if ($this->nsHelper === null) {
25*aa591c90SLORTET            $this->nsHelper = $this->loadHelper('catmenu_namespace');
26*aa591c90SLORTET        }
27*aa591c90SLORTET        return $this->nsHelper;
28*aa591c90SLORTET    }
29*aa591c90SLORTET
306983cdfdSLORTET    public function getType() {
316983cdfdSLORTET        return 'substition'; // substitution = remplacer la balise par du contenu
326983cdfdSLORTET    }
336983cdfdSLORTET
346983cdfdSLORTET    public function getPType() {
356983cdfdSLORTET        return 'block';
366983cdfdSLORTET    }
376983cdfdSLORTET
386983cdfdSLORTET    public function getSort() { // priorité du plugin par rapport à d'autres
396983cdfdSLORTET        return 15;
406983cdfdSLORTET    }
416983cdfdSLORTET
426983cdfdSLORTET    /**
436983cdfdSLORTET     * Reconnaît la syntaxe {{catmenu>[namespace]}}
446983cdfdSLORTET     */
45*aa591c90SLORTET    public function connectTo($mode) {
466983cdfdSLORTET        $this->Lexer->addSpecialPattern('{{catmenu>.*?}}', $mode, 'plugin_catmenu_catmenu');
476983cdfdSLORTET    }
486983cdfdSLORTET
496983cdfdSLORTET    /**
50*aa591c90SLORTET     * Nettoie {{catmenu>[namespace]}} et extrait le namespace.
516983cdfdSLORTET     */
526983cdfdSLORTET    public function handle($match, $state, $pos, Doku_Handler $handler) {
53*aa591c90SLORTET        $namespace = trim(substr($match, 10, -2)); // retirer {{catmenu> et }}
546983cdfdSLORTET        return ['namespace' => $namespace];
556983cdfdSLORTET    }
566983cdfdSLORTET
576983cdfdSLORTET    public function render($mode, Doku_Renderer $renderer, $data) {
586983cdfdSLORTET        if ($mode !== 'xhtml') return false;
596983cdfdSLORTET
606983cdfdSLORTET        global $ID;
616983cdfdSLORTET        global $conf;
626983cdfdSLORTET
636983cdfdSLORTET        $random = uniqid();
64*aa591c90SLORTET        $nsHelper = $this->getNsHelper();
656983cdfdSLORTET
66*aa591c90SLORTET        if ($data['namespace'] === '.') { // Résolution du namespace courant
67*aa591c90SLORTET            $namespace = $nsHelper->getCurrentNamespace($ID);
68*aa591c90SLORTET        } else {
696983cdfdSLORTET            $namespace = cleanID($data['namespace']);
706983cdfdSLORTET        }
716983cdfdSLORTET
726983cdfdSLORTET        $pages = $this->getPagesAndSubfoldersItems($namespace);
736983cdfdSLORTET        if ($pages === false) {
746983cdfdSLORTET            $renderer->doc .= '<div>' . hsc($this->getLang('namespace_not_found')) . '</div>';
756983cdfdSLORTET            return true;
766983cdfdSLORTET        }
776983cdfdSLORTET
786983cdfdSLORTET        $renderer->doc .= '<div id="catmenu_' . $random . '" class="catmenu" style=""></div>';
796983cdfdSLORTET        $renderer->doc .= "<script>
806983cdfdSLORTET            let catmenuconf_" . $random . " = { userewrite: '" . $conf['userewrite'] . "', start: '" . $conf['start'] . "' };
816983cdfdSLORTET            let catmenuobj_" . $random . " = JSON.parse(`" . htmlspecialchars_decode(json_encode($pages)) . "`);
826983cdfdSLORTET            (function initCatmenu_" . $random . "() {
836983cdfdSLORTET                const target = document.getElementById('catmenu_" . $random . "');
846983cdfdSLORTET                if (!target) return;
856983cdfdSLORTET                const renderOnce = function () {
866983cdfdSLORTET                    if (target.dataset.catmenuRendered === '1') return;
876983cdfdSLORTET                    if (typeof window.catmenu_generateSectionMenu !== 'function') return;
886983cdfdSLORTET                    target.dataset.catmenuRendered = '1';
896983cdfdSLORTET                    target.innerHTML = '';
906983cdfdSLORTET                    window.catmenu_generateSectionMenu(catmenuconf_" . $random . ", catmenuobj_" . $random . ", target);
916983cdfdSLORTET                };
926983cdfdSLORTET                if (typeof window.catmenu_generateSectionMenu === 'function') {
936983cdfdSLORTET                    renderOnce();
946983cdfdSLORTET                    return;
956983cdfdSLORTET                }
966983cdfdSLORTET
97*aa591c90SLORTET                // En mode édition/aperçu, les scripts du plugin peuvent se charger
98*aa591c90SLORTET                // après le rendu inline — on attend l'événement catmenu:ready.
996983cdfdSLORTET                document.addEventListener('catmenu:ready', function onReady() {
1006983cdfdSLORTET                    renderOnce();
1016983cdfdSLORTET                }, { once: true });
1026983cdfdSLORTET
1036983cdfdSLORTET                setTimeout(function retryCatmenu_" . $random . "() {
1046983cdfdSLORTET                    renderOnce();
1056983cdfdSLORTET                }, 0);
1066983cdfdSLORTET            })();
1076983cdfdSLORTET        </script>";
1086983cdfdSLORTET
109*aa591c90SLORTET        // Injection du footer DokuCode (si configuré)
110*aa591c90SLORTET        $footerContent = trim((string)$this->getConf('footer_content'));
111*aa591c90SLORTET        if ($footerContent !== '') {
112*aa591c90SLORTET            $footerHtml = p_render('xhtml', p_get_instructions($footerContent), $info);
113*aa591c90SLORTET            if ($footerHtml !== '') {
114*aa591c90SLORTET                $renderer->doc .= '<div class="catmenu-footer">' . $footerHtml . '</div>';
115*aa591c90SLORTET            }
116*aa591c90SLORTET        }
117*aa591c90SLORTET
1186983cdfdSLORTET        return true;
1196983cdfdSLORTET    }
1206983cdfdSLORTET
1216983cdfdSLORTET    /**
122*aa591c90SLORTET     * Récupère à la fois les pages et les sous-dossiers d'un namespace.
123*aa591c90SLORTET     *
124*aa591c90SLORTET     * @return array|false  Tableau d'items ou false si le namespace n'existe pas
1256983cdfdSLORTET     */
1266983cdfdSLORTET    public function getPagesAndSubfoldersItems($namespace) {
1276983cdfdSLORTET        global $conf;
1286983cdfdSLORTET        $skipPageWithoutTitle = (bool)$this->getConf('skip_page_without_title');
129*aa591c90SLORTET        $nsHelper = $this->getNsHelper();
1306983cdfdSLORTET
131*aa591c90SLORTET        $childrens = @scandir($nsHelper->namespaceDir($namespace));
1326983cdfdSLORTET        if ($childrens === false) {
1336983cdfdSLORTET            return false;
1346983cdfdSLORTET        }
1356983cdfdSLORTET
136*aa591c90SLORTET        $start = $conf['start']; // page de démarrage (ex. 'accueil', 'start')
1376983cdfdSLORTET
1386983cdfdSLORTET        $items = [];
139*aa591c90SLORTET        foreach ($childrens as $child) {
140*aa591c90SLORTET            if ($child[0] === '.') { // ignorer ., .. et fichiers cachés
1416983cdfdSLORTET                continue;
1426983cdfdSLORTET            }
1436983cdfdSLORTET
1446983cdfdSLORTET            $childPathInfo  = pathinfo($child);
1456983cdfdSLORTET            $childID        = cleanID($childPathInfo['filename']);
1466983cdfdSLORTET            $childNamespace = cleanID($namespace !== '' ? ($namespace . ':' . $childID) : $childID);
1476983cdfdSLORTET
1486983cdfdSLORTET            $childHasExtension = isset($childPathInfo['extension']) && $childPathInfo['extension'] !== '';
149*aa591c90SLORTET            $isDirNamespace    = is_dir($nsHelper->namespaceDir($childNamespace));
1506983cdfdSLORTET            $isPageNamespace   = page_exists($childNamespace);
1516983cdfdSLORTET
152*aa591c90SLORTET            if (!$childHasExtension && $isDirNamespace) { // Dossier/namespace
153*aa591c90SLORTET                $pageNamespaceInfo = $nsHelper->getPageNamespaceInfo($childNamespace);
154*aa591c90SLORTET                if ($nsHelper->isHomepage($childID, (string)$pageNamespaceInfo['parentID'])) {
155*aa591c90SLORTET                    // Aplatir les dossiers "page d'accueil" (ex. ns:ns) — leurs enfants remontent d'un niveau.
1566983cdfdSLORTET                    $subItems = $this->getPagesAndSubfoldersItems($childNamespace);
1576983cdfdSLORTET                    if (is_array($subItems) && $subItems) {
1586983cdfdSLORTET                        $items = array_merge($items, $subItems);
1596983cdfdSLORTET                    }
1606983cdfdSLORTET                    continue;
1616983cdfdSLORTET                }
1626983cdfdSLORTET
1636983cdfdSLORTET                $pageID = null;
164*aa591c90SLORTET                if (page_exists("$childNamespace:$start")) {
165*aa591c90SLORTET                    // Page d'accueil standard
1666983cdfdSLORTET                    $pageID = "$childNamespace:$start";
167*aa591c90SLORTET                } elseif (page_exists("$childNamespace:$childID")) {
168*aa591c90SLORTET                    // Page homonyme dans le dossier
1696983cdfdSLORTET                    $pageID = "$childNamespace:$childID";
170*aa591c90SLORTET                } elseif ($isPageNamespace) {
171*aa591c90SLORTET                    // Page homonyme au même niveau que le dossier
1726983cdfdSLORTET                    $pageID = cleanID($namespace !== '' ? ($namespace . ':' . $childID) : $childID);
1736983cdfdSLORTET                }
1746983cdfdSLORTET
1756983cdfdSLORTET                $permission = auth_quickaclcheck($pageID);
1766983cdfdSLORTET                if ($permission < AUTH_READ) {
1776983cdfdSLORTET                    continue;
1786983cdfdSLORTET                }
1796983cdfdSLORTET
1806983cdfdSLORTET                $title = $pageID ? p_get_first_heading($pageID) : $pageID;
1816983cdfdSLORTET                if (empty($title)) {
1826983cdfdSLORTET                    if ($skipPageWithoutTitle || empty($pageID)) {
1836983cdfdSLORTET                        continue;
1846983cdfdSLORTET                    }
1856983cdfdSLORTET                    $title = noNS($pageID);
1866983cdfdSLORTET                }
1876983cdfdSLORTET
188*aa591c90SLORTET                $items[] = [
1896983cdfdSLORTET                    'title'              => $title,
1906983cdfdSLORTET                    'url'                => $pageID ? wl($pageID) : null,
1916983cdfdSLORTET                    'icon'               => $this->getPageImage($pageID),
1926983cdfdSLORTET                    'pagesiconUploadUrl' => $this->getPagesiconUploadUrl($pageID ?: $childNamespace),
1936983cdfdSLORTET                    'folderNamespace'    => $childNamespace,
1946983cdfdSLORTET                    'namespace'          => $childNamespace,
1956983cdfdSLORTET                    'subtree'            => $this->getPagesAndSubfoldersItems($childNamespace),
196*aa591c90SLORTET                    'permission'         => $permission,
197*aa591c90SLORTET                ];
1986983cdfdSLORTET                continue;
1996983cdfdSLORTET            }
2006983cdfdSLORTET
201*aa591c90SLORTET            if (!$isDirNamespace && $isPageNamespace) { // Page seule
202*aa591c90SLORTET                $skipRegex = $this->resolveSkipRegex();
2036983cdfdSLORTET                if (!empty($skipRegex) && preg_match($skipRegex, $childNamespace)) {
2046983cdfdSLORTET                    continue;
2056983cdfdSLORTET                }
2066983cdfdSLORTET
207*aa591c90SLORTET                $pageNamespaceInfo = $nsHelper->getPageNamespaceInfo("$namespace:$childID");
208*aa591c90SLORTET                if ($nsHelper->isHomepage($childID, $pageNamespaceInfo['parentID'])) {
2096983cdfdSLORTET                    continue;
2106983cdfdSLORTET                }
2116983cdfdSLORTET
2126983cdfdSLORTET                $permission = auth_quickaclcheck($childNamespace);
2136983cdfdSLORTET                if ($permission < AUTH_READ) {
2146983cdfdSLORTET                    continue;
2156983cdfdSLORTET                }
2166983cdfdSLORTET
2176983cdfdSLORTET                $title = p_get_first_heading($childNamespace);
2186983cdfdSLORTET                if (empty($title)) {
2196983cdfdSLORTET                    if ($skipPageWithoutTitle) {
2206983cdfdSLORTET                        continue;
2216983cdfdSLORTET                    }
2226983cdfdSLORTET                    $title = noNS($childNamespace);
2236983cdfdSLORTET                }
2246983cdfdSLORTET
225*aa591c90SLORTET                $items[] = [
2266983cdfdSLORTET                    'title'              => $title,
2276983cdfdSLORTET                    'url'                => $childNamespace ? wl($childNamespace) : null,
2286983cdfdSLORTET                    'icon'               => $this->getPageImage($childNamespace),
2296983cdfdSLORTET                    'pagesiconUploadUrl' => $this->getPagesiconUploadUrl($childNamespace),
2306983cdfdSLORTET                    'folderNamespace'    => $namespace,
2316983cdfdSLORTET                    'namespace'          => $childNamespace,
232*aa591c90SLORTET                    'permission'         => $permission,
233*aa591c90SLORTET                ];
2346983cdfdSLORTET            }
2356983cdfdSLORTET        }
2366983cdfdSLORTET
2376983cdfdSLORTET        return $items;
2386983cdfdSLORTET    }
2396983cdfdSLORTET
2406983cdfdSLORTET    /**
241*aa591c90SLORTET     * Résout la valeur effective de l'option skip_file.
242*aa591c90SLORTET     *
243*aa591c90SLORTET     * Si la valeur est le jeton spécial "@hidepages", retourne la regex de masquage
244*aa591c90SLORTET     * de pages configurée globalement dans DokuWiki ($conf['hidepages']).
245*aa591c90SLORTET     * Sinon, retourne la valeur brute telle quelle.
246*aa591c90SLORTET     */
247*aa591c90SLORTET    private function resolveSkipRegex(): string
248*aa591c90SLORTET    {
249*aa591c90SLORTET        global $conf;
250*aa591c90SLORTET        $raw = (string)$this->getConf('skip_file');
251*aa591c90SLORTET        if (trim($raw) === '@hidepages') {
252*aa591c90SLORTET            return (string)($conf['hidepages'] ?? '');
253*aa591c90SLORTET        }
254*aa591c90SLORTET        return $raw;
255*aa591c90SLORTET    }
256*aa591c90SLORTET
257*aa591c90SLORTET    /**
258*aa591c90SLORTET     * Retourne l'URL de la miniature d'icône via le helper pagesicon.
259*aa591c90SLORTET     * Retourne une chaîne vide si aucune icône n'est définie.
2606983cdfdSLORTET     */
2616983cdfdSLORTET    public function getPageImage($page) {
2626983cdfdSLORTET        if (!$page) return '';
2636983cdfdSLORTET
2646983cdfdSLORTET        $page = cleanID((string)$page);
2656983cdfdSLORTET        if ($page === '') return '';
2666983cdfdSLORTET
2676983cdfdSLORTET        /** @var helper_plugin_pagesicon|null $helper */
2686983cdfdSLORTET        $helper = plugin_load('helper', 'pagesicon');
2696983cdfdSLORTET        if (!$helper) return '';
2706983cdfdSLORTET
2716983cdfdSLORTET        $namespace = getNS($page);
2726983cdfdSLORTET        $pageID    = noNS($page);
273*aa591c90SLORTET
274*aa591c90SLORTET        // Nouvelle API pagesicon (préférée)
2756983cdfdSLORTET        if (method_exists($helper, 'getPageIconUrl')) {
2766983cdfdSLORTET            $mtime   = null;
2776983cdfdSLORTET            $iconUrl = $helper->getPageIconUrl($namespace, $pageID, 'smallorbig', ['width' => 55], $mtime, true);
2786983cdfdSLORTET            if ($iconUrl) return $iconUrl;
2796983cdfdSLORTET        } elseif (method_exists($helper, 'getImageIcon')) {
2806983cdfdSLORTET            $mtime                = null;
2816983cdfdSLORTET            $withDefaultSupported = false;
2826983cdfdSLORTET            try {
2836983cdfdSLORTET                $method               = new ReflectionMethod($helper, 'getImageIcon');
2846983cdfdSLORTET                $withDefaultSupported = $method->getNumberOfParameters() >= 6;
2856983cdfdSLORTET            } catch (ReflectionException $e) {
2866983cdfdSLORTET                $withDefaultSupported = false;
2876983cdfdSLORTET            }
2886983cdfdSLORTET
289*aa591c90SLORTET            $iconUrl = $withDefaultSupported
290*aa591c90SLORTET                ? $helper->getImageIcon($namespace, $pageID, 'smallorbig', ['width' => 55], $mtime, true)
291*aa591c90SLORTET                : $helper->getImageIcon($namespace, $pageID, 'smallorbig', ['width' => 55], $mtime);
2926983cdfdSLORTET            if ($iconUrl) return $iconUrl;
2936983cdfdSLORTET        }
2946983cdfdSLORTET
295*aa591c90SLORTET        // Fallback : récupération de l'ID média puis génération de l'URL
2966983cdfdSLORTET        $iconMediaID = false;
2976983cdfdSLORTET        if (method_exists($helper, 'getPageIconId')) {
2986983cdfdSLORTET            $iconMediaID = $helper->getPageIconId($namespace, $pageID, 'smallorbig');
2996983cdfdSLORTET        } elseif (method_exists($helper, 'getPageImage')) {
3006983cdfdSLORTET            $withDefaultSupported = false;
3016983cdfdSLORTET            try {
3026983cdfdSLORTET                $method               = new ReflectionMethod($helper, 'getPageImage');
3036983cdfdSLORTET                $withDefaultSupported = $method->getNumberOfParameters() >= 4;
3046983cdfdSLORTET            } catch (ReflectionException $e) {
3056983cdfdSLORTET                $withDefaultSupported = false;
3066983cdfdSLORTET            }
3076983cdfdSLORTET
308*aa591c90SLORTET            $iconMediaID = $withDefaultSupported
309*aa591c90SLORTET                ? $helper->getPageImage($namespace, $pageID, 'smallorbig', true)
310*aa591c90SLORTET                : $helper->getPageImage($namespace, $pageID, 'smallorbig');
3116983cdfdSLORTET        }
3126983cdfdSLORTET        if (!$iconMediaID) return '';
3136983cdfdSLORTET
3146983cdfdSLORTET        return ml($iconMediaID, ['width' => 55]);
3156983cdfdSLORTET    }
3166983cdfdSLORTET
317*aa591c90SLORTET    /**
318*aa591c90SLORTET     * Retourne l'URL de la page d'upload d'icône pour un namespace (via pagesicon).
319*aa591c90SLORTET     */
3206983cdfdSLORTET    private function getPagesiconUploadUrl($namespace) {
3216983cdfdSLORTET        if ($this->pagesiconHelper === false) {
3226983cdfdSLORTET            $this->pagesiconHelper = plugin_load('helper', 'pagesicon');
3236983cdfdSLORTET        }
3246983cdfdSLORTET        if (!$this->pagesiconHelper) return null;
3256983cdfdSLORTET        if (!method_exists($this->pagesiconHelper, 'getUploadIconPage')) return null;
3266983cdfdSLORTET
3276983cdfdSLORTET        return $this->pagesiconHelper->getUploadIconPage((string)$namespace);
3286983cdfdSLORTET    }
3296983cdfdSLORTET}
330