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