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