xref: /plugin/bpmnio/inc/link_processor.php (revision 57e8c51e519401ec595e8597ab561491ffb8cb9e)
1<?php
2
3class plugin_bpmnio_link_processor
4{
5    /**
6     * @return array{xml: string, links: array<string, array{href: string, target: string}>}
7     */
8    public static function buildPayload(string $xml): array
9    {
10        $xml = trim($xml);
11        if ($xml === '') {
12            return ['xml' => $xml, 'links' => []];
13        }
14
15        $document = new DOMDocument('1.0', 'UTF-8');
16        $document->preserveWhiteSpace = true;
17        $document->formatOutput = false;
18
19        $previous = libxml_use_internal_errors(true);
20        $loaded = $document->loadXML($xml);
21        libxml_clear_errors();
22        libxml_use_internal_errors($previous);
23
24        if (!$loaded) {
25            return ['xml' => $xml, 'links' => []];
26        }
27
28        $links = [];
29        foreach ($document->getElementsByTagName('*') as $element) {
30            if (!$element->hasAttribute('id') || !$element->hasAttribute('name')) {
31                continue;
32            }
33
34            $parsedLink = self::parseLinkMarkup($element->getAttribute('name'));
35            if ($parsedLink === null) {
36                continue;
37            }
38
39            $target = self::resolveTarget($parsedLink['target']);
40            if ($target === null || auth_quickaclcheck($target) < AUTH_READ) {
41                continue;
42            }
43
44            $elementId = trim($element->getAttribute('id'));
45            if ($elementId === '') {
46                continue;
47            }
48
49            $label = $parsedLink['label'] !== '' ? $parsedLink['label'] : $target;
50
51            $element->setAttribute('name', $label);
52            $links[$elementId] = [
53                'href' => self::buildHref($target),
54                'target' => $target,
55            ];
56        }
57
58        $renderXml = $document->saveXML();
59        if ($renderXml === false) {
60            $renderXml = $xml;
61        }
62
63        return ['xml' => $renderXml, 'links' => $links];
64    }
65
66    /**
67     * @return array{target: string, label: string}|null
68     */
69    private static function parseLinkMarkup(string $value): ?array
70    {
71        $value = trim($value);
72        if (!preg_match('/^\[\[([^\]|]+)(?:\|([^\]]*))?\]\]$/', $value, $matches)) {
73            return null;
74        }
75
76        $target = trim($matches[1]);
77        if ($target === '') {
78            return null;
79        }
80
81        return [
82            'target' => $target,
83            'label' => isset($matches[2]) ? trim($matches[2]) : '',
84        ];
85    }
86
87    private static function resolveTarget(string $target): ?string
88    {
89        global $ID;
90
91        $target = trim($target);
92        if ($target === '' || preg_match('#^[a-z][a-z0-9+.-]*://#i', $target)) {
93            return null;
94        }
95
96        if (str_starts_with($target, ':')) {
97            return self::normalizeId(trim($target, ':'));
98        }
99
100        $baseNamespace = self::getNamespace((string) $ID);
101        if (str_starts_with($target, '.')) {
102            $segments = $baseNamespace === '' ? [] : explode(':', $baseNamespace);
103            foreach (explode(':', $target) as $segment) {
104                $segment = trim($segment);
105                if ($segment === '' || $segment === '.') {
106                    continue;
107                }
108                if ($segment === '..') {
109                    array_pop($segments);
110                    continue;
111                }
112                $segments[] = $segment;
113            }
114
115            return self::normalizeId(implode(':', $segments));
116        }
117
118        if (str_contains($target, ':')) {
119            return self::normalizeId($target);
120        }
121
122        $pageId = $baseNamespace === '' ? $target : $baseNamespace . ':' . $target;
123        return self::normalizeId($pageId);
124    }
125
126    private static function buildHref(string $target): string
127    {
128        return DOKU_BASE . 'doku.php?' . http_build_query(['id' => $target], '', '&', PHP_QUERY_RFC3986);
129    }
130
131    private static function getNamespace(string $pageId): string
132    {
133        $pos = strrpos($pageId, ':');
134        if ($pos === false) {
135            return '';
136        }
137
138        return substr($pageId, 0, $pos);
139    }
140
141    private static function normalizeId(string $value): ?string
142    {
143        $value = trim($value);
144        if ($value === '') {
145            return null;
146        }
147
148        if (function_exists('cleanID')) {
149            $value = cleanID($value);
150        }
151
152        $segments = [];
153        foreach (explode(':', $value) as $segment) {
154            $segment = trim($segment);
155            if ($segment === '' || $segment === '.') {
156                continue;
157            }
158            if ($segment === '..') {
159                array_pop($segments);
160                continue;
161            }
162            $segments[] = $segment;
163        }
164
165        if ($segments === []) {
166            return null;
167        }
168
169        return implode(':', $segments);
170    }
171}
172