xref: /plugin/extranet/action.php (revision ef5bb1f3884025c3556b3c46e33332ca386a73a6) !
12670d39fSLORTET<?php
22670d39fSLORTET
32670d39fSLORTETif (!defined('DOKU_INC')) die();
42670d39fSLORTET
52670d39fSLORTETuse dokuwiki\Action\Exception\ActionDisabledException;
62670d39fSLORTET
72670d39fSLORTETclass action_plugin_extranet extends DokuWiki_Action_Plugin
82670d39fSLORTET{
9*ef5bb1f3SLORTET    /** @var helper_plugin_extranet|null */
10*ef5bb1f3SLORTET    private $helper = null;
11*ef5bb1f3SLORTET
12*ef5bb1f3SLORTET    protected function getHelper(): ?helper_plugin_extranet
13*ef5bb1f3SLORTET    {
14*ef5bb1f3SLORTET        if ($this->helper === null) {
15*ef5bb1f3SLORTET            $this->helper = plugin_load('helper', 'extranet');
16*ef5bb1f3SLORTET        }
17*ef5bb1f3SLORTET        return $this->helper;
18*ef5bb1f3SLORTET    }
19*ef5bb1f3SLORTET
202670d39fSLORTET    public function register(Doku_Event_Handler $controller)
212670d39fSLORTET    {
22*ef5bb1f3SLORTET        $controller->register_hook('DOKUWIKI_STARTED', 'AFTER', $this, 'injectJsInfo');
232670d39fSLORTET        $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'addProsemirrorPolyfillAsset');
242670d39fSLORTET        $controller->register_hook('PROSEMIRROR_RENDER_PLUGIN', 'BEFORE', $this, 'handleRenderForProsemirror');
252670d39fSLORTET        $controller->register_hook('ACTION_ACT_PREPROCESS', 'AFTER', $this, 'syncTextFromProsemirrorState');
262670d39fSLORTET        $controller->register_hook('IO_WIKIPAGE_WRITE', 'BEFORE', $this, 'syncMacroFromProsemirrorStateBeforeWrite');
272670d39fSLORTET        $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handleProsemirrorSwitchToText');
282670d39fSLORTET
29*ef5bb1f3SLORTET        $helper = $this->getHelper();
30*ef5bb1f3SLORTET        if ($helper && $helper->isExtranetRequest()) {
312670d39fSLORTET            $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'blockConfiguredActions');
32*ef5bb1f3SLORTET            $controller->register_hook('AUTH_LOGIN_CHECK', 'AFTER', $this, 'disableActions');
33*ef5bb1f3SLORTET            $controller->register_hook('PARSER_CACHE_USE', 'BEFORE', $this, 'createExtranetCache');
34*ef5bb1f3SLORTET            $controller->register_hook('IO_WIKIPAGE_READ', 'AFTER', $this, 'displayHideMessageIfRestricted');
35*ef5bb1f3SLORTET            $controller->register_hook('FETCH_MEDIA_STATUS', 'BEFORE', $this, 'hideMediaIfRestricted');
362670d39fSLORTET        }
372670d39fSLORTET    }
382670d39fSLORTET
392670d39fSLORTET    public function handleRenderForProsemirror(Doku_Event $event, $param): void
402670d39fSLORTET    {
412670d39fSLORTET        $data = $event->data;
422670d39fSLORTET        $name = strtolower(trim((string)($data['name'] ?? '')));
432670d39fSLORTET        if ($name !== 'extranet') return;
442670d39fSLORTET
452670d39fSLORTET        $match = trim((string)($data['match'] ?? ''));
462670d39fSLORTET        if (!preg_match('/^~~\s*(NOEXTRANET|EXTRANET)\s*~~$/i', $match, $matches)) return;
472670d39fSLORTET
482670d39fSLORTET        $renderer = $data['renderer'] ?? null;
492670d39fSLORTET        if (!is_object($renderer) || !isset($renderer->nodestack) || !method_exists($renderer->nodestack, 'getDocNode')) {
502670d39fSLORTET            return;
512670d39fSLORTET        }
522670d39fSLORTET
532670d39fSLORTET        $macro = strtoupper($matches[1]);
542670d39fSLORTET        $docNode = $renderer->nodestack->getDocNode();
552670d39fSLORTET        if ($macro === 'NOEXTRANET') {
562670d39fSLORTET            $docNode->attr('noextranet', true);
572670d39fSLORTET            $docNode->attr('extranet', false);
582670d39fSLORTET        } else {
592670d39fSLORTET            $docNode->attr('extranet', true);
602670d39fSLORTET            $docNode->attr('noextranet', false);
612670d39fSLORTET        }
622670d39fSLORTET
632670d39fSLORTET        $event->preventDefault();
642670d39fSLORTET        $event->stopPropagation();
652670d39fSLORTET    }
662670d39fSLORTET
672670d39fSLORTET    public function addProsemirrorPolyfillAsset(Doku_Event $event, $param): void
682670d39fSLORTET    {
692670d39fSLORTET        global $ACT;
702670d39fSLORTET
712670d39fSLORTET        if (!in_array((string)$ACT, ['edit', 'preview'], true)) return;
722670d39fSLORTET        if (defined('DOKUWIKI_PM_FILE_STATE_POLYFILL_INCLUDED')) return;
732670d39fSLORTET        if (empty($event->data) || !is_array($event->data)) return;
742670d39fSLORTET
752670d39fSLORTET        define('DOKUWIKI_PM_FILE_STATE_POLYFILL_INCLUDED', 1);
762670d39fSLORTET        $path = DOKU_INC . 'lib/plugins/extranet/script/prosemirror_file_state_polyfill.js';
772670d39fSLORTET        $version = @filemtime($path) ?: time();
782670d39fSLORTET
792670d39fSLORTET        $event->data['script'][] = [
802670d39fSLORTET            'type' => 'text/javascript',
812670d39fSLORTET            'src' => DOKU_BASE . 'lib/plugins/extranet/script/prosemirror_file_state_polyfill.js?v=' . rawurlencode((string)$version),
822670d39fSLORTET            '_data' => '',
832670d39fSLORTET            'defer' => 'defer',
842670d39fSLORTET        ];
852670d39fSLORTET    }
862670d39fSLORTET
872670d39fSLORTET    public function handleProsemirrorSwitchToText(Doku_Event $event, $param): void
882670d39fSLORTET    {
892670d39fSLORTET        global $INPUT, $ID;
902670d39fSLORTET
912670d39fSLORTET        if ($event->data !== 'plugin_prosemirror_switch_editors') return;
922670d39fSLORTET        if ($INPUT->bool('getJSON')) return;
932670d39fSLORTET
942670d39fSLORTET        $json = (string)$INPUT->str('data');
952670d39fSLORTET        if ($json === '') return;
962670d39fSLORTET
972670d39fSLORTET        $ID = $INPUT->str('id');
982670d39fSLORTET
992670d39fSLORTET        /** @var helper_plugin_prosemirror $helper */
1002670d39fSLORTET        $helper = plugin_load('helper', 'prosemirror');
1012670d39fSLORTET        if (!$helper) return;
1022670d39fSLORTET
1032670d39fSLORTET        try {
1042670d39fSLORTET            $syntax = $helper->getSyntaxFromProsemirrorData($json);
1052670d39fSLORTET        } catch (Throwable $e) {
1062670d39fSLORTET            return;
1072670d39fSLORTET        }
1082670d39fSLORTET
1092670d39fSLORTET        $macroState = $this->extractMacroStateFromJson($json);
1102670d39fSLORTET        if ($macroState === null) {
1112670d39fSLORTET            $macroState = $this->extractMacroStateFromSyntax($syntax);
1122670d39fSLORTET        }
1132670d39fSLORTET        $syntax = $this->applyMacroStateToSyntax($syntax, $macroState);
1142670d39fSLORTET
1152670d39fSLORTET        $event->preventDefault();
1162670d39fSLORTET        $event->stopPropagation();
1172670d39fSLORTET        header('Content-Type: application/json');
1182670d39fSLORTET        echo json_encode(['text' => $syntax]);
1192670d39fSLORTET    }
1202670d39fSLORTET
1212670d39fSLORTET    public function syncTextFromProsemirrorState(Doku_Event $event, $param): void
1222670d39fSLORTET    {
1232670d39fSLORTET        global $INPUT, $TEXT;
1242670d39fSLORTET
1252670d39fSLORTET        if ($INPUT->server->str('REQUEST_METHOD') !== 'POST') return;
1262670d39fSLORTET        if (!in_array((string)$event->data, ['save', 'preview'], true)) return;
1272670d39fSLORTET        if (!$INPUT->post->has('prosemirror_json')) return;
1282670d39fSLORTET        if (!get_doku_pref('plugin_prosemirror_useWYSIWYG', false)) return;
1292670d39fSLORTET
1302670d39fSLORTET        $macroState = $this->extractMacroStateFromJson((string)$INPUT->post->str('prosemirror_json'));
1312670d39fSLORTET        if ($macroState === null) {
1322670d39fSLORTET            $macroState = $this->extractMacroStateFromSyntax((string)$TEXT);
1332670d39fSLORTET        }
1342670d39fSLORTET
1352670d39fSLORTET        $TEXT = $this->applyMacroStateToSyntax((string)$TEXT, $macroState);
1362670d39fSLORTET    }
1372670d39fSLORTET
1382670d39fSLORTET    public function syncMacroFromProsemirrorStateBeforeWrite(Doku_Event $event, $param): void
1392670d39fSLORTET    {
140*ef5bb1f3SLORTET        global $INPUT;
141*ef5bb1f3SLORTET
142*ef5bb1f3SLORTET        if (!$INPUT->post->has('prosemirror_json')) return;
143*ef5bb1f3SLORTET        $macroState = $this->extractMacroStateFromJson((string)$INPUT->post->str('prosemirror_json'));
144*ef5bb1f3SLORTET
145*ef5bb1f3SLORTET        $helper = $this->getHelper();
146*ef5bb1f3SLORTET        $defaultPolicy = $helper ? $helper->getDefaultPolicy() : 'allow';
147*ef5bb1f3SLORTET        if ($macroState === null && !in_array($defaultPolicy, ['force_allow', 'force_block'], true)) return;
1482670d39fSLORTET
1492670d39fSLORTET        if (!isset($event->data[0]) || !is_array($event->data[0]) || !isset($event->data[0][1])) return;
1502670d39fSLORTET        $event->data[0][1] = $this->applyMacroStateToSyntax((string)$event->data[0][1], $macroState);
1512670d39fSLORTET    }
1522670d39fSLORTET
1532670d39fSLORTET    protected function extractMacroStateFromJson(string $json): ?array
1542670d39fSLORTET    {
1552670d39fSLORTET        if ($json === '') return null;
1562670d39fSLORTET        $data = json_decode($json, true);
1572670d39fSLORTET        if (!is_array($data)) return null;
1582670d39fSLORTET        $attrs = $data['attrs'] ?? null;
1592670d39fSLORTET        if (!is_array($attrs)) return null;
1602670d39fSLORTET
1612670d39fSLORTET        $hasNoExtranetAttr = array_key_exists('noextranet', $attrs);
1622670d39fSLORTET        $hasExtranetAttr = array_key_exists('extranet', $attrs);
1632670d39fSLORTET        if (!$hasNoExtranetAttr && !$hasExtranetAttr) return null;
1642670d39fSLORTET
1652670d39fSLORTET        return [
1662670d39fSLORTET            'noextranet' => $hasNoExtranetAttr ? (bool)$attrs['noextranet'] : false,
1672670d39fSLORTET            'extranet' => $hasExtranetAttr ? (bool)$attrs['extranet'] : false,
1682670d39fSLORTET        ];
1692670d39fSLORTET    }
1702670d39fSLORTET
1712670d39fSLORTET    protected function applyMacroStateToSyntax(string $content, ?array $macroState): string
1722670d39fSLORTET    {
1732670d39fSLORTET        $content = preg_replace('/^\h*~~\h*(NOEXTRANET|EXTRANET)\h*~~\h*$(\R)?/imu', '', $content);
1742670d39fSLORTET        $content = rtrim((string)$content);
1752670d39fSLORTET
176*ef5bb1f3SLORTET        $helper = $this->getHelper();
177*ef5bb1f3SLORTET        $defaultPolicy = $helper ? $helper->getDefaultPolicy() : 'allow';
178*ef5bb1f3SLORTET
179*ef5bb1f3SLORTET        if ($macroState === null || in_array($defaultPolicy, ['force_allow', 'force_block'], true)) {
180*ef5bb1f3SLORTET            return $content !== '' ? $content . "\n" : '';
181*ef5bb1f3SLORTET        }
182*ef5bb1f3SLORTET
1832670d39fSLORTET        if (!empty($macroState['noextranet'])) {
1842670d39fSLORTET            $content .= "\n\n~~NOEXTRANET~~\n";
1852670d39fSLORTET        } elseif (!empty($macroState['extranet'])) {
1862670d39fSLORTET            $content .= "\n\n~~EXTRANET~~\n";
1872670d39fSLORTET        } elseif ($content !== '') {
1882670d39fSLORTET            $content .= "\n";
1892670d39fSLORTET        }
1902670d39fSLORTET
1912670d39fSLORTET        return $content;
1922670d39fSLORTET    }
1932670d39fSLORTET
1942670d39fSLORTET    protected function extractMacroStateFromSyntax(string $content): ?array
1952670d39fSLORTET    {
1962670d39fSLORTET        $hasNoExtranet = (bool)preg_match('/~~\s*NOEXTRANET\s*~~/i', $content);
1972670d39fSLORTET        $hasExtranet = (bool)preg_match('/~~\s*EXTRANET\s*~~/i', $content);
1982670d39fSLORTET
1992670d39fSLORTET        if (!$hasNoExtranet && !$hasExtranet) {
2002670d39fSLORTET            return null;
2012670d39fSLORTET        }
2022670d39fSLORTET
2032670d39fSLORTET        return [
2042670d39fSLORTET            'noextranet' => $hasNoExtranet,
2052670d39fSLORTET            'extranet' => $hasExtranet,
2062670d39fSLORTET        ];
2072670d39fSLORTET    }
2082670d39fSLORTET
209*ef5bb1f3SLORTET    public function injectJsInfo(Doku_Event $event, $param): void
2102670d39fSLORTET    {
2112670d39fSLORTET        global $JSINFO;
212*ef5bb1f3SLORTET
213*ef5bb1f3SLORTET        $helper = $this->getHelper();
214*ef5bb1f3SLORTET        $mode = $helper ? $helper->getDefaultPolicy() : 'allow';
2152670d39fSLORTET
2162670d39fSLORTET        $JSINFO['plugin_extranet_default_mode'] = $mode;
2172670d39fSLORTET        $JSINFO['plugin_extranet_label_noextranet'] = (string)$this->getLang('label_noextranet');
2182670d39fSLORTET        $JSINFO['plugin_extranet_label_extranet'] = (string)$this->getLang('label_extranet');
2192670d39fSLORTET    }
2202670d39fSLORTET
221*ef5bb1f3SLORTET    protected function isConfiguredActionDisabled(string $actionName): bool
2222670d39fSLORTET    {
2232670d39fSLORTET        $actionName = strtolower(trim($actionName));
2242670d39fSLORTET        if ($actionName === '') return false;
2252670d39fSLORTET
226*ef5bb1f3SLORTET        $helper = $this->getHelper();
227*ef5bb1f3SLORTET        $actions = $helper ? $helper->parseRuleList($this->getConf('disable_actions')) : [];
2282670d39fSLORTET        $actions = array_map(static function ($action) {
2292670d39fSLORTET            return strtolower(trim((string)$action));
2302670d39fSLORTET        }, $actions);
2312670d39fSLORTET
2322670d39fSLORTET        return in_array($actionName, $actions, true);
2332670d39fSLORTET    }
2342670d39fSLORTET
235*ef5bb1f3SLORTET    protected function isRestrictedPageActionDisabled(string $actionName): bool
236*ef5bb1f3SLORTET    {
237*ef5bb1f3SLORTET        $actionName = strtolower(trim($actionName));
238*ef5bb1f3SLORTET        if ($actionName === '') return false;
239*ef5bb1f3SLORTET
240*ef5bb1f3SLORTET        $helper = $this->getHelper();
241*ef5bb1f3SLORTET        $actions = $helper ? $helper->parseRuleList($this->getConf('restricted_disable_actions')) : [];
242*ef5bb1f3SLORTET        $actions = array_map(static function ($action) {
243*ef5bb1f3SLORTET            return strtolower(trim((string)$action));
244*ef5bb1f3SLORTET        }, $actions);
245*ef5bb1f3SLORTET
246*ef5bb1f3SLORTET        if (!in_array($actionName, $actions, true)) return false;
247*ef5bb1f3SLORTET
248*ef5bb1f3SLORTET        global $ID;
249*ef5bb1f3SLORTET        return $helper && !$helper->isPageAllowed((string)$ID);
250*ef5bb1f3SLORTET    }
251*ef5bb1f3SLORTET
2522670d39fSLORTET    public function blockConfiguredActions(Doku_Event $event, $param): void
2532670d39fSLORTET    {
2542670d39fSLORTET        $actionName = (string)$event->data;
255*ef5bb1f3SLORTET        if ($this->isConfiguredActionDisabled($actionName) || $this->isRestrictedPageActionDisabled($actionName)) {
2562670d39fSLORTET            throw new ActionDisabledException();
2572670d39fSLORTET        }
2582670d39fSLORTET    }
2592670d39fSLORTET
260*ef5bb1f3SLORTET    protected function isContentRestrictedFromExtranet(string $content): bool
2612670d39fSLORTET    {
2622670d39fSLORTET        global $ID;
263*ef5bb1f3SLORTET        $helper = $this->getHelper();
264*ef5bb1f3SLORTET        if (!$helper) return false;
265*ef5bb1f3SLORTET        return !$helper->isPageVisibleFromExtranet((string)$ID, $content);
2662670d39fSLORTET    }
2672670d39fSLORTET
268*ef5bb1f3SLORTET    protected function isMediaRestrictedFromExtranet(string $media): bool
2692670d39fSLORTET    {
270*ef5bb1f3SLORTET        $helper = $this->getHelper();
271*ef5bb1f3SLORTET        if (!$helper) return false;
272*ef5bb1f3SLORTET        return !$helper->isMediaAllowed($media);
2732670d39fSLORTET    }
2742670d39fSLORTET
275*ef5bb1f3SLORTET    public function disableActions(Doku_Event $event, $param): void
2762670d39fSLORTET    {
2772670d39fSLORTET        if (!empty($this->getConf('disable_actions'))) {
2782670d39fSLORTET            global $conf;
2792670d39fSLORTET            $conf['disableactions'] = (!empty($conf['disableactions']) ? $conf['disableactions'] . ',' : '') . $this->getConf('disable_actions');
2802670d39fSLORTET        }
2812670d39fSLORTET    }
2822670d39fSLORTET
283*ef5bb1f3SLORTET    public function createExtranetCache(Doku_Event $event, $param): void
2842670d39fSLORTET    {
2852670d39fSLORTET        $cache = $event->data;
2862670d39fSLORTET        $cache->key .= '#extranet';
2872670d39fSLORTET        $cache->cache = getCacheName($cache->key, $cache->ext);
2882670d39fSLORTET    }
2892670d39fSLORTET
290*ef5bb1f3SLORTET    public function displayHideMessageIfRestricted(Doku_Event $event, $param): void
2912670d39fSLORTET    {
292*ef5bb1f3SLORTET        if (!$this->isContentRestrictedFromExtranet((string)$event->result)) return;
2932670d39fSLORTET
2942670d39fSLORTET        $result = '';
2952670d39fSLORTET
2962670d39fSLORTET        if ($this->getConf('preserve_first_title')) {
2972670d39fSLORTET            $titlePattern = '/(?:^|\v)(={2,6}.+={2,})(?:\v|$)/';
2982670d39fSLORTET            preg_match($titlePattern, $event->result, $matches);
2992670d39fSLORTET
3002670d39fSLORTET            if (!empty($matches[0])) {
3012670d39fSLORTET                $result .= $matches[0] . "\r\n";
3022670d39fSLORTET            }
3032670d39fSLORTET        }
3042670d39fSLORTET        $result .= $this->getConf('message_prefix') . $this->getLang('hidden_message') . $this->getConf('message_suffix');
3052670d39fSLORTET
3062670d39fSLORTET        $event->result = $result;
3072670d39fSLORTET    }
3082670d39fSLORTET
309*ef5bb1f3SLORTET    public function hideMediaIfRestricted(Doku_Event $event, $param): void
3102670d39fSLORTET    {
311*ef5bb1f3SLORTET        $hideFilesMode = $this->getHideFilesMode();
3122670d39fSLORTET        if ($hideFilesMode === 'none') return;
3132670d39fSLORTET
3142670d39fSLORTET        $mediaID = (string)($event->data['media'] ?? '');
3152670d39fSLORTET        if ($mediaID === '') return;
316*ef5bb1f3SLORTET        if ($hideFilesMode === 'except_pageicons' && $this->isPagesIconMedia($mediaID)) return;
317*ef5bb1f3SLORTET        if (!$this->isMediaRestrictedFromExtranet($mediaID)) return;
3182670d39fSLORTET
3192670d39fSLORTET        $event->data['file'] = dirname(__FILE__) . '/images/restricted.png';
320*ef5bb1f3SLORTET        $event->data['orig'] = $event->data['file'];
321*ef5bb1f3SLORTET        $event->data['status'] = 200;
322*ef5bb1f3SLORTET        $event->data['statusmessage'] = 'OK';
3232670d39fSLORTET        $event->data['mime'] = 'image/png';
3242670d39fSLORTET        $event->data['download'] = false;
325*ef5bb1f3SLORTET        $event->data['cache'] = false;
326*ef5bb1f3SLORTET        $event->data['ispublic'] = false;
3272670d39fSLORTET    }
3282670d39fSLORTET
329*ef5bb1f3SLORTET    protected function getHideFilesMode(): string
3302670d39fSLORTET    {
3312670d39fSLORTET        $value = $this->getConf('hide_files');
3322670d39fSLORTET
3332670d39fSLORTET        if ($value === true || $value === 1 || $value === '1') return 'all';
3342670d39fSLORTET        if ($value === false || $value === 0 || $value === '0' || $value === '') return 'none';
3352670d39fSLORTET
3362670d39fSLORTET        $value = strtolower(trim((string)$value));
3372670d39fSLORTET        if (!in_array($value, ['all', 'except_pageicons', 'none'], true)) {
3382670d39fSLORTET            return 'none';
3392670d39fSLORTET        }
3402670d39fSLORTET
3412670d39fSLORTET        return $value;
3422670d39fSLORTET    }
3432670d39fSLORTET
344*ef5bb1f3SLORTET    protected function isPagesIconMedia(string $mediaID): bool
3452670d39fSLORTET    {
3462670d39fSLORTET        $mediaID = cleanID($mediaID);
3472670d39fSLORTET        if ($mediaID === '') return false;
3482670d39fSLORTET
3492670d39fSLORTET        /** @var helper_plugin_pagesicon|null $helper */
3502670d39fSLORTET        $helper = plugin_load('helper', 'pagesicon');
3512670d39fSLORTET        if (!$helper || !method_exists($helper, 'isPageIconMedia')) return false;
3522670d39fSLORTET
3532670d39fSLORTET        return (bool)$helper->isPageIconMedia($mediaID);
3542670d39fSLORTET    }
3552670d39fSLORTET}
356