Lexer->addSpecialPattern('\{\{scratchpad>.+?\}\}', $mode, 'plugin_luxtools_scratchpad'); } /** @inheritdoc */ public function handle($match, $state, $pos, Doku_Handler $handler) { $match = substr($match, strlen('{{scratchpad>'), -2); [$path,] = array_pad(explode('&', $match, 2), 2, ''); return [ 'path' => trim((string)$path), ]; } /** @inheritdoc */ public function render($format, Doku_Renderer $renderer, $data) { if ($data === false) return false; if (!is_array($data)) return false; if ($format !== 'xhtml' && $format !== 'odt') return false; // Always disable caching: the scratchpad is external to page revisions. $renderer->nocache(); $rawPad = (string)($data['path'] ?? ''); if ($rawPad === '') { $this->renderError($renderer, 'scratchpad_err_nopath'); return true; } $pathInfo = $this->getScratchpadPathInfoSafe($rawPad, $renderer); if ($pathInfo === false) return true; $filePath = (string)($pathInfo['path'] ?? ''); if ($filePath === '' || str_ends_with($filePath, '/')) { $this->renderError($renderer, 'scratchpad_err_badpath'); return true; } // Never allow writing/reading within DokuWiki-controlled paths if (Path::isWikiControlled(Path::cleanPath($filePath, false))) { $this->renderError($renderer, 'error_outsidejail'); return true; } $text = ''; $exists = @is_file($filePath); // If the scratchpad file is missing, render empty content. This allows // creating the file via the inline editor without showing an error. if ($exists) { if (!@is_readable($filePath)) { $this->renderError($renderer, 'scratchpad_err_unreadable'); return true; } $read = io_readFile($filePath, false); if ($read === false) { $this->renderError($renderer, 'scratchpad_err_unreadable'); return true; } $text = (string)$read; } if ($format === 'odt') { $renderer->cdata($text); return true; } /** @var Doku_Renderer_xhtml $renderer */ $endpoint = DOKU_BASE . 'lib/plugins/luxtools/scratchpad.php'; $sectok = ''; if (function_exists('getSecurityToken')) { $sectok = (string)getSecurityToken(); } global $ID; $pageId = (string)$ID; $canEdit = function_exists('auth_quickaclcheck') ? (auth_quickaclcheck($pageId) >= AUTH_EDIT) : false; $renderer->doc .= '
'; // Stable, template-friendly container around the scratchpad. $renderer->doc .= '
'; // Invisible container around the rendered scratchpad (templates can decorate). $renderer->doc .= '
'; // Well-defined place for the edit button (templates can reposition/style). $renderer->doc .= '
'; // Always show the scratchpad name (alias) for context. $renderer->doc .= '' . hsc($rawPad) . ''; if ($canEdit) { $label = (string)$this->getLang('scratchpad_edit'); if ($label === '') $label = 'Edit'; $renderer->doc .= '✎ edit'; } $renderer->doc .= '
'; $renderer->doc .= '
'; $renderer->doc .= $this->renderWikitextFragment($text); $renderer->doc .= '
'; if ($canEdit) { $renderer->doc .= ''; } $renderer->doc .= '
'; // .luxtools-scratchpad-rendered $renderer->doc .= '
'; // .luxtools-scratchpad-frame $renderer->doc .= '
'; return true; } protected function renderWikitextFragment(string $text): string { // Render wikitext to XHTML and return it as a string $info = ['cache' => false]; $instructions = p_get_instructions($text); return (string)p_render('xhtml', $instructions, $info); } protected function renderError(Doku_Renderer $renderer, string $langKey): void { $msg = (string)$this->getLang($langKey); if ($msg === '') $msg = $langKey; if ($renderer instanceof Doku_Renderer_xhtml) { $renderer->doc .= '
' . hsc($msg) . '
'; return; } $renderer->cdata('[n/a: ' . $msg . ']'); } /** * Resolve a scratchpad alias to its file path using the configured scratchpad_paths setting. * * @param string $pad * @param Doku_Renderer $renderer * @return array|false */ protected function getScratchpadPathInfoSafe(string $pad, Doku_Renderer $renderer) { try { $map = new ScratchpadMap((string)$this->getConf('scratchpad_paths')); return [ 'path' => $map->resolve($pad), ]; } catch (Exception $e) { $this->renderError($renderer, 'scratchpad_err_unknown'); return false; } } }