220 lines
7.5 KiB
PHP
220 lines
7.5 KiB
PHP
<?php
|
|
|
|
use dokuwiki\Extension\SyntaxPlugin;
|
|
use dokuwiki\plugin\luxtools\Path;
|
|
use dokuwiki\plugin\luxtools\ScratchpadMap;
|
|
|
|
require_once(__DIR__ . '/../autoload.php');
|
|
|
|
/**
|
|
* 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 = '';
|
|
$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 .= '<div class="luxtools-plugin luxtools-scratchpad"'
|
|
. ' data-luxtools-scratchpad="1"'
|
|
. ' data-endpoint="' . hsc($endpoint) . '"'
|
|
. ' data-pad="' . hsc($rawPad) . '"'
|
|
. ' data-pageid="' . hsc($pageId) . '"'
|
|
. ' data-sectok="' . hsc($sectok) . '"'
|
|
. '>';
|
|
|
|
// Stable, template-friendly container around the scratchpad.
|
|
$renderer->doc .= '<div class="luxtools-scratchpad-frame">';
|
|
|
|
// Invisible container around the rendered scratchpad (templates can decorate).
|
|
$renderer->doc .= '<div class="luxtools-scratchpad-rendered">';
|
|
|
|
// Well-defined place for the edit button (templates can reposition/style).
|
|
$renderer->doc .= '<div class="luxtools-scratchpad-bar">';
|
|
|
|
// Always show the scratchpad name (alias) for context.
|
|
$renderer->doc .= '<span class="luxtools-scratchpad-name">' . hsc($rawPad) . '</span>';
|
|
|
|
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) . '">✎ edit</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>'; // .luxtools-scratchpad-rendered
|
|
$renderer->doc .= '</div>'; // .luxtools-scratchpad-frame
|
|
|
|
$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;
|
|
}
|
|
}
|
|
}
|