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