Scratchpads V1

This commit is contained in:
2026-01-09 09:26:39 +01:00
parent 16a07701ee
commit 0948f50d76
15 changed files with 718 additions and 27 deletions

View File

@@ -1,11 +0,0 @@
name: DokuWiki Default Tasks
on:
push:
pull_request:
schedule:
- cron: '58 2 25 * *'
jobs:
all:
uses: dokuwiki/github-action/.github/workflows/all.yml@main

4
.github/workflows/instructions.md vendored Normal file
View File

@@ -0,0 +1,4 @@
# Development instructions
- Do not use `phpunit` There are missing dependencies that make it fail.
- Consider The official documentation for wirting dokuwiki plugins: https://www.dokuwiki.org/devel:plugins

15
README
View File

@@ -12,6 +12,21 @@ will not work!
Syntax:
{{files>...}}
Scratchpad (shared, file-backed, no page revisions):
{{scratchpad>...}}
The scratchpad is resolved via the "scratchpad_paths" setting.
Each scratchpad entry is a full filesystem path to a file (including extension),
followed by an alias line that defines the pad name used in the wiki.
Example:
/var/lib/dokuwiki-scratchpads/startpad.txt
A> start
Then use:
{{scratchpad>start}}
Please refer to http://www.dokuwiki.org/extensions for additional info
on how to install extensions in DokuWiki.

96
ScratchpadMap.php Normal file
View File

