1<?php 2if (!defined('DOKU_INC')) die(); 3 4class helper_plugin_extranet extends DokuWiki_Plugin 5{ 6 private function getConfiguredString(array $keys, string $fallback = ''): string 7 { 8 foreach ($keys as $key) { 9 $value = trim((string)$this->getConf($key)); 10 if ($value !== '') return $value; 11 } 12 13 return $fallback; 14 } 15 16 public function getDefaultPolicy(): string 17 { 18 $mode = strtolower(trim((string)$this->getConf('default_policy'))); 19 if (in_array($mode, ['allow', 'block', 'force_allow', 'force_block'], true)) { 20 return $mode; 21 } 22 return 'allow'; 23 } 24 25 private function hasMacro(string $content, string $macro): bool 26 { 27 return (bool)preg_match('/~~\s*' . preg_quote($macro, '/') . '\s*~~/i', $content); 28 } 29 30 public function parseRuleList($raw): array 31 { 32 $parts = preg_split('/[\r\n,;]+/', (string)$raw); 33 $rules = []; 34 foreach ($parts as $part) { 35 $rule = trim((string)$part); 36 if ($rule !== '') $rules[] = $rule; 37 } 38 return $rules; 39 } 40 41 public function idMatchesRule(string $id, string $rule): bool 42 { 43 $rule = trim($rule); 44 if ($rule === '') return false; 45 46 if (strlen($rule) > 1 && $rule[0] === '/' && substr($rule, -1) === '/') { 47 return (bool)@preg_match($rule, $id); 48 } 49 50 if (substr($rule, -1) === ':') { 51 return strpos($id, $rule) === 0; 52 } 53 54 if (strpos($rule, '*') !== false) { 55 $regex = '/^' . str_replace('\\*', '.*', preg_quote($rule, '/')) . '$/'; 56 return (bool)preg_match($regex, $id); 57 } 58 59 return $id === $rule; 60 } 61 62 public function matchesConfiguredList(string $id, string $confKey): bool 63 { 64 $rules = $this->parseRuleList($this->getConf($confKey)); 65 foreach ($rules as $rule) { 66 if ($this->idMatchesRule($id, $rule)) return true; 67 } 68 return false; 69 } 70 71 public function hasConfiguredList(string $confKey): bool 72 { 73 return !empty($this->parseRuleList($this->getConf($confKey))); 74 } 75 76 private function hasFilter(): bool 77 { 78 if ($this->hasConfiguredList('filter_list')) return true; 79 return trim((string)$this->getConf('filter_regex')) !== ''; 80 } 81 82 private function isInFilter(string $id): bool 83 { 84 // filter_list: exact pages, namespace prefixes and wildcards only — no regex entries 85 foreach ($this->parseRuleList($this->getConf('filter_list')) as $rule) { 86 $isRegex = strlen($rule) > 1 && $rule[0] === '/' && substr($rule, -1) === '/'; 87 if (!$isRegex && $this->idMatchesRule($id, $rule)) return true; 88 } 89 90 // filter_regex: dedicated regex field 91 $regex = trim((string)$this->getConf('filter_regex')); 92 if ($regex !== '' && @preg_match($regex, $id)) return true; 93 94 return false; 95 } 96 97 public function getRequestMatchKey(): string 98 { 99 return $this->getConfiguredString(['request_match_key', 'server_ip_key'], 'REMOTE_ADDR'); 100 } 101 102 public function getExtranetMatchList(): string 103 { 104 return $this->getConfiguredString(['extranet_match_list', 'extranet_ip_list']); 105 } 106 107 public function getExtranetMatchRegex(): string 108 { 109 return $this->getConfiguredString(['extranet_match_regex', 'extranet_ip_regex']); 110 } 111 112 /** 113 * Build candidate values from the configured request marker. 114 * 115 * A proxy header may contain a single marker ("extranet"), a hostname, 116 * or a comma-separated list such as X-Forwarded-For. We accept both the 117 * full raw value and each comma-separated token. 118 * 119 * @return string[] 120 */ 121 private function getRequestMatchCandidates(): array 122 { 123 $requestKey = $this->getRequestMatchKey(); 124 $rawValue = trim((string)($_SERVER[$requestKey] ?? '')); 125 if ($rawValue === '') return []; 126 127 $candidates = [$rawValue]; 128 if (strpos($rawValue, ',') !== false) { 129 foreach (explode(',', $rawValue) as $part) { 130 $part = trim((string)$part); 131 if ($part !== '') $candidates[] = $part; 132 } 133 } 134 135 return array_values(array_unique($candidates)); 136 } 137 138 public function isExtranetRequest(): bool 139 { 140 $candidates = $this->getRequestMatchCandidates(); 141 if ($candidates === []) return false; 142 143 $matchList = $this->getExtranetMatchList(); 144 if ($matchList !== '') { 145 foreach (explode(',', $matchList) as $configuredValue) { 146 $configuredValue = trim((string)$configuredValue); 147 if ($configuredValue !== '' && in_array($configuredValue, $candidates, true)) { 148 return true; 149 } 150 } 151 } 152 153 $matchRegex = $this->getExtranetMatchRegex(); 154 if ($matchRegex !== '') { 155 foreach ($candidates as $candidate) { 156 if ((bool)@preg_match($matchRegex, $candidate)) return true; 157 } 158 } 159 160 return false; 161 } 162 163 public function isClientFromExtranet(): bool 164 { 165 return $this->isExtranetRequest(); 166 } 167 168 public function isMediaVisibleFromExtranet(string $mediaID): bool 169 { 170 if (!$this->hasFilter()) return true; 171 172 $inFilter = $this->isInFilter($mediaID); 173 $defaultPolicy = $this->getDefaultPolicy(); 174 175 if ($defaultPolicy === 'force_allow' || $defaultPolicy === 'allow') { 176 return !$inFilter; 177 } 178 179 return $inFilter; 180 } 181 182 public function isPageVisibleFromExtranet(string $id, ?string $content = null): bool 183 { 184 if ($content === null) { 185 $content = rawWiki($id); 186 } 187 $content = (string)$content; 188 189 $hasFilter = $this->hasFilter(); 190 $inFilter = $hasFilter && $this->isInFilter($id); 191 192 $defaultPolicy = $this->getDefaultPolicy(); 193 194 if ($defaultPolicy === 'force_allow') { 195 return !$inFilter; 196 } 197 198 if ($defaultPolicy === 'force_block') { 199 return $inFilter; 200 } 201 202 $hasExtranet = $this->hasMacro($content, 'EXTRANET'); 203 $hasNoExtranet = $this->hasMacro($content, 'NOEXTRANET'); 204 205 if ($defaultPolicy === 'allow') { 206 return !$inFilter && !$hasNoExtranet; 207 } 208 209 // block 210 return $inFilter || $hasExtranet; 211 } 212 213 public function isPageAllowed(string $id, ?string $content = null): bool 214 { 215 if (!$this->isExtranetRequest()) return true; 216 return $this->isPageVisibleFromExtranet($id, $content); 217 } 218 219 public function isMediaAllowed(string $mediaID): bool 220 { 221 if (!$this->isExtranetRequest()) return true; 222 return $this->isMediaVisibleFromExtranet($mediaID); 223 } 224} 225