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