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