@@ -0,0 +1,96 @@
<?php
namespace dokuwiki\plugin\luxtools;
/**
* Maps scratchpad aliases (used in wiki pages) to full filesystem file paths.
*
* Config format (one per line):
* /full/path/to/pad-file.txt
* A> padname
*
* The next A> line assigns an alias for the previously listed file.
*/
class ScratchpadMap
{
/** @var array<string, array{alias:string, path:string}> */
protected $map = [];
/**
* @param string $config
*/
public function __construct($config)
{
$this->map = $this->parseConfig((string)$config);
}
/**
* Resolve the given alias to a full file path.
*
* @param string $alias
* @return string
* @throws \Exception
*/
public function resolve($alias)
{
$alias = trim((string)$alias);
if ($alias === '') throw new \Exception('Empty alias');
if (!isset($this->map[$alias])) {
throw new \Exception('Unknown scratchpad alias');
}
return (string)$this->map[$alias]['path'];
}
/**
* Return the parsed mapping.
*
* @return array<string, array{alias:string, path:string}>
*/
public function getMap()
{
return $this->map;
}
/**
* @param string $config
* @return array<string, array{alias:string, path:string}>
*/
protected function parseConfig($config)
{
$map = [];
$lines = explode("\n", (string)$config);
$lastFile = '';
foreach ($lines as $line) {
$line = trim($line);
if ($line === '') continue;
if (str_starts_with($line, 'A>')) {
$alias = trim(substr($line, 2));
if ($alias === '' || $lastFile === '') continue;
$map[$alias] = [
'alias' => $alias,
'path' => $lastFile,
];
continue;
}
// Ignore W> lines for compatibility with the Path config style
if (str_starts_with($line, 'W>')) {
continue;
}
// Treat as file path (no trailing slash enforced)
$filePath = Path::cleanPath($line, false);
if ($filePath === '' || str_ends_with($filePath, '/')) {
// Ignore invalid entries; they will not be resolvable
$lastFile = '';
continue;
}
$lastFile = $filePath;
}
return $map;
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace dokuwiki\plugin\luxtools\test;
use dokuwiki\plugin\luxtools\ScratchpadMap;
use DokuWikiTest;
/**
* ScratchpadMap tests for the luxtools plugin
*
* @group plugin_luxtools
* @group plugins
*/
class ScratchpadMapTest extends DokuWikiTest
{
public function testResolveAndParsing()
{
$map = new ScratchpadMap(
<<<EOT
/var/scratchpads/startpad.txt
A> start
C:\\pads\\notes.md
A> notes
W> ignored
\\\\server\\share\\pads\\team.txt
A> team
EOT
);
$this->assertEquals('/var/scratchpads/startpad.txt', $map->resolve('start'));
$this->assertEquals('C:/pads/notes.md', $map->resolve('notes'));
$this->assertEquals('\\\\server/share/pads/team.txt', $map->resolve('team'));
}
public function testUnknownAliasThrows()
{
$map = new ScratchpadMap("/tmp/pad.txt\nA> pad\n");
$this->expectExceptionMessageMatches('/Unknown scratchpad alias/');
$map->resolve('nope');
}
}

View File

@@ -11,6 +11,7 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin
/** @var string[] */
protected $configKeys = [
'paths',
'scratchpad_paths',
'allow_in_comments',
'defaults',
'extensions',
@@ -51,6 +52,11 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin
$paths = $INPUT->str('paths');
$paths = str_replace(["\r\n", "\r"], "\n", $paths);
$newConf['paths'] = $paths;
$scratchpadPaths = $INPUT->str('scratchpad_paths');
$scratchpadPaths = str_replace(["\r\n", "\r"], "\n", $scratchpadPaths);
$newConf['scratchpad_paths'] = $scratchpadPaths;
$newConf['allow_in_comments'] = (int)$INPUT->bool('allow_in_comments');
$newConf['defaults'] = $INPUT->str('defaults');
$newConf['extensions'] = $INPUT->str('extensions');
@@ -88,6 +94,12 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin
echo '<textarea name="paths" rows="8" cols="80" class="edit">' . hsc($paths) . '</textarea>';
echo '</label><br />';
// scratchpad_paths: multiline textarea
$scratchpadPaths = (string)$this->getConf('scratchpad_paths');
echo '<label class="block"><span>' . hsc($this->getLang('scratchpad_paths')) . '</span><br />';
echo '<textarea name="scratchpad_paths" rows="6" cols="80" class="edit">' . hsc($scratchpadPaths) . '</textarea>';
echo '</label><br />';
// allow_in_comments
$checked = $this->getConf('allow_in_comments') ? ' checked="checked"' : '';
echo '<label class="block"><span>' . hsc($this->getLang('allow_in_comments')) . '</span> ';
@@ -128,7 +140,11 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin
}
/**
* Persist plugin settings to conf/plugins/luxtools.local.php.
* Persist plugin settings to conf/local.php.
*
* DokuWiki loads conf/local.php on each request; values written there will
* be available via getConf(). We write into a dedicated BEGIN/END block so
* updates are idempotent.
*
* @param array $newConf
* @return bool
@@ -138,30 +154,62 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin
if (!defined('DOKU_CONF')) return false;
$plugin = 'luxtools';
$confDir = DOKU_CONF . 'plugins/';
$file = $confDir . $plugin . '.local.php';
$file = DOKU_CONF . 'local.php';
if (function_exists('io_mkdir_p')) {
io_mkdir_p($confDir);
} elseif (!@is_dir($confDir)) {
@mkdir($confDir, 0777, true);
$existing = '';
if (@is_file($file) && @is_readable($file)) {
$existing = (string)file_get_contents($file);
}
if ($existing === '') {
$existing = "<?php\n";
}
if (!str_starts_with($existing, "<?php")) {
// unexpected format - do not overwrite
return false;
}
// Only write known keys; ignore extras.
$lines = ["<?php"];
$begin = "// BEGIN LUXTOOLS\n";
$end = "// END LUXTOOLS\n";
// Build the block
$lines = [$begin];
foreach ($this->configKeys as $key) {
if (!array_key_exists($key, $newConf)) continue;
$value = $newConf[$key];
$lines[] = '$conf[' . var_export($key, true) . '] = ' . $this->exportPhpValue($value, $key) . ';';
$lines[] = '$conf[\'plugin\'][\'' . $plugin . '\'][' . var_export($key, true) . '] = ' . $this->exportPhpValue($value, $key) . ';';
}
$lines[] = '';
$content = implode("\n", $lines);
$lines[] = $end;
$block = implode("\n", $lines);
// Replace or append the block in conf/local.php
$beginPos = strpos($existing, $begin);
if ($beginPos !== false) {
$endPos = strpos($existing, $end, $beginPos);
if ($endPos === false) {
// malformed existing block - append a new one
$content = rtrim($existing) . "\n\n" . $block;
} else {
$endPos += strlen($end);
$content = substr($existing, 0, $beginPos) . $block . substr($existing, $endPos);
}
} else {
$content = rtrim($existing) . "\n\n" . $block;
}
$ok = false;
if (function_exists('io_saveFile')) {
return (bool)io_saveFile($file, $content);
$ok = (bool)io_saveFile($file, $content);
} else {
$ok = @file_put_contents($file, $content, LOCK_EX) !== false;
}
return @file_put_contents($file, $content, LOCK_EX) !== false;
// Best-effort cleanup: stop creating/using legacy conf/plugins/luxtools.local.php
$legacy = DOKU_CONF . 'plugins/' . $plugin . '.local.php';
if (@is_file($legacy)) {
@unlink($legacy);
}
return $ok;
}
/**

View File

@@ -5,6 +5,7 @@
*/
$conf['paths'] = '';
$conf['scratchpad_paths'] = '';
$conf['allow_in_comments'] = 0;
$conf['defaults'] = '';
$conf['extensions'] = '';

View File

@@ -22,7 +22,7 @@ $lang['settings'] = 'luxtools-Einstellungen';
$lang['legend'] = 'Einstellungen';
$lang['btn_save'] = 'Speichern';
$lang['saved'] = 'Einstellungen gespeichert.';
$lang['err_save'] = 'Einstellungen konnten nicht gespeichert werden. Bitte Schreibrechte für conf/plugins/ prüfen.';
$lang['err_save'] = 'Einstellungen konnten nicht gespeichert werden. Bitte Schreibrechte für conf/local.php prüfen.';
$lang['err_security'] = 'Sicherheits-Token ungültig. Bitte erneut versuchen.';
$lang['paths'] = 'Erlaubte Basis-Pfade (eine pro Zeile oder komma-separiert).';

View File

@@ -22,13 +22,21 @@ $lang['settings'] = 'luxtools settings';
$lang['legend'] = 'Settings';
$lang['btn_save'] = 'Save';
$lang['saved'] = 'Settings saved.';
$lang['err_save'] = 'Could not save settings. Please check write permissions for conf/plugins/.';
$lang['err_save'] = 'Could not save settings. Please check write permissions for conf/local.php.';
$lang['err_security'] = 'Security token mismatch. Please retry.';
$lang['paths'] = 'Allowed base paths (one per line or comma-separated).';
$lang['scratchpad_paths'] = 'Scratchpad files (one per line). Each file path must include the extension. Use a following A> line to set the pad name used in the wiki.';
$lang['allow_in_comments'] = 'Whether to allow the files syntax to be used in comments.';
$lang['defaults'] = 'Default options. Use the same syntax as in inline configuration.';
$lang['extensions'] = 'Comma-separated list of allowed file extensions to list.';
$lang['thumb_placeholder'] = 'MediaManager ID for the gallery thumbnail placeholder.';
$lang['gallery_thumb_scale'] = 'Gallery thumbnail scale factor. Use 2 for sharper thumbnails on HiDPI screens (still displayed as 150×150).';
$lang['open_service_url'] = 'Local client service URL for the {{open>...}} button (e.g. http://127.0.0.1:8765).';
$lang['scratchpad_edit'] = 'Edit scratchpad';
$lang['scratchpad_save'] = 'Save';
$lang['scratchpad_cancel'] = 'Cancel';
$lang['scratchpad_err_nopath'] = 'Scratchpad path missing';
$lang['scratchpad_err_badpath'] = 'Invalid scratchpad path';
$lang['scratchpad_err_unknown'] = 'Unknown scratchpad pad name';

View File

@@ -1,6 +1,7 @@
<?php
$lang['paths'] = 'Allowed base paths (one per line or comma-separated).';
$lang['scratchpad_paths'] = 'Scratchpad files (one per line). Each file path must include the extension; use a following A> line to set the pad name used in the wiki.';
$lang['allow_in_comments'] = 'Whether to allow the files syntax to be used in comments.';
$lang['defaults'] = 'Default options. Use the same syntax as in inline configuration';
$lang['extensions'] = 'Comma-separated list of allowed file extensions to list';

117
scratchpad.php Normal file
View File

@@ -0,0 +1,117 @@
<?php
// phpcs:disable PSR1.Files.SideEffects.FoundWithSymbols
use dokuwiki\plugin\luxtools\Path;
use dokuwiki\plugin\luxtools\ScratchpadMap;
if (!defined('DOKU_INC')) define('DOKU_INC', __DIR__ . '/../../../');
require_once(DOKU_INC . 'inc/init.php');
global $INPUT;
$syntax = plugin_load('syntax', 'luxtools');
if (!$syntax) {
http_status(500);
header('Content-Type: application/json');
echo json_encode(['ok' => false, 'error' => 'plugin disabled']);
exit;
}
/**
* Send a JSON response.
*
* @param int $status
* @param array $payload
* @return void
*/
function luxtools_scratchpad_json(int $status, array $payload): void
{
http_status($status);
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Pragma: no-cache');
echo json_encode($payload);
exit;
}
$cmd = (string)$INPUT->str('cmd');
$pad = (string)$INPUT->str('pad');
$pageId = (string)$INPUT->str('id');
if (function_exists('cleanID')) {
$pageId = cleanID($pageId);
}
if ($cmd === '' || $pad === '' || $pageId === '') {
luxtools_scratchpad_json(400, ['ok' => false, 'error' => 'missing parameters']);
}
// Require the user to at least be able to read the host page.
if (function_exists('auth_quickaclcheck') && auth_quickaclcheck($pageId) < AUTH_READ) {
luxtools_scratchpad_json(403, ['ok' => false, 'error' => 'forbidden']);
}
$map = new ScratchpadMap((string)$syntax->getConf('scratchpad_paths'));
try {
$resolved = (string)$map->resolve($pad);
if ($resolved === '' || str_ends_with($resolved, '/')) {
throw new Exception('Invalid scratchpad path');
}
// Never allow writing/reading within DokuWiki-controlled paths.
if (Path::isWikiControlled(Path::cleanPath($resolved, false))) {
throw new Exception('Access to wiki files is not allowed');
}
} catch (Exception $e) {
luxtools_scratchpad_json(403, ['ok' => false, 'error' => 'access denied']);
}
if ($cmd === 'load') {
$text = '';
if (@is_file($resolved) && @is_readable($resolved)) {
$text = (string)io_readFile($resolved, false);
}
luxtools_scratchpad_json(200, ['ok' => true, 'text' => $text]);
}
if ($cmd === 'save') {
if (strtoupper($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
luxtools_scratchpad_json(405, ['ok' => false, 'error' => 'method not allowed']);
}
// Require edit permission on the host page.
if (function_exists('auth_quickaclcheck') && auth_quickaclcheck($pageId) < AUTH_EDIT) {
luxtools_scratchpad_json(403, ['ok' => false, 'error' => 'forbidden']);
}
if (!checkSecurityToken()) {
luxtools_scratchpad_json(403, ['ok' => false, 'error' => 'bad token']);
}
$text = (string)$INPUT->str('text');
// Ensure directory exists
$dir = dirname($resolved);
if (function_exists('io_mkdir_p')) {
io_mkdir_p($dir);
} elseif (!@is_dir($dir)) {
@mkdir($dir, 0777, true);
}
$ok = false;
if (function_exists('io_saveFile')) {
$ok = (bool)io_saveFile($resolved, $text);
} else {
$ok = @file_put_contents($resolved, $text, LOCK_EX) !== false;
}
if (!$ok) {
luxtools_scratchpad_json(500, ['ok' => false, 'error' => 'save failed']);
}
luxtools_scratchpad_json(200, ['ok' => true]);
}
luxtools_scratchpad_json(400, ['ok' => false, 'error' => 'unknown command']);

137
script.js
View File

@@ -284,6 +284,142 @@
return path;
}
function initScratchpads() {
var pads = document.querySelectorAll('div.luxtools-scratchpad[data-luxtools-scratchpad="1"]');
if (!pads || !pads.length) return;
function setStatus(root, msg) {
var el = root.querySelector('.luxtools-scratchpad-status');
if (!el) return;
el.textContent = msg || '';
}
function getSectok() {
try {
if (window.JSINFO && window.JSINFO.sectok) return String(window.JSINFO.sectok);
} catch (e) {}
return '';
}
function loadPad(root) {
var endpoint = (root.getAttribute('data-endpoint') || '').trim();
var pad = (root.getAttribute('data-pad') || '').trim();
var pageId = (root.getAttribute('data-pageid') || '').trim();
if (!endpoint || !pad || !pageId) return Promise.reject(new Error('missing params'));
var url = endpoint + '?cmd=load&pad=' + encodeURIComponent(pad) + '&id=' + encodeURIComponent(pageId);
return window.fetch(url, {
method: 'GET',
credentials: 'same-origin'
}).then(function (res) {
return res.json().catch(function () { return null; }).then(function (body) {
if (!res.ok || !body || body.ok !== true) {
var msg = (body && body.error) ? body.error : ('HTTP ' + res.status);
throw new Error(msg);
}
return body.text || '';
});
});
}
function savePad(root, text) {
var endpoint = (root.getAttribute('data-endpoint') || '').trim();
var pad = (root.getAttribute('data-pad') || '').trim();
var pageId = (root.getAttribute('data-pageid') || '').trim();
if (!endpoint || !pad || !pageId) return Promise.reject(new Error('missing params'));
var params = new window.URLSearchParams();
params.set('cmd', 'save');
params.set('pad', pad);
params.set('id', pageId);
params.set('text', text || '');
params.set('sectok', getSectok());
return window.fetch(endpoint, {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: params.toString()
}).then(function (res) {
return res.json().catch(function () { return null; }).then(function (body) {
if (!res.ok || !body || body.ok !== true) {
var msg = (body && body.error) ? body.error : ('HTTP ' + res.status);
throw new Error(msg);
}
return true;
});
});
}
function openEditor(root) {
var editor = root.querySelector('.luxtools-scratchpad-editor');
var textarea = root.querySelector('textarea.luxtools-scratchpad-text');
if (!editor || !textarea) return;
editor.hidden = false;
setStatus(root, 'Loading…');
textarea.disabled = true;
loadPad(root).then(function (text) {
textarea.value = text;
textarea.disabled = false;
setStatus(root, '');
textarea.focus();
}).catch(function (e) {
textarea.disabled = false;
setStatus(root, 'Load failed: ' + (e && e.message ? e.message : 'error'));
});
}
function closeEditor(root) {
var editor = root.querySelector('.luxtools-scratchpad-editor');
if (!editor) return;
editor.hidden = true;
setStatus(root, '');
}
document.addEventListener('click', function (e) {
var t = e.target;
if (!t) return;
var edit = t.closest ? t.closest('a.luxtools-scratchpad-edit') : null;
if (edit) {
var root = edit.closest('div.luxtools-scratchpad');
if (!root) return;
e.preventDefault();
openEditor(root);
return;
}
var save = t.closest ? t.closest('button.luxtools-scratchpad-save') : null;
if (save) {
var rootS = save.closest('div.luxtools-scratchpad');
if (!rootS) return;
e.preventDefault();
var textareaS = rootS.querySelector('textarea.luxtools-scratchpad-text');
if (!textareaS) return;
textareaS.disabled = true;
setStatus(rootS, 'Saving…');
savePad(rootS, textareaS.value).then(function () {
setStatus(rootS, 'Saved. Reloading…');
try { window.location.reload(); } catch (err) {}
}).catch(function (err) {
textareaS.disabled = false;
setStatus(rootS, 'Save failed: ' + (err && err.message ? err.message : 'error'));
});
return;
}
var cancel = t.closest ? t.closest('button.luxtools-scratchpad-cancel') : null;
if (cancel) {
var rootC = cancel.closest('div.luxtools-scratchpad');
if (!rootC) return;
e.preventDefault();
closeEditor(rootC);
}
}, true);
}
function findOpenElement(target) {
var el = target;
while (el && el !== document) {
@@ -350,4 +486,5 @@
document.addEventListener('click', onClick, false);
document.addEventListener('DOMContentLoaded', initGalleryThumbs, false);
document.addEventListener('DOMContentLoaded', initScratchpads, false);
})();

View File

@@ -96,6 +96,54 @@ div.luxtools-gallery .luxtools-gallery-caption {
pointer-events: none;
}
/* Scratchpad */
div.luxtools-scratchpad {
position: relative;
}
div.luxtools-scratchpad .luxtools-scratchpad-bar {
display: flex;
justify-content: flex-end;
margin-bottom: 0.2em;
}
div.luxtools-scratchpad a.luxtools-scratchpad-edit {
text-decoration: none;
opacity: 0.7;
padding: 0 0.2em;
}
div.luxtools-scratchpad a.luxtools-scratchpad-edit:hover,
div.luxtools-scratchpad a.luxtools-scratchpad-edit:focus {
opacity: 1;
}
div.luxtools-scratchpad .luxtools-scratchpad-editor {
margin-top: 0.4em;
}
div.luxtools-scratchpad textarea.luxtools-scratchpad-text {
width: 100%;
box-sizing: border-box;
min-height: 8em;
}
div.luxtools-scratchpad .luxtools-scratchpad-actions {
display: flex;
align-items: center;
gap: 0.4em;
margin-top: 0.35em;
}
div.luxtools-scratchpad .luxtools-scratchpad-status {
opacity: 0.8;
font-size: 90%;
}
div.luxtools-scratchpad .luxtools-scratchpad-error {
opacity: 0.8;
font-style: italic;
}
/* Disable background scrolling while the lightbox is open. */
html.luxtools-noscroll,
html.luxtools-noscroll body {

View File

@@ -2,6 +2,7 @@
require_once(__DIR__ . '/syntax/AbstractSyntax.php');
require_once(__DIR__ . '/syntax/files.php');
require_once(__DIR__ . '/syntax/scratchpad.php');
/**
* luxtools plugin bootstrap.

183
syntax/scratchpad.php Normal file
View File

@@ -0,0 +1,183 @@
<?php
use dokuwiki\Extension\SyntaxPlugin;
use dokuwiki\plugin\luxtools\Path;
use dokuwiki\plugin\luxtools\ScratchpadMap;
/**
* luxtools Plugin: Scratchpad syntax.
*
* Renders the contents of a configured file as wikitext and provides a minimal
* inline editor that saves directly to that file (no page revisions).
*/
class syntax_plugin_luxtools_scratchpad extends SyntaxPlugin
{
/** @inheritdoc */
public function getType()
{
return 'substition';
}
/** @inheritdoc */
public function getPType()
{
return 'block';
}
/** @inheritdoc */
public function getSort()
{
// After most formatting, similar to other luxtools syntaxes
return 223;
}
/** @inheritdoc */
public function connectTo($mode)
{
$this->Lexer->addSpecialPattern('\{\{scratchpad>.+?\}\}', $mode, 'plugin_luxtools_scratchpad');
}
/** @inheritdoc */
public function handle($match, $state, $pos, Doku_Handler $handler)
{
global $INPUT;
// Do not allow the syntax in discussion plugin comments
if (!$this->getConf('allow_in_comments') && $INPUT->has('comment')) {
return false;
}
$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 = '';
if (@is_file($filePath) && @is_readable($filePath)) {
$text = (string)io_readFile($filePath, false);
}
if ($format === 'odt') {
$renderer->cdata($text);
return true;
}
/** @var Doku_Renderer_xhtml $renderer */
$endpoint = DOKU_BASE . 'lib/plugins/luxtools/scratchpad.php';
global $ID;
$pageId = (string)$ID;
$canEdit = function_exists('auth_quickaclcheck') ? (auth_quickaclcheck($pageId) >= AUTH_EDIT) : false;
$renderer->doc .= '<div class="luxtools-plugin luxtools-scratchpad"'
. ' data-luxtools-scratchpad="1"'
. ' data-endpoint="' . hsc($endpoint) . '"'
. ' data-pad="' . hsc($rawPad) . '"'
. ' data-pageid="' . hsc($pageId) . '"'
. '>';
$renderer->doc .= '<div class="luxtools-scratchpad-bar">';
if ($canEdit) {
$label = (string)$this->getLang('scratchpad_edit');
if ($label === '') $label = 'Edit';
$renderer->doc .= '<a href="#" class="luxtools-scratchpad-edit" title="' . hsc($label) . '" aria-label="' . hsc($label) . '">✎</a>';
}
$renderer->doc .= '</div>';
$renderer->doc .= '<div class="luxtools-scratchpad-view">';
$renderer->doc .= $this->renderWikitextFragment($text);
$renderer->doc .= '</div>';
if ($canEdit) {
$renderer->doc .= '<div class="luxtools-scratchpad-editor" hidden>';
$renderer->doc .= '<textarea class="luxtools-scratchpad-text" rows="10" spellcheck="true"></textarea>';
$renderer->doc .= '<div class="luxtools-scratchpad-actions">'
. '<button type="button" class="button luxtools-scratchpad-save">' . hsc((string)$this->getLang('scratchpad_save') ?: 'Save') . '</button>'
. '<button type="button" class="button luxtools-scratchpad-cancel">' . hsc((string)$this->getLang('scratchpad_cancel') ?: 'Cancel') . '</button>'
. '<span class="luxtools-scratchpad-status" aria-live="polite"></span>'
. '</div>';
$renderer->doc .= '</div>';
}
$renderer->doc .= '</div>';
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 .= '<div class="luxtools-plugin luxtools-scratchpad"><div class="luxtools-scratchpad-error">'
. hsc($msg)
. '</div></div>';
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;
}
}
}