Files
luxtools-plugin/admin/main.php
2026-03-11 13:15:20 +01:00

499 lines
22 KiB
PHP

<?php
/**
* luxtools: Admin settings page
*/
// must be run within Dokuwiki
if (!defined('DOKU_INC')) die();
class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin
{
/** @var string[] Calendar slot keys */
protected $calendarSlotKeys = ['general', 'maintenance', 'slot3', 'slot4'];
/** @var string[] */
protected $configKeys = [
'paths',
'scratchpad_paths',
'extensions',
'default_sort',
'default_order',
'default_tableheader',
'default_foldersfirst',
'default_recursive',
'default_titlefile',
'default_cache',
'default_randlinks',
'default_showsize',
'default_showdate',
'default_tablecolumns',
'default_maxheight',
'thumb_placeholder',
'gallery_thumb_scale',
'open_service_url',
'image_base_path',
'calendar_general_file',
'calendar_general_caldav_url',
'calendar_general_username',
'calendar_general_password',
'calendar_general_color',
'calendar_general_display',
'calendar_maintenance_file',
'calendar_maintenance_caldav_url',
'calendar_maintenance_username',
'calendar_maintenance_password',
'calendar_maintenance_color',
'calendar_maintenance_display',
'calendar_slot3_file',
'calendar_slot3_caldav_url',
'calendar_slot3_username',
'calendar_slot3_password',
'calendar_slot3_color',
'calendar_slot3_display',
'calendar_slot4_file',
'calendar_slot4_caldav_url',
'calendar_slot4_username',
'calendar_slot4_password',
'calendar_slot4_color',
'calendar_slot4_display',
'pagelink_search_depth',
];
public function getMenuText($language)
{
return $this->getLang('menu');
}
public function getMenuSort()
{
// keep near other plugin tools
return 1011;
}
public function forAdminOnly()
{
return true;
}
public function handle()
{
global $INPUT;
if ($INPUT->str('luxtools_cmd') !== 'save') return;
if (!checkSecurityToken()) {
msg($this->getLang('err_security'), -1);
return;
}
$newConf = [];
// Normalize newlines to "\n" for consistent parsing
$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['extensions'] = $INPUT->str('extensions');
$newConf['default_sort'] = $INPUT->str('default_sort');
$newConf['default_order'] = $INPUT->str('default_order');
$newConf['default_tableheader'] = (int)$INPUT->bool('default_tableheader');
$newConf['default_foldersfirst'] = (int)$INPUT->bool('default_foldersfirst');
$newConf['default_recursive'] = (int)$INPUT->bool('default_recursive');
$newConf['default_titlefile'] = $INPUT->str('default_titlefile');
$newConf['default_cache'] = (int)$INPUT->bool('default_cache');
$newConf['default_randlinks'] = (int)$INPUT->bool('default_randlinks');
$newConf['default_showsize'] = (int)$INPUT->bool('default_showsize');
$newConf['default_showdate'] = (int)$INPUT->bool('default_showdate');
$newConf['default_tablecolumns'] = $INPUT->str('default_tablecolumns');
$newConf['default_maxheight'] = $INPUT->str('default_maxheight');
$newConf['thumb_placeholder'] = $INPUT->str('thumb_placeholder');
$newConf['gallery_thumb_scale'] = $INPUT->str('gallery_thumb_scale');
$newConf['open_service_url'] = $INPUT->str('open_service_url');
$newConf['image_base_path'] = $INPUT->str('image_base_path');
// Calendar slot settings
foreach ($this->calendarSlotKeys as $slot) {
$newConf['calendar_' . $slot . '_file'] = trim($INPUT->str('calendar_' . $slot . '_file'));
$newConf['calendar_' . $slot . '_caldav_url'] = trim($INPUT->str('calendar_' . $slot . '_caldav_url'));
$newConf['calendar_' . $slot . '_username'] = trim($INPUT->str('calendar_' . $slot . '_username'));
$newConf['calendar_' . $slot . '_password'] = trim($INPUT->str('calendar_' . $slot . '_password'));
$newConf['calendar_' . $slot . '_color'] = trim($INPUT->str('calendar_' . $slot . '_color'));
$newConf['calendar_' . $slot . '_display'] = trim($INPUT->str('calendar_' . $slot . '_display'));
}
$depth = (int)$INPUT->int('pagelink_search_depth');
if ($depth < 0) $depth = 0;
$newConf['pagelink_search_depth'] = $depth;
if ($this->savePluginLocalConf($newConf)) {
msg($this->getLang('saved'), 1);
} else {
msg($this->getLang('err_save'), -1);
}
}
public function html()
{
global $ID;
echo '<div class="plugin_luxtools_admin">';
echo '<h1>' . hsc($this->getLang('settings')) . '</h1>';
echo '<form action="' . hsc(wl($ID)) . '" method="post" class="plugin_luxtools_admin_form">';
echo '<input type="hidden" name="do" value="admin" />';
echo '<input type="hidden" name="page" value="luxtools_main" />';
echo '<input type="hidden" name="id" value="' . hsc($ID) . '" />';
echo '<input type="hidden" name="luxtools_cmd" value="save" />';
echo formSecurityToken();
echo '<fieldset>';
echo '<legend>' . hsc($this->getLang('legend')) . '</legend>';
// paths: multiline textarea
$paths = $this->normalizeMultilineDisplay((string)$this->getConf('paths'), 'paths');
echo '<label class="block"><span>' . hsc($this->getLang('paths')) . '</span><br />';
echo '<textarea name="paths" rows="8" cols="80" class="edit">' . hsc($paths) . '</textarea>';
echo '</label><br />';
// scratchpad_paths: multiline textarea
$scratchpadPaths = $this->normalizeMultilineDisplay((string)$this->getConf('scratchpad_paths'), '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 />';
// extensions
echo '<label class="block"><span>' . hsc($this->getLang('extensions')) . '</span> ';
echo '<input type="text" class="edit" name="extensions" value="' . hsc((string)$this->getConf('extensions')) . '" />';
echo '</label><br />';
echo '<h2>' . hsc($this->getLang('listing_defaults')) . '</h2>';
// default_sort
$defaultSort = (string)$this->getConf('default_sort');
echo '<label class="block"><span>' . hsc($this->getLang('default_sort')) . '</span>';
echo '<select name="default_sort" class="edit">';
foreach (['name', 'iname', 'ctime', 'mtime', 'size'] as $opt) {
$sel = ($defaultSort === $opt) ? ' selected="selected"' : '';
echo '<option value="' . hsc($opt) . '"' . $sel . '>' . hsc($opt) . '</option>';
}
echo '</select>';
echo '</label><br />';
// default_order
$defaultOrder = (string)$this->getConf('default_order');
echo '<label class="block"><span>' . hsc($this->getLang('default_order')) . '</span>';
echo '<select name="default_order" class="edit">';
foreach (['asc', 'desc'] as $opt) {
$sel = ($defaultOrder === $opt) ? ' selected="selected"' : '';
echo '<option value="' . hsc($opt) . '"' . $sel . '>' . hsc($opt) . '</option>';
}
echo '</select>';
echo '</label><br />';
// default_tableheader
$checked = $this->getConf('default_tableheader') ? ' checked="checked"' : '';
echo '<label class="block"><span>' . hsc($this->getLang('default_tableheader')) . '</span> ';
echo '<input type="checkbox" name="default_tableheader" value="1"' . $checked . ' />';
echo '</label><br />';
// default_foldersfirst
$checked = $this->getConf('default_foldersfirst') ? ' checked="checked"' : '';
echo '<label class="block"><span>' . hsc($this->getLang('default_foldersfirst')) . '</span> ';
echo '<input type="checkbox" name="default_foldersfirst" value="1"' . $checked . ' />';
echo '</label><br />';
// default_recursive
$checked = $this->getConf('default_recursive') ? ' checked="checked"' : '';
echo '<label class="block"><span>' . hsc($this->getLang('default_recursive')) . '</span> ';
echo '<input type="checkbox" name="default_recursive" value="1"' . $checked . ' />';
echo '</label><br />';
// default_titlefile
echo '<label class="block"><span>' . hsc($this->getLang('default_titlefile')) . '</span>';
echo '<input type="text" class="edit" name="default_titlefile" value="' . hsc((string)$this->getConf('default_titlefile')) . '" />';
echo '</label><br />';
// default_cache
$checked = $this->getConf('default_cache') ? ' checked="checked"' : '';
echo '<label class="block"><span>' . hsc($this->getLang('default_cache')) . '</span> ';
echo '<input type="checkbox" name="default_cache" value="1"' . $checked . ' />';
echo '</label><br />';
// default_randlinks
$checked = $this->getConf('default_randlinks') ? ' checked="checked"' : '';
echo '<label class="block"><span>' . hsc($this->getLang('default_randlinks')) . '</span> ';
echo '<input type="checkbox" name="default_randlinks" value="1"' . $checked . ' />';
echo '</label><br />';
// default_showsize
$checked = $this->getConf('default_showsize') ? ' checked="checked"' : '';
echo '<label class="block"><span>' . hsc($this->getLang('default_showsize')) . '</span> ';
echo '<input type="checkbox" name="default_showsize" value="1"' . $checked . ' />';
echo '</label><br />';
// default_showdate
$checked = $this->getConf('default_showdate') ? ' checked="checked"' : '';
echo '<label class="block"><span>' . hsc($this->getLang('default_showdate')) . '</span> ';
echo '<input type="checkbox" name="default_showdate" value="1"' . $checked . ' />';
echo '</label><br />';
// default_tablecolumns
echo '<label class="block"><span>' . hsc($this->getLang('default_tablecolumns')) . '</span>';
echo '<input type="text" class="edit" name="default_tablecolumns" value="' . hsc((string)$this->getConf('default_tablecolumns')) . '" />';
echo '</label><br />';
// default_maxheight
echo '<label class="block"><span>' . hsc($this->getLang('default_maxheight')) . '</span>';
echo '<input type="number" class="edit" name="default_maxheight" value="' . hsc((string)$this->getConf('default_maxheight')) . '" />';
echo '</label><br />';
// thumb_placeholder
echo '<label class="block"><span>' . hsc($this->getLang('thumb_placeholder')) . '</span> ';
echo '<input type="text" class="edit" name="thumb_placeholder" value="' . hsc((string)$this->getConf('thumb_placeholder')) . '" />';
echo '</label><br />';
// gallery_thumb_scale
echo '<label class="block"><span>' . hsc($this->getLang('gallery_thumb_scale')) . '</span> ';
echo '<input type="text" class="edit" name="gallery_thumb_scale" value="' . hsc((string)$this->getConf('gallery_thumb_scale')) . '" />';
echo '</label><br />';
// open_service_url
echo '<label class="block"><span>' . hsc($this->getLang('open_service_url')) . '</span> ';
echo '<input type="text" class="edit" name="open_service_url" value="' . hsc((string)$this->getConf('open_service_url')) . '" />';
echo '</label><br />';
// image_base_path
echo '<label class="block"><span>' . hsc($this->getLang('image_base_path')) . '</span> ';
echo '<input type="text" class="edit" name="image_base_path" value="' . hsc((string)$this->getConf('image_base_path')) . '" />';
echo '</label><br />';
// Calendar slot settings
$slotLabels = [
'general' => 'General',
'maintenance' => 'Maintenance',
'slot3' => 'Slot 3',
'slot4' => 'Slot 4',
];
$displayOptions = [
'none' => (string)$this->getLang('calendar_slot_display_none'),
'top-left' => (string)$this->getLang('calendar_slot_display_top_left'),
'top-right' => (string)$this->getLang('calendar_slot_display_top_right'),
'bottom-left' => (string)$this->getLang('calendar_slot_display_bottom_left'),
'bottom-right' => (string)$this->getLang('calendar_slot_display_bottom_right'),
];
foreach ($this->calendarSlotKeys as $slot) {
echo '<h2>' . hsc($this->getLang('calendar_slot_heading') . ': ' . $slotLabels[$slot]) . '</h2>';
$prefix = 'calendar_' . $slot . '_';
echo '<label class="block"><span>' . hsc($this->getLang('calendar_slot_file')) . '</span> ';
echo '<input type="text" class="edit" name="' . hsc($prefix . 'file') . '" value="' . hsc((string)$this->getConf($prefix . 'file')) . '" />';
echo '</label><br />';
echo '<label class="block"><span>' . hsc($this->getLang('calendar_slot_caldav_url')) . '</span> ';
echo '<input type="text" class="edit" name="' . hsc($prefix . 'caldav_url') . '" value="' . hsc((string)$this->getConf($prefix . 'caldav_url')) . '" />';
echo '</label><br />';
echo '<label class="block"><span>' . hsc($this->getLang('calendar_slot_username')) . '</span> ';
echo '<input type="text" class="edit" name="' . hsc($prefix . 'username') . '" value="' . hsc((string)$this->getConf($prefix . 'username')) . '" />';
echo '</label><br />';
echo '<label class="block"><span>' . hsc($this->getLang('calendar_slot_password')) . '</span> ';
echo '<input type="password" class="edit" name="' . hsc($prefix . 'password') . '" value="' . hsc((string)$this->getConf($prefix . 'password')) . '" />';
echo '</label><br />';
echo '<label class="block"><span>' . hsc($this->getLang('calendar_slot_color')) . '</span> ';
echo '<input type="color" name="' . hsc($prefix . 'color') . '" value="' . hsc((string)$this->getConf($prefix . 'color') ?: '#999999') . '" />';
echo '</label><br />';
$currentDisplay = (string)$this->getConf($prefix . 'display');
if ($currentDisplay === '') $currentDisplay = 'none';
echo '<label class="block"><span>' . hsc($this->getLang('calendar_slot_display')) . '</span> ';
echo '<select name="' . hsc($prefix . 'display') . '" class="edit">';
foreach ($displayOptions as $value => $label) {
if ($label === '') $label = $value;
$selected = ($currentDisplay === $value) ? ' selected="selected"' : '';
echo '<option value="' . hsc($value) . '"' . $selected . '>' . hsc($label) . '</option>';
}
echo '</select>';
echo '</label><br />';
}
// CalDAV Sync button (outside the save form, separate action)
$ajaxUrl = DOKU_BASE . 'lib/exe/ajax.php';
$sectok = getSecurityToken();
echo '<div class="luxtools-admin-sync" style="margin: 1em 0;">';
echo '<button type="button" class="button" id="luxtools-sync-btn">'
. hsc($this->getLang('calendar_sync_button'))
. '</button>';
echo '<span id="luxtools-sync-status" style="margin-left: 1em;"></span>';
echo '</div>';
echo '<script>';
echo 'document.getElementById("luxtools-sync-btn").addEventListener("click", function() {';
echo ' var btn = this;';
echo ' var status = document.getElementById("luxtools-sync-status");';
echo ' btn.disabled = true;';
echo ' status.textContent = "Syncing...";';
echo ' var xhr = new XMLHttpRequest();';
echo ' xhr.open("POST", ' . json_encode($ajaxUrl) . ', true);';
echo ' xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");';
echo ' xhr.onload = function() {';
echo ' btn.disabled = false;';
echo ' try {';
echo ' var r = JSON.parse(xhr.responseText);';
echo ' status.textContent = r.message || (r.ok ? "Done" : "Failed");';
echo ' status.style.color = r.ok ? "green" : "red";';
echo ' } catch(e) { status.textContent = "Error"; status.style.color = "red"; }';
echo ' };';
echo ' xhr.onerror = function() { btn.disabled = false; status.textContent = "Network error"; status.style.color = "red"; };';
echo ' xhr.send("call=luxtools_calendar_sync&sectok=" + encodeURIComponent(' . json_encode($sectok) . '));';
echo '});';
echo '</script>';
// pagelink_search_depth
echo '<label class="block"><span>' . hsc($this->getLang('pagelink_search_depth')) . '</span> ';
echo '<input type="number" class="edit" min="0" name="pagelink_search_depth" value="' . hsc((string)$this->getConf('pagelink_search_depth')) . '" />';
echo '</label><br />';
echo '<button type="submit" class="button">' . hsc($this->getLang('btn_save')) . '</button>';
echo '</fieldset>';
echo '</form>';
echo '</div>';
}
/**
* 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
*/
protected function savePluginLocalConf(array $newConf)
{
if (!defined('DOKU_CONF')) return false;
$plugin = 'luxtools';
$file = DOKU_CONF . 'local.php';
$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;
}
$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[\'plugin\'][\'' . $plugin . '\'][' . var_export($key, true) . '] = ' . $this->exportPhpValue($value, $key) . ';';
}
$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')) {
$ok = (bool)io_saveFile($file, $content);
} else {
$ok = @file_put_contents($file, $content, LOCK_EX) !== false;
}
// Ensure the updated conf/local.php is picked up immediately even when
// OPcache is configured to revalidate infrequently (e.g. revalidate_freq=60).
if ($ok && function_exists('opcache_invalidate')) {
@opcache_invalidate($file, true);
}
// 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;
}
/**
* Export a value to PHP code.
*
* We use nowdoc for multiline strings to safely preserve newlines.
*
* @param mixed $value
* @param string $key
* @return string
*/
protected function exportPhpValue($value, string $key): string
{
if (is_bool($value) || is_int($value) || is_float($value) || $value === null) {
return var_export($value, true);
}
$value = (string)$value;
if (str_contains($value, "\n") || str_contains($value, "\r")) {
$marker = strtoupper('LUXTOOLS_' . preg_replace('/[^A-Z0-9_]/i', '_', $key) . '_EOT');
// Extremely unlikely, but avoid delimiter collision.
while (str_contains($value, $marker)) {
$marker .= '_X';
}
return "<<<'$marker'\n" . $value . "\n$marker";
}
return var_export($value, true);
}
/**
* Strip nowdoc markers from values when displaying in the admin form.
*
* @param string $value
* @param string $key
* @return string
*/
protected function normalizeMultilineDisplay(string $value, string $key): string
{
$marker = strtoupper('LUXTOOLS_' . preg_replace('/[^A-Z0-9_]/i', '_', $key) . '_EOT');
$prefix = "<<<'$marker'\n";
$suffix = "\n$marker";
if (str_starts_with($value, $prefix) && str_ends_with($value, $suffix)) {
return substr($value, strlen($prefix), -strlen($suffix));
}
return $value;
}
}