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