xref: /plugin/catmenu/action/prosemirror.php (revision 6983cdfd4483215ff5a1e573925c9c612964e790)
1*6983cdfdSLORTET<?php
2*6983cdfdSLORTET
3*6983cdfdSLORTETuse dokuwiki\plugin\catmenu\parser\CatmenuNode;
4*6983cdfdSLORTETuse dokuwiki\plugin\prosemirror\schema\Node;
5*6983cdfdSLORTET
6*6983cdfdSLORTETclass action_plugin_catmenu_prosemirror extends \dokuwiki\Extension\ActionPlugin
7*6983cdfdSLORTET{
8*6983cdfdSLORTET    public function register(Doku_Event_Handler $controller)
9*6983cdfdSLORTET    {
10*6983cdfdSLORTET        $controller->register_hook('DOKUWIKI_STARTED', 'AFTER', $this, 'addJsInfo');
11*6983cdfdSLORTET        $controller->register_hook('PROSEMIRROR_RENDER_PLUGIN', 'BEFORE', $this, 'handleRender');
12*6983cdfdSLORTET        $controller->register_hook('PROSEMIRROR_PARSE_UNKNOWN', 'BEFORE', $this, 'handleParseUnknown');
13*6983cdfdSLORTET        $controller->register_hook('PLUGIN_PAGESICON_UPDATED', 'AFTER', $this, 'handlePagesiconUpdated');
14*6983cdfdSLORTET    }
15*6983cdfdSLORTET
16*6983cdfdSLORTET    private function namespaceDir(string $namespace): string
17*6983cdfdSLORTET    {
18*6983cdfdSLORTET        global $conf;
19*6983cdfdSLORTET        return rtrim((string)$conf['datadir'], '/') . '/' . utf8_encodeFN(str_replace(':', '/', $namespace));
20*6983cdfdSLORTET    }
21*6983cdfdSLORTET
22*6983cdfdSLORTET    private function getPageNamespaceInfo(string $namespace): array
23*6983cdfdSLORTET    {
24*6983cdfdSLORTET        $namespaces = explode(':', $namespace);
25*6983cdfdSLORTET        $pageID = array_pop($namespaces);
26*6983cdfdSLORTET        $parentNamespace = implode(':', $namespaces);
27*6983cdfdSLORTET        $parentID = '';
28*6983cdfdSLORTET        if ($parentNamespace !== '') {
29*6983cdfdSLORTET            $parts = explode(':', $parentNamespace);
30*6983cdfdSLORTET            $parentID = (string)array_pop($parts);
31*6983cdfdSLORTET        }
32*6983cdfdSLORTET        return [
33*6983cdfdSLORTET            'pageID' => $pageID,
34*6983cdfdSLORTET            'parentNamespace' => $parentNamespace,
35*6983cdfdSLORTET            'parentID' => $parentID,
36*6983cdfdSLORTET        ];
37*6983cdfdSLORTET    }
38*6983cdfdSLORTET
39*6983cdfdSLORTET    private function isHomepage(string $pageID, string $parentID): bool
40*6983cdfdSLORTET    {
41*6983cdfdSLORTET        global $conf;
42*6983cdfdSLORTET        $startPageID = (string)$conf['start'];
43*6983cdfdSLORTET        return $pageID === $startPageID || ($parentID !== '' && $pageID === $parentID);
44*6983cdfdSLORTET    }
45*6983cdfdSLORTET
46*6983cdfdSLORTET    private function getCurrentNamespace(string $hostPageID): string
47*6983cdfdSLORTET    {
48*6983cdfdSLORTET        if (!is_dir($this->namespaceDir($hostPageID))) {
49*6983cdfdSLORTET            $info = $this->getPageNamespaceInfo($hostPageID);
50*6983cdfdSLORTET            if ($this->isHomepage((string)$info['pageID'], (string)$info['parentID'])) {
51*6983cdfdSLORTET                return (string)$info['parentNamespace'];
52*6983cdfdSLORTET            }
53*6983cdfdSLORTET        }
54*6983cdfdSLORTET        return $hostPageID;
55*6983cdfdSLORTET    }
56*6983cdfdSLORTET
57*6983cdfdSLORTET    private function resolveNamespaceExpression(string $expr, string $hostPageID): string
58*6983cdfdSLORTET    {
59*6983cdfdSLORTET        $expr = trim($expr);
60*6983cdfdSLORTET        if ($expr === '.') return $this->getCurrentNamespace($hostPageID);
61*6983cdfdSLORTET        if ($expr !== '' && $expr[0] === '~') {
62*6983cdfdSLORTET            $rel = cleanID(ltrim($expr, '~'));
63*6983cdfdSLORTET            $base = $this->getCurrentNamespace($hostPageID);
64*6983cdfdSLORTET            return cleanID($base !== '' ? ($base . ':' . $rel) : $rel);
65*6983cdfdSLORTET        }
66*6983cdfdSLORTET        return cleanID($expr);
67*6983cdfdSLORTET    }
68*6983cdfdSLORTET
69*6983cdfdSLORTET    private function isTargetInNamespace(string $targetPage, string $namespace): bool
70*6983cdfdSLORTET    {
71*6983cdfdSLORTET        if ($namespace === '') return true;
72*6983cdfdSLORTET        $targetPage = cleanID($targetPage);
73*6983cdfdSLORTET        $namespace = cleanID($namespace);
74*6983cdfdSLORTET        if ($targetPage === '' || $namespace === '') return false;
75*6983cdfdSLORTET        return ($targetPage === $namespace) || (strpos($targetPage . ':', $namespace . ':') === 0);
76*6983cdfdSLORTET    }
77*6983cdfdSLORTET
78*6983cdfdSLORTET    private function pageUsesAffectedCatmenu(string $hostPageID, string $content, string $targetPage): bool
79*6983cdfdSLORTET    {
80*6983cdfdSLORTET        if (!preg_match_all('/\{\{catmenu>(.*?)\}\}/i', $content, $matches, PREG_SET_ORDER)) return false;
81*6983cdfdSLORTET        foreach ($matches as $match) {
82*6983cdfdSLORTET            $namespaceExpr = (string)($match[1] ?? '');
83*6983cdfdSLORTET            $resolvedNS = $this->resolveNamespaceExpression($namespaceExpr, $hostPageID);
84*6983cdfdSLORTET            if ($this->isTargetInNamespace($targetPage, $resolvedNS)) {
85*6983cdfdSLORTET                return true;
86*6983cdfdSLORTET            }
87*6983cdfdSLORTET        }
88*6983cdfdSLORTET        return false;
89*6983cdfdSLORTET    }
90*6983cdfdSLORTET
91*6983cdfdSLORTET    private function invalidateCacheForTarget(string $needle): void
92*6983cdfdSLORTET    {
93*6983cdfdSLORTET        global $conf;
94*6983cdfdSLORTET        $datadir = rtrim((string)$conf['datadir'], '/');
95*6983cdfdSLORTET        if ($datadir === '' || !is_dir($datadir)) return;
96*6983cdfdSLORTET
97*6983cdfdSLORTET        $it = new RecursiveIteratorIterator(
98*6983cdfdSLORTET            new RecursiveDirectoryIterator($datadir, FilesystemIterator::SKIP_DOTS)
99*6983cdfdSLORTET        );
100*6983cdfdSLORTET        foreach ($it as $fileinfo) {
101*6983cdfdSLORTET            /** @var SplFileInfo $fileinfo */
102*6983cdfdSLORTET            if (!$fileinfo->isFile()) continue;
103*6983cdfdSLORTET            if (substr($fileinfo->getFilename(), -4) !== '.txt') continue;
104*6983cdfdSLORTET
105*6983cdfdSLORTET            $path = $fileinfo->getPathname();
106*6983cdfdSLORTET            $content = @file_get_contents($path);
107*6983cdfdSLORTET            if ($content === false || strpos($content, $needle) === false) continue;
108*6983cdfdSLORTET
109*6983cdfdSLORTET            $id = pathID($path);
110*6983cdfdSLORTET            if ($id === '') continue;
111*6983cdfdSLORTET            $cache = new \dokuwiki\Cache\CacheRenderer($id, wikiFN($id), 'xhtml');
112*6983cdfdSLORTET            $cache->removeCache();
113*6983cdfdSLORTET        }
114*6983cdfdSLORTET    }
115*6983cdfdSLORTET
116*6983cdfdSLORTET    public function addJsInfo(Doku_Event $event)
117*6983cdfdSLORTET    {
118*6983cdfdSLORTET        global $ID;
119*6983cdfdSLORTET        global $JSINFO;
120*6983cdfdSLORTET        if (!isset($JSINFO['plugins'])) $JSINFO['plugins'] = [];
121*6983cdfdSLORTET        if (!isset($JSINFO['plugins']['catmenu'])) $JSINFO['plugins']['catmenu'] = [];
122*6983cdfdSLORTET        $JSINFO['plugins']['catmenu']['show_in_editor_menu'] = (bool)$this->getConf('show_in_editor_menu');
123*6983cdfdSLORTET
124*6983cdfdSLORTET        $pagesiconHelper = plugin_load('helper', 'pagesicon');
125*6983cdfdSLORTET        $JSINFO['plugins']['catmenu']['pagesicon_available'] = (bool)$pagesiconHelper;
126*6983cdfdSLORTET        if ($pagesiconHelper) {
127*6983cdfdSLORTET            $JSINFO['plugins']['catmenu']['pagesicon_upload_url'] = wl((string)$ID, ['do' => 'pagesicon']);
128*6983cdfdSLORTET        }
129*6983cdfdSLORTET    }
130*6983cdfdSLORTET
131*6983cdfdSLORTET    public function handleRender(Doku_Event $event)
132*6983cdfdSLORTET    {
133*6983cdfdSLORTET        $data = $event->data;
134*6983cdfdSLORTET        if (($data['name'] ?? '') !== 'catmenu_catmenu') return;
135*6983cdfdSLORTET
136*6983cdfdSLORTET        $event->preventDefault();
137*6983cdfdSLORTET        $event->stopPropagation();
138*6983cdfdSLORTET
139*6983cdfdSLORTET        $syntax = trim((string)($data['match'] ?? ''));
140*6983cdfdSLORTET        if ($syntax === '') {
141*6983cdfdSLORTET            $syntax = '{{catmenu>.}}';
142*6983cdfdSLORTET        }
143*6983cdfdSLORTET
144*6983cdfdSLORTET        $node = new Node('dwplugin_block');
145*6983cdfdSLORTET        $node->attr('class', 'dwplugin');
146*6983cdfdSLORTET        $node->attr('data-pluginname', 'catmenu');
147*6983cdfdSLORTET
148*6983cdfdSLORTET        $textNode = new Node('text');
149*6983cdfdSLORTET        $textNode->setText($syntax);
150*6983cdfdSLORTET        $node->addChild($textNode);
151*6983cdfdSLORTET
152*6983cdfdSLORTET        $data['renderer']->addToNodestack($node);
153*6983cdfdSLORTET    }
154*6983cdfdSLORTET
155*6983cdfdSLORTET    public function handleParseUnknown(Doku_Event $event)
156*6983cdfdSLORTET    {
157*6983cdfdSLORTET        if (($event->data['node']['type'] ?? '') !== 'catmenu') return;
158*6983cdfdSLORTET
159*6983cdfdSLORTET        $event->data['newNode'] = new CatmenuNode($event->data['node'], $event->data['parent']);
160*6983cdfdSLORTET        $event->preventDefault();
161*6983cdfdSLORTET        $event->stopPropagation();
162*6983cdfdSLORTET    }
163*6983cdfdSLORTET
164*6983cdfdSLORTET    public function handlePagesiconUpdated(Doku_Event $event): void
165*6983cdfdSLORTET    {
166*6983cdfdSLORTET        $this->invalidateCacheForTarget('{{catmenu>');
167*6983cdfdSLORTET    }
168*6983cdfdSLORTET}
169