1<?php
2
3if (!defined('DOKU_INC')) die();
4
5use dokuwiki\Action\Exception\ActionDisabledException;
6
7class action_plugin_extranet extends DokuWiki_Action_Plugin
8{
9    /** @var helper_plugin_extranet|null */
10    private $helper = null;
11
12    protected function getHelper(): ?helper_plugin_extranet
13    {
14        if ($this->helper === null) {
15            $this->helper = plugin_load('helper', 'extranet');
16        }
17        return $this->helper;
18    }
19
20    public function register(Doku_Event_Handler $controller)
21    {
22        $controller->register_hook('DOKUWIKI_STARTED', 'AFTER', $this, 'injectJsInfo');
23        $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'addProsemirrorPolyfillAsset');
24        $controller->register_hook('PROSEMIRROR_RENDER_PLUGIN', 'BEFORE', $this, 'handleRenderForProsemirror');
25        $controller->register_hook('ACTION_ACT_PREPROCESS', 'AFTER', $this, 'syncTextFromProsemirrorState');
26        $controller->register_hook('IO_WIKIPAGE_WRITE', 'BEFORE', $this, 'syncMacroFromProsemirrorStateBeforeWrite');
27        $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handleProsemirrorSwitchToText');
28
29        $helper = $this->getHelper();
30        if ($helper && $helper->isExtranetRequest()) {
31            $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'blockConfiguredActions');
32            $controller->register_hook('AUTH_LOGIN_CHECK', 'AFTER', $this, 'disableActions');
33            $controller->register_hook('PARSER_CACHE_USE', 'BEFORE', $this, 'createExtranetCache');
34            $controller->register_hook('IO_WIKIPAGE_READ', 'AFTER', $this, 'displayHideMessageIfRestricted');
35            $controller->register_hook('FETCH_MEDIA_STATUS', 'BEFORE', $this, 'hideMediaIfRestricted');
36        }
37    }
38
39    public function handleRenderForProsemirror(Doku_Event $event, $param): void
40    {
41        $data = $event->data;
42        $name = strtolower(trim((string)($data['name'] ?? '')));
43        if ($name !== 'extranet') return;
44
45        $match = trim((string)($data['match'] ?? ''));
46        if (!preg_match('/^~~\s*(NOEXTRANET|EXTRANET)\s*~~$/i', $match, $matches)) return;
47
48        $renderer = $data['renderer'] ?? null;
49        if (!is_object($renderer) || !isset($renderer->nodestack) || !method_exists($renderer->nodestack, 'getDocNode')) {
50            return;
51        }
52
53        $macro = strtoupper($matches[1]);
54        $docNode = $renderer->nodestack->getDocNode();
55        if ($macro === 'NOEXTRANET') {
56            $docNode->attr('noextranet', true);
57            $docNode->attr('extranet', false);
58        } else {
59            $docNode->attr('extranet', true);
60            $docNode->attr('noextranet', false);
61        }
62
63        $event->preventDefault();
64        $event->stopPropagation();
65    }
66
67    public function addProsemirrorPolyfillAsset(Doku_Event $event, $param): void
68    {
69        global $ACT;
70
71        if (!in_array((string)$ACT, ['edit', 'preview'], true)) return;
72        if (defined('DOKUWIKI_PM_FILE_STATE_POLYFILL_INCLUDED')) return;
73        if (empty($event->data) || !is_array($event->data)) return;
74
75        define('DOKUWIKI_PM_FILE_STATE_POLYFILL_INCLUDED', 1);
76        $path = DOKU_INC . 'lib/plugins/extranet/script/prosemirror_file_state_polyfill.js';
77        $version = @filemtime($path) ?: time();
78
79        $event->data['script'][] = [
80            'type' => 'text/javascript',
81            'src' => DOKU_BASE . 'lib/plugins/extranet/script/prosemirror_file_state_polyfill.js?v=' . rawurlencode((string)$version),
82            '_data' => '',
83            'defer' => 'defer',
84        ];
85    }
86
87    public function handleProsemirrorSwitchToText(Doku_Event $event, $param): void
88    {
89        global $INPUT, $ID;
90
91        if ($event->data !== 'plugin_prosemirror_switch_editors') return;
92        if ($INPUT->bool('getJSON')) return;
93
94        $json = (string)$INPUT->str('data');
95        if ($json === '') return;
96
97        $ID = $INPUT->str('id');
98
99        /** @var helper_plugin_prosemirror $helper */
100        $helper = plugin_load('helper', 'prosemirror');
101        if (!$helper) return;
102
103        try {
104            $syntax = $helper->getSyntaxFromProsemirrorData($json);
105        } catch (Throwable $e) {
106            return;
107        }
108
109        $macroState = $this->extractMacroStateFromJson($json);
110        if ($macroState === null) {
111            $macroState = $this->extractMacroStateFromSyntax($syntax);
112        }
113        $syntax = $this->applyMacroStateToSyntax($syntax, $macroState);
114
115        $event->preventDefault();
116        $event->stopPropagation();
117        header('Content-Type: application/json');
118        echo json_encode(['text' => $syntax]);
119    }
120
121    public function syncTextFromProsemirrorState(Doku_Event $event, $param): void
122    {
123        global $INPUT, $TEXT;
124
125        if ($INPUT->server->str('REQUEST_METHOD') !== 'POST') return;
126        if (!in_array((string)$event->data, ['save', 'preview'], true)) return;
127        if (!$INPUT->post->has('prosemirror_json')) return;
128        if (!get_doku_pref('plugin_prosemirror_useWYSIWYG', false)) return;
129
130        $macroState = $this->extractMacroStateFromJson((string)$INPUT->post->str('prosemirror_json'));
131        if ($macroState === null) {
132            $macroState = $this->extractMacroStateFromSyntax((string)$TEXT);
133        }
134
135        $TEXT = $this->applyMacroStateToSyntax((string)$TEXT, $macroState);
136    }
137
138    public function syncMacroFromProsemirrorStateBeforeWrite(Doku_Event $event, $param): void
139    {
140        global $INPUT;
141
142        if (!$INPUT->post->has('prosemirror_json')) return;
143        $macroState = $this->extractMacroStateFromJson((string)$INPUT->post->str('prosemirror_json'));
144
145        $helper = $this->getHelper();
146        $defaultPolicy = $helper ? $helper->getDefaultPolicy() : 'allow';
147        if ($macroState === null && !in_array($defaultPolicy, ['force_allow', 'force_block'], true)) return;
148
149        if (!isset($event->data[0]) || !is_array($event->data[0]) || !isset($event->data[0][1])) return;
150        $event->data[0][1] = $this->applyMacroStateToSyntax((string)$event->data[0][1], $macroState);
151    }
152
153    protected function extractMacroStateFromJson(string $json): ?array
154    {
155        if ($json === '') return null;
156        $data = json_decode($json, true);
157        if (!is_array($data)) return null;
158        $attrs = $data['attrs'] ?? null;
159        if (!is_array($attrs)) return null;
160
161        $hasNoExtranetAttr = array_key_exists('noextranet', $attrs);
162        $hasExtranetAttr = array_key_exists('extranet', $attrs);
163        if (!$hasNoExtranetAttr && !$hasExtranetAttr) return null;
164
165        return [
166            'noextranet' => $hasNoExtranetAttr ? (bool)$attrs['noextranet'] : false,
167            'extranet' => $hasExtranetAttr ? (bool)$attrs['extranet'] : false,
168        ];
169    }
170
171    protected function applyMacroStateToSyntax(string $content, ?array $macroState): string
172    {
173        $content = preg_replace('/^\h*~~\h*(NOEXTRANET|EXTRANET)\h*~~\h*$(\R)?/imu', '', $content);
174        $content = rtrim((string)$content);
175
176        $helper = $this->getHelper();
177        $defaultPolicy = $helper ? $helper->getDefaultPolicy() : 'allow';
178
179        if ($macroState === null || in_array($defaultPolicy, ['force_allow', 'force_block'], true)) {
180            return $content !== '' ? $content . "\n" : '';
181        }
182
183        if (!empty($macroState['noextranet'])) {
184            $content .= "\n\n~~NOEXTRANET~~\n";
185        } elseif (!empty($macroState['extranet'])) {
186            $content .= "\n\n~~EXTRANET~~\n";
187        } elseif ($content !== '') {
188            $content .= "\n";
189        }
190
191        return $content;
192    }
193
194    protected function extractMacroStateFromSyntax(string $content): ?array
195    {
196        $hasNoExtranet = (bool)preg_match('/~~\s*NOEXTRANET\s*~~/i', $content);
197        $hasExtranet = (bool)preg_match('/~~\s*EXTRANET\s*~~/i', $content);
198
199        if (!$hasNoExtranet && !$hasExtranet) {
200            return null;
201        }
202
203        return [
204            'noextranet' => $hasNoExtranet,
205            'extranet' => $hasExtranet,
206        ];
207    }
208
209    public function injectJsInfo(Doku_Event $event, $param): void
210    {
211        global $JSINFO;
212
213        $helper = $this->getHelper();
214        $mode = $helper ? $helper->getDefaultPolicy() : 'allow';
215
216        $JSINFO['plugin_extranet_default_mode'] = $mode;
217        $JSINFO['plugin_extranet_label_noextranet'] = (string)$this->getLang('label_noextranet');
218        $JSINFO['plugin_extranet_label_extranet'] = (string)$this->getLang('label_extranet');
219    }
220
221    protected function isConfiguredActionDisabled(string $actionName): bool
222    {
223        $actionName = strtolower(trim($actionName));
224        if ($actionName === '') return false;
225
226        $helper = $this->getHelper();
227        $actions = $helper ? $helper->parseRuleList($this->getConf('disable_actions')) : [];
228        $actions = array_map(static function ($action) {
229            return strtolower(trim((string)$action));
230        }, $actions);
231
232        return in_array($actionName, $actions, true);
233    }
234
235    protected function isRestrictedPageActionDisabled(string $actionName): bool
236    {
237        $actionName = strtolower(trim($actionName));
238        if ($actionName === '') return false;
239
240        $helper = $this->getHelper();
241        $actions = $helper ? $helper->parseRuleList($this->getConf('restricted_disable_actions')) : [];
242        $actions = array_map(static function ($action) {
243            return strtolower(trim((string)$action));
244        }, $actions);
245
246        if (!in_array($actionName, $actions, true)) return false;
247
248        global $ID;
249        return $helper && !$helper->isPageAllowed((string)$ID);
250    }
251
252    public function blockConfiguredActions(Doku_Event $event, $param): void
253    {
254        $actionName = (string)$event->data;
255        if ($this->isConfiguredActionDisabled($actionName) || $this->isRestrictedPageActionDisabled($actionName)) {
256            throw new ActionDisabledException();
257        }
258    }
259
260    protected function isContentRestrictedFromExtranet(string $content): bool
261    {
262        global $ID;
263        $helper = $this->getHelper();
264        if (!$helper) return false;
265        return !$helper->isPageVisibleFromExtranet((string)$ID, $content);
266    }
267
268    protected function isMediaRestrictedFromExtranet(string $media): bool
269    {
270        $helper = $this->getHelper();
271        if (!$helper) return false;
272        return !$helper->isMediaAllowed($media);
273    }
274
275    public function disableActions(Doku_Event $event, $param): void
276    {
277        if (!empty($this->getConf('disable_actions'))) {
278            global $conf;
279            $conf['disableactions'] = (!empty($conf['disableactions']) ? $conf['disableactions'] . ',' : '') . $this->getConf('disable_actions');
280        }
281    }
282
283    public function createExtranetCache(Doku_Event $event, $param): void
284    {
285        $cache = $event->data;
286        $cache->key .= '#extranet';
287        $cache->cache = getCacheName($cache->key, $cache->ext);
288    }
289
290    public function displayHideMessageIfRestricted(Doku_Event $event, $param): void
291    {
292        if (!$this->isContentRestrictedFromExtranet((string)$event->result)) return;
293
294        $result = '';
295
296        if ($this->getConf('preserve_first_title')) {
297            $titlePattern = '/(?:^|\v)(={2,6}.+={2,})(?:\v|$)/';
298            preg_match($titlePattern, $event->result, $matches);
299
300            if (!empty($matches[0])) {
301                $result .= $matches[0] . "\r\n";
302            }
303        }
304        $result .= $this->getConf('message_prefix') . $this->getLang('hidden_message') . $this->getConf('message_suffix');
305
306        $event->result = $result;
307    }
308
309    public function hideMediaIfRestricted(Doku_Event $event, $param): void
310    {
311        $hideFilesMode = $this->getHideFilesMode();
312        if ($hideFilesMode === 'none') return;
313
314        $mediaID = (string)($event->data['media'] ?? '');
315        if ($mediaID === '') return;
316        if ($hideFilesMode === 'except_pageicons' && $this->isPagesIconMedia($mediaID)) return;
317        if (!$this->isMediaRestrictedFromExtranet($mediaID)) return;
318
319        $event->data['file'] = dirname(__FILE__) . '/images/restricted.png';
320        $event->data['orig'] = $event->data['file'];
321        $event->data['status'] = 200;
322        $event->data['statusmessage'] = 'OK';
323        $event->data['mime'] = 'image/png';
324        $event->data['download'] = false;
325        $event->data['cache'] = false;
326        $event->data['ispublic'] = false;
327    }
328
329    protected function getHideFilesMode(): string
330    {
331        $value = $this->getConf('hide_files');
332
333        if ($value === true || $value === 1 || $value === '1') return 'all';
334        if ($value === false || $value === 0 || $value === '0' || $value === '') return 'none';
335
336        $value = strtolower(trim((string)$value));
337        if (!in_array($value, ['all', 'except_pageicons', 'none'], true)) {
338            return 'none';
339        }
340
341        return $value;
342    }
343
344    protected function isPagesIconMedia(string $mediaID): bool
345    {
346        $mediaID = cleanID($mediaID);
347        if ($mediaID === '') return false;
348
349        /** @var helper_plugin_pagesicon|null $helper */
350        $helper = plugin_load('helper', 'pagesicon');
351        if (!$helper || !method_exists($helper, 'isPageIconMedia')) return false;
352
353        return (bool)$helper->isPageIconMedia($mediaID);
354    }
355}
356