1484 lines
47 KiB
PHP
1484 lines
47 KiB
PHP
<?php
|
|
|
|
use dokuwiki\Extension\ActionPlugin;
|
|
use dokuwiki\Extension\Event;
|
|
use dokuwiki\Extension\EventHandler;
|
|
use dokuwiki\plugin\luxtools\CacheInvalidation;
|
|
use dokuwiki\plugin\luxtools\CalDavClient;
|
|
use dokuwiki\plugin\luxtools\CalendarEvent;
|
|
use dokuwiki\plugin\luxtools\CalendarService;
|
|
use dokuwiki\plugin\luxtools\CalendarSlot;
|
|
use dokuwiki\plugin\luxtools\CalendarSyncService;
|
|
use dokuwiki\plugin\luxtools\ChronoID;
|
|
use dokuwiki\plugin\luxtools\ChronologicalCalendarWidget;
|
|
use dokuwiki\plugin\luxtools\ChronologicalDateAutoLinker;
|
|
use dokuwiki\plugin\luxtools\ChronologicalDayTemplate;
|
|
use dokuwiki\plugin\luxtools\IcsWriter;
|
|
use dokuwiki\plugin\luxtools\MenuItem\InvalidateCache;
|
|
require_once(__DIR__ . '/autoload.php');
|
|
|
|
/**
|
|
* luxtools action plugin: register JS assets.
|
|
*/
|
|
class action_plugin_luxtools extends ActionPlugin
|
|
{
|
|
/** @var bool Guard to prevent postprocess appenders during internal renders */
|
|
protected static $internalRenderInProgress = false;
|
|
|
|
/** @inheritdoc */
|
|
public function register(EventHandler $controller)
|
|
{
|
|
$controller->register_hook(
|
|
"TPL_METAHEADER_OUTPUT",
|
|
"BEFORE",
|
|
$this,
|
|
"addScripts",
|
|
);
|
|
$controller->register_hook(
|
|
"DOKUWIKI_STARTED",
|
|
"AFTER",
|
|
$this,
|
|
"provideJsInfo",
|
|
);
|
|
$controller->register_hook(
|
|
"RENDERER_CONTENT_POSTPROCESS",
|
|
"BEFORE",
|
|
$this,
|
|
"autoLinkChronologicalDates",
|
|
);
|
|
$controller->register_hook(
|
|
"RENDERER_CONTENT_POSTPROCESS",
|
|
"BEFORE",
|
|
$this,
|
|
"appendChronologicalDayEvents",
|
|
);
|
|
$controller->register_hook(
|
|
"RENDERER_CONTENT_POSTPROCESS",
|
|
"BEFORE",
|
|
$this,
|
|
"appendChronologicalDayPhotos",
|
|
);
|
|
$controller->register_hook(
|
|
"COMMON_PAGETPL_LOAD",
|
|
"BEFORE",
|
|
$this,
|
|
"prefillChronologicalDayTemplate",
|
|
);
|
|
$controller->register_hook(
|
|
"TPL_ACT_RENDER",
|
|
"BEFORE",
|
|
$this,
|
|
"renderVirtualChronologicalDayPage",
|
|
);
|
|
$controller->register_hook(
|
|
"AJAX_CALL_UNKNOWN",
|
|
"BEFORE",
|
|
$this,
|
|
"handleCalendarWidgetAjax",
|
|
);
|
|
$controller->register_hook(
|
|
"AJAX_CALL_UNKNOWN",
|
|
"BEFORE",
|
|
$this,
|
|
"handleMaintenanceTaskAction",
|
|
);
|
|
$controller->register_hook(
|
|
"AJAX_CALL_UNKNOWN",
|
|
"BEFORE",
|
|
$this,
|
|
"handleCalendarSyncAction",
|
|
);
|
|
$controller->register_hook(
|
|
"AJAX_CALL_UNKNOWN",
|
|
"BEFORE",
|
|
$this,
|
|
"handleCalendarSlotsAction",
|
|
);
|
|
$controller->register_hook(
|
|
"AJAX_CALL_UNKNOWN",
|
|
"BEFORE",
|
|
$this,
|
|
"handleCalendarEventAction",
|
|
);
|
|
$controller->register_hook(
|
|
"ACTION_ACT_PREPROCESS",
|
|
"BEFORE",
|
|
$this,
|
|
"handleInvalidateCacheAction",
|
|
);
|
|
$controller->register_hook(
|
|
"MENU_ITEMS_ASSEMBLY",
|
|
"AFTER",
|
|
$this,
|
|
"addInvalidateCacheMenuItem",
|
|
);
|
|
$controller->register_hook(
|
|
"TOOLBAR_DEFINE",
|
|
"AFTER",
|
|
$this,
|
|
"addToolbarButton",
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Add plugin JavaScript files in a deterministic order.
|
|
*
|
|
* @param Event $event
|
|
* @param mixed $param
|
|
* @return void
|
|
*/
|
|
public function addScripts(Event $event, $param)
|
|
{
|
|
$plugin = $this->getPluginName();
|
|
$base = DOKU_BASE . "lib/plugins/$plugin/js/";
|
|
$scripts = [
|
|
"lightbox.js",
|
|
"gallery-thumbnails.js",
|
|
"open-service.js",
|
|
"scratchpads.js",
|
|
"date-fix.js",
|
|
"page-link.js",
|
|
"linkfavicon.js",
|
|
"calendar-widget.js",
|
|
"event-popup.js",
|
|
"movie-import.js",
|
|
"main.js",
|
|
];
|
|
|
|
foreach ($scripts as $script) {
|
|
$event->data["script"][] = [
|
|
"type" => "text/javascript",
|
|
"src" => $base . $script,
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Pass plugin data to client-side JavaScript via JSINFO.
|
|
*
|
|
* Must run before TPL_METAHEADER_OUTPUT because JSINFO is serialized
|
|
* during tpl_metaheaders() before that event fires.
|
|
*
|
|
* @param Event $event
|
|
* @param mixed $param
|
|
* @return void
|
|
*/
|
|
public function provideJsInfo(Event $event, $param)
|
|
{
|
|
// Intentional: the key is exposed to the browser for direct OMDb lookups.
|
|
global $JSINFO;
|
|
$JSINFO['luxtools_omdb_apikey'] = (string)$this->getConf('omdb_apikey');
|
|
}
|
|
|
|
/**
|
|
* Serve server-rendered calendar widget HTML for month navigation.
|
|
*
|
|
* @param Event $event
|
|
* @param mixed $param
|
|
* @return void
|
|
*/
|
|
public function handleCalendarWidgetAjax(Event $event, $param)
|
|
{
|
|
if ($event->data !== 'luxtools_calendar_month') return;
|
|
|
|
$event->preventDefault();
|
|
$event->stopPropagation();
|
|
|
|
global $INPUT;
|
|
|
|
$year = (int)$INPUT->int('year');
|
|
$month = (int)$INPUT->int('month');
|
|
$baseNs = trim((string)$INPUT->str('base'));
|
|
if ($baseNs === '') {
|
|
$baseNs = 'chronological';
|
|
}
|
|
$size = ChronologicalCalendarWidget::normalizeSize((string)$INPUT->str('size'));
|
|
$showTimes = ChronologicalCalendarWidget::normalizeShowTimes($INPUT->str('show_times'));
|
|
|
|
if (!ChronologicalCalendarWidget::isValidMonth($year, $month)) {
|
|
http_status(400);
|
|
echo 'Invalid month';
|
|
return;
|
|
}
|
|
|
|
$this->sendNoStoreHeaders();
|
|
|
|
$slots = CalendarSlot::loadEnabled($this);
|
|
$widgetSlots = CalendarSlot::filterWidgetVisible($slots);
|
|
$indicators = [];
|
|
$dayEvents = [];
|
|
if ($size === 'large') {
|
|
$widgetData = CalendarService::monthWidgetData($widgetSlots, $year, $month);
|
|
$indicators = $widgetData['indicators'];
|
|
$dayEvents = $widgetData['events'];
|
|
} else {
|
|
$indicators = CalendarService::monthIndicators($widgetSlots, $year, $month);
|
|
}
|
|
|
|
$slotColors = [];
|
|
$slotDisplays = [];
|
|
foreach ($widgetSlots as $slot) {
|
|
$color = $slot->getColor();
|
|
if ($color !== '') {
|
|
$slotColors[$slot->getKey()] = $color;
|
|
}
|
|
$slotDisplays[$slot->getKey()] = $slot->getDisplay();
|
|
}
|
|
|
|
$html = ChronologicalCalendarWidget::render($year, $month, $baseNs, $indicators, $slotColors, $slotDisplays, [
|
|
'size' => $size,
|
|
'showTimes' => $showTimes,
|
|
'dayEvents' => $dayEvents,
|
|
]);
|
|
if ($html === '') {
|
|
http_status(500);
|
|
echo 'Calendar rendering failed';
|
|
return;
|
|
}
|
|
|
|
header('Content-Type: text/html; charset=utf-8');
|
|
echo $html;
|
|
}
|
|
|
|
/**
|
|
* Auto-link strict ISO dates (YYYY-MM-DD) in rendered XHTML text nodes.
|
|
*
|
|
* Excludes content inside tags where links should not be altered.
|
|
*
|
|
* @param Event $event
|
|
* @param mixed $param
|
|
* @return void
|
|
*/
|
|
public function autoLinkChronologicalDates(Event $event, $param)
|
|
{
|
|
if (!is_array($event->data)) return;
|
|
|
|
$mode = (string)($event->data[0] ?? '');
|
|
if ($mode !== 'xhtml') return;
|
|
|
|
$doc = $event->data[1] ?? null;
|
|
if (!is_string($doc) || $doc === '') return;
|
|
if (!preg_match('/\d{4}-\d{2}-\d{2}/', $doc)) return;
|
|
|
|
$event->data[1] = ChronologicalDateAutoLinker::linkHtml($doc);
|
|
}
|
|
|
|
/**
|
|
* Prefill new chronological day pages with a German date headline.
|
|
*
|
|
* @param Event $event
|
|
* @param mixed $param
|
|
* @return void
|
|
*/
|
|
public function prefillChronologicalDayTemplate(Event $event, $param)
|
|
{
|
|
if (!is_array($event->data)) return;
|
|
|
|
$id = (string)($event->data['id'] ?? '');
|
|
if ($id === '') return;
|
|
|
|
if (function_exists('cleanID')) {
|
|
$id = (string)cleanID($id);
|
|
}
|
|
if ($id === '') return;
|
|
if (!ChronoID::isDayId($id)) return;
|
|
|
|
$template = ChronologicalDayTemplate::buildForDayId($id);
|
|
if ($template === null || $template === '') return;
|
|
|
|
$event->data['tpl'] = $template;
|
|
$event->data['tplfile'] = '';
|
|
$event->data['doreplace'] = false;
|
|
}
|
|
|
|
/**
|
|
* Append matching date-prefixed photos to chronological day page output.
|
|
*
|
|
* @param Event $event
|
|
* @param mixed $param
|
|
* @return void
|
|
*/
|
|
public function appendChronologicalDayPhotos(Event $event, $param)
|
|
{
|
|
if (self::$internalRenderInProgress) return;
|
|
if (!is_array($event->data)) return;
|
|
|
|
$mode = (string)($event->data[0] ?? '');
|
|
if ($mode !== 'xhtml') return;
|
|
|
|
global $ACT;
|
|
if (!is_string($ACT) || $ACT !== 'show') return;
|
|
|
|
$doc = $event->data[1] ?? null;
|
|
if (!is_string($doc)) return;
|
|
if (str_contains($doc, 'luxtools-chronological-photos')) return;
|
|
|
|
global $ID;
|
|
$id = is_string($ID) ? $ID : '';
|
|
if ($id === '') return;
|
|
if (function_exists('cleanID')) {
|
|
$id = (string)cleanID($id);
|
|
}
|
|
if ($id === '') return;
|
|
|
|
$parts = ChronoID::parseDayId($id);
|
|
if ($parts === null) return;
|
|
|
|
if (!function_exists('page_exists') || !page_exists($id)) return;
|
|
|
|
$basePath = trim((string)$this->getConf('image_base_path'));
|
|
if ($basePath === '') return;
|
|
|
|
$dateIso = sprintf('%04d-%02d-%02d', $parts['year'], $parts['month'], $parts['day']);
|
|
if (!$this->hasAnyChronologicalPhotos($dateIso)) return;
|
|
|
|
$photosHtml = $this->renderChronologicalPhotosMacro($dateIso);
|
|
if ($photosHtml === '') return;
|
|
|
|
$event->data[1] = $doc . $photosHtml;
|
|
}
|
|
|
|
/**
|
|
* Append local calendar events to existing chronological day pages.
|
|
*
|
|
* @param Event $event
|
|
* @param mixed $param
|
|
* @return void
|
|
*/
|
|
public function appendChronologicalDayEvents(Event $event, $param)
|
|
{
|
|
static $appendInProgress = false;
|
|
if ($appendInProgress) return;
|
|
if (self::$internalRenderInProgress) return;
|
|
|
|
if (!is_array($event->data)) return;
|
|
|
|
$mode = (string)($event->data[0] ?? '');
|
|
if ($mode !== 'xhtml') return;
|
|
|
|
global $ACT;
|
|
if (!is_string($ACT) || $ACT !== 'show') return;
|
|
|
|
$doc = $event->data[1] ?? null;
|
|
if (!is_string($doc)) return;
|
|
if (str_contains($doc, 'luxtools-chronological-events')) return;
|
|
|
|
global $ID;
|
|
$id = is_string($ID) ? $ID : '';
|
|
if ($id === '') return;
|
|
if (function_exists('cleanID')) {
|
|
$id = (string)cleanID($id);
|
|
}
|
|
if ($id === '') return;
|
|
|
|
$parts = ChronoID::parseDayId($id);
|
|
if ($parts === null) return;
|
|
if (!function_exists('page_exists') || !page_exists($id)) return;
|
|
|
|
$dateIso = sprintf('%04d-%02d-%02d', $parts['year'], $parts['month'], $parts['day']);
|
|
$appendInProgress = true;
|
|
try {
|
|
$eventsHtml = $this->renderChronologicalEventsHtml($dateIso);
|
|
} finally {
|
|
$appendInProgress = false;
|
|
}
|
|
if ($eventsHtml === '') return;
|
|
|
|
$event->data[1] = $doc . $eventsHtml;
|
|
}
|
|
|
|
/**
|
|
* Render chronological day photos using existing {{images>...}} syntax.
|
|
*
|
|
* @param string $dateIso
|
|
* @return string
|
|
*/
|
|
protected function renderChronologicalPhotosMacro(string $dateIso): string
|
|
{
|
|
$syntax = $this->buildChronologicalImagesSyntax($dateIso);
|
|
if ($syntax === '') return '';
|
|
|
|
if (self::$internalRenderInProgress) return '';
|
|
self::$internalRenderInProgress = true;
|
|
|
|
try {
|
|
$info = ['cache' => false];
|
|
$instructions = p_get_instructions($syntax);
|
|
$galleryHtml = (string)p_render('xhtml', $instructions, $info);
|
|
} finally {
|
|
self::$internalRenderInProgress = false;
|
|
}
|
|
|
|
if ($galleryHtml === '') return '';
|
|
|
|
$title = (string)$this->getLang('chronological_photos_title');
|
|
if ($title === '') $title = 'Photos';
|
|
|
|
return '<div class="luxtools-plugin luxtools-chronological-photos">'
|
|
. '<h2>' . hsc($title) . '</h2>'
|
|
. $galleryHtml
|
|
. '</div>';
|
|
}
|
|
|
|
/**
|
|
* Build {{images>...}} syntax for a given day.
|
|
*
|
|
* @param string $dateIso
|
|
* @return string
|
|
*/
|
|
protected function buildChronologicalImagesSyntax(string $dateIso): string
|
|
{
|
|
$basePath = trim((string)$this->getConf('image_base_path'));
|
|
if ($basePath === '') return '';
|
|
|
|
$base = \dokuwiki\plugin\luxtools\Path::cleanPath($basePath);
|
|
if (!is_dir($base) || !is_readable($base)) return '';
|
|
|
|
$yearDir = rtrim($base, '/') . '/' . substr($dateIso, 0, 4) . '/';
|
|
$targetDir = (is_dir($yearDir) && is_readable($yearDir)) ? $yearDir : $base;
|
|
|
|
return '{{images>' . $targetDir . $dateIso . '*&recursive=0}}';
|
|
}
|
|
|
|
/**
|
|
* Render a virtual day page for missing chronological day IDs.
|
|
*
|
|
* Shows a German date heading and existing day photos (if any) without creating the page.
|
|
*
|
|
* @param Event $event
|
|
* @param mixed $param
|
|
* @return void
|
|
*/
|
|
public function renderVirtualChronologicalDayPage(Event $event, $param)
|
|
{
|
|
if (!is_string($event->data) || $event->data !== 'show') return;
|
|
|
|
global $ID;
|
|
$id = is_string($ID) ? $ID : '';
|
|
if ($id === '') return;
|
|
if (function_exists('cleanID')) {
|
|
$id = (string)cleanID($id);
|
|
}
|
|
if ($id === '') return;
|
|
|
|
if (!ChronoID::isDayId($id)) return;
|
|
|
|
$this->sendNoStoreHeaders();
|
|
|
|
if (function_exists('page_exists') && page_exists($id)) return;
|
|
|
|
$wikiText = ChronologicalDayTemplate::buildForDayId($id) ?? '';
|
|
if ($wikiText === '') return;
|
|
|
|
$parts = ChronoID::parseDayId($id);
|
|
$extraHtml = '';
|
|
if ($parts !== null) {
|
|
$dateIso = sprintf('%04d-%02d-%02d', $parts['year'], $parts['month'], $parts['day']);
|
|
|
|
$eventsHtml = $this->renderChronologicalEventsHtml($dateIso);
|
|
if ($eventsHtml !== '') {
|
|
$extraHtml .= $eventsHtml;
|
|
}
|
|
|
|
if ($this->hasAnyChronologicalPhotos($dateIso)) {
|
|
$photosHtml = $this->renderChronologicalPhotosMacro($dateIso);
|
|
if ($photosHtml !== '') {
|
|
$extraHtml .= $photosHtml;
|
|
}
|
|
}
|
|
}
|
|
|
|
$editUrl = function_exists('wl') ? (string)wl($id, ['do' => 'edit']) : '';
|
|
$createLinkHtml = '';
|
|
if ($editUrl !== '') {
|
|
$label = (string)$this->getLang('btn_create');
|
|
if ($label === '') $label = 'Create this page';
|
|
$createLinkHtml = '<p><a href="' . hsc($editUrl) . '">✎ ' . hsc($label) . '</a></p>';
|
|
}
|
|
|
|
$info = ['cache' => false];
|
|
$instructions = p_get_instructions($wikiText);
|
|
$html = (string)p_render('xhtml', $instructions, $info);
|
|
|
|
echo $html . $createLinkHtml . $extraHtml;
|
|
$event->preventDefault();
|
|
$event->stopPropagation();
|
|
}
|
|
|
|
/**
|
|
* Check if there is at least one date-prefixed image for the given day.
|
|
*
|
|
* @param string $dateIso
|
|
* @return bool
|
|
*/
|
|
protected function hasAnyChronologicalPhotos(string $dateIso): bool
|
|
{
|
|
if (!ChronoID::isIsoDate($dateIso)) return false;
|
|
|
|
$basePath = trim((string)$this->getConf('image_base_path'));
|
|
if ($basePath === '') return false;
|
|
|
|
$base = \dokuwiki\plugin\luxtools\Path::cleanPath($basePath);
|
|
if (!is_dir($base) || !is_readable($base)) return false;
|
|
|
|
$yearDir = rtrim($base, '/') . '/' . substr($dateIso, 0, 4) . '/';
|
|
$targetDir = (is_dir($yearDir) && is_readable($yearDir)) ? $yearDir : $base;
|
|
|
|
$pattern = rtrim($targetDir, '/') . '/' . $dateIso . '*';
|
|
$matches = glob($pattern) ?: [];
|
|
foreach ($matches as $match) {
|
|
if (!is_file($match)) continue;
|
|
$ext = strtolower(pathinfo($match, PATHINFO_EXTENSION));
|
|
if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'], true)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Render local calendar events section for a given date.
|
|
*
|
|
* Uses the slot-aware CalendarService to render events from all enabled slots.
|
|
*
|
|
* @param string $dateIso
|
|
* @return string
|
|
*/
|
|
protected function renderChronologicalEventsHtml(string $dateIso): string
|
|
{
|
|
$slots = CalendarSlot::loadEnabled($this);
|
|
if ($slots === []) return '';
|
|
|
|
$grouped = CalendarService::eventsForDateGrouped($slots, $dateIso);
|
|
if ($grouped === []) return '';
|
|
|
|
$html = '';
|
|
|
|
// Render general events
|
|
if (isset($grouped['general'])) {
|
|
$title = (string)$this->getLang('chronological_events_title');
|
|
if ($title === '') $title = 'Events';
|
|
$html .= $this->renderEventSection($grouped['general'], $title, 'general');
|
|
}
|
|
|
|
// Render maintenance tasks
|
|
if (isset($grouped['maintenance'])) {
|
|
$title = (string)$this->getLang('chronological_maintenance_title');
|
|
if ($title === '') $title = 'Tasks';
|
|
$html .= $this->renderMaintenanceSection($grouped['maintenance'], $title, $dateIso);
|
|
}
|
|
|
|
// Render slot3/slot4 if present
|
|
foreach (['slot3', 'slot4'] as $slotKey) {
|
|
if (isset($grouped[$slotKey]) && isset($slots[$slotKey])) {
|
|
$label = $slots[$slotKey]->getLabel();
|
|
$html .= $this->renderEventSection($grouped[$slotKey], $label, $slotKey);
|
|
}
|
|
}
|
|
|
|
return $html;
|
|
}
|
|
|
|
/**
|
|
* Render a section of events for a given slot.
|
|
*
|
|
* @param CalendarEvent[] $events
|
|
* @param string $title
|
|
* @param string $slotKey
|
|
* @return string
|
|
*/
|
|
protected function renderEventSection(array $events, string $title, string $slotKey): string
|
|
{
|
|
$items = '';
|
|
foreach ($events as $event) {
|
|
$items .= $this->renderEventListItem($event);
|
|
}
|
|
if ($items === '') return '';
|
|
|
|
return '<div class="luxtools-plugin luxtools-chronological-events luxtools-slot-' . hsc($slotKey) . '">'
|
|
. '<h2>' . hsc($title) . '</h2>'
|
|
. '<ul>' . $items . '</ul>'
|
|
. '</div>';
|
|
}
|
|
|
|
/**
|
|
* Render a maintenance task section with completion buttons.
|
|
*
|
|
* @param CalendarEvent[] $events
|
|
* @param string $title
|
|
* @param string $dateIso
|
|
* @return string
|
|
*/
|
|
protected function renderMaintenanceSection(array $events, string $title, string $dateIso): string
|
|
{
|
|
$items = '';
|
|
$ajaxUrl = defined('DOKU_BASE') ? (string)DOKU_BASE . 'lib/exe/ajax.php' : 'lib/exe/ajax.php';
|
|
|
|
foreach ($events as $event) {
|
|
$items .= $this->renderMaintenanceListItem($event, $ajaxUrl);
|
|
}
|
|
if ($items === '') return '';
|
|
|
|
$secToken = function_exists('getSecurityToken') ? getSecurityToken() : '';
|
|
|
|
return '<div class="luxtools-plugin luxtools-chronological-events luxtools-chronological-maintenance"'
|
|
. ' data-luxtools-ajax-url="' . hsc($ajaxUrl) . '"'
|
|
. ' data-luxtools-sectok="' . hsc($secToken) . '">'
|
|
. '<h2>' . hsc($title) . '</h2>'
|
|
. '<ul>' . $items . '</ul>'
|
|
. '</div>';
|
|
}
|
|
|
|
/**
|
|
* Render a single event as a list item with popup data attributes.
|
|
*
|
|
* @param CalendarEvent $event
|
|
* @return string
|
|
*/
|
|
protected function renderEventListItem(CalendarEvent $event): string
|
|
{
|
|
$summaryHtml = hsc($event->summary);
|
|
|
|
// Build event detail data attributes for popup
|
|
$dataAttrs = ' data-luxtools-event="1"';
|
|
$dataAttrs .= ' data-event-summary="' . hsc($event->summary) . '"';
|
|
$dataAttrs .= ' data-event-start="' . hsc($event->startIso) . '"';
|
|
if ($event->endIso !== '') {
|
|
$dataAttrs .= ' data-event-end="' . hsc($event->endIso) . '"';
|
|
}
|
|
if ($event->location !== '') {
|
|
$dataAttrs .= ' data-event-location="' . hsc($event->location) . '"';
|
|
}
|
|
if ($event->description !== '') {
|
|
$dataAttrs .= ' data-event-description="' . hsc($event->description) . '"';
|
|
}
|
|
$dataAttrs .= ' data-event-allday="' . ($event->allDay ? '1' : '0') . '"';
|
|
$dataAttrs .= ' data-event-slot="' . hsc($event->slotKey) . '"';
|
|
if ($event->uid !== '') {
|
|
$dataAttrs .= ' data-event-uid="' . hsc($event->uid) . '"';
|
|
}
|
|
if ($event->recurrenceId !== '') {
|
|
$dataAttrs .= ' data-event-recurrence="' . hsc($event->recurrenceId) . '"';
|
|
}
|
|
if ($event->dateIso !== '') {
|
|
$dataAttrs .= ' data-event-date="' . hsc($event->dateIso) . '"';
|
|
}
|
|
|
|
if ($event->allDay || $event->time === '') {
|
|
return '<li' . $dataAttrs . '><span class="luxtools-event-summary">' . $summaryHtml . '</span></li>';
|
|
}
|
|
|
|
$timeHtml = '<span class="luxtools-event-time" data-luxtools-start="' . hsc($event->startIso) . '">'
|
|
. hsc($event->time) . '</span>';
|
|
return '<li' . $dataAttrs . '>' . $timeHtml . ' - <span class="luxtools-event-summary">' . $summaryHtml . '</span></li>';
|
|
}
|
|
|
|
/**
|
|
* Render a maintenance task as a list item with completion button.
|
|
*
|
|
* @param CalendarEvent $event
|
|
* @param string $ajaxUrl
|
|
* @return string
|
|
*/
|
|
protected function renderMaintenanceListItem(CalendarEvent $event, string $ajaxUrl): string
|
|
{
|
|
$isCompleted = $event->isCompleted();
|
|
$classes = 'luxtools-maintenance-task';
|
|
if ($isCompleted) $classes .= ' luxtools-task-completed';
|
|
|
|
$summaryHtml = hsc($event->summary);
|
|
|
|
// Data attributes for popup and completion
|
|
$dataAttrs = ' data-luxtools-event="1"';
|
|
$dataAttrs .= ' data-event-summary="' . hsc($event->summary) . '"';
|
|
$dataAttrs .= ' data-event-start="' . hsc($event->startIso) . '"';
|
|
if ($event->endIso !== '') {
|
|
$dataAttrs .= ' data-event-end="' . hsc($event->endIso) . '"';
|
|
}
|
|
if ($event->location !== '') {
|
|
$dataAttrs .= ' data-event-location="' . hsc($event->location) . '"';
|
|
}
|
|
if ($event->description !== '') {
|
|
$dataAttrs .= ' data-event-description="' . hsc($event->description) . '"';
|
|
}
|
|
$dataAttrs .= ' data-event-allday="' . ($event->allDay ? '1' : '0') . '"';
|
|
$dataAttrs .= ' data-event-slot="maintenance"';
|
|
$dataAttrs .= ' data-task-uid="' . hsc($event->uid) . '"';
|
|
$dataAttrs .= ' data-task-date="' . hsc($event->dateIso) . '"';
|
|
$dataAttrs .= ' data-task-recurrence="' . hsc($event->recurrenceId) . '"';
|
|
$dataAttrs .= ' data-task-status="' . hsc($event->status) . '"';
|
|
|
|
$buttonLabel = $isCompleted
|
|
? (string)$this->getLang('maintenance_task_reopen')
|
|
: (string)$this->getLang('maintenance_task_complete');
|
|
if ($buttonLabel === '') $buttonLabel = $isCompleted ? 'Reopen' : 'Complete';
|
|
$buttonAction = $isCompleted ? 'reopen' : 'complete';
|
|
|
|
$buttonHtml = '<button type="button" class="luxtools-task-action" data-action="' . hsc($buttonAction) . '">'
|
|
. hsc($buttonLabel) . '</button>';
|
|
|
|
$timeHtml = '';
|
|
if (!$event->allDay && $event->time !== '') {
|
|
$timeHtml = '<span class="luxtools-event-time" data-luxtools-start="' . hsc($event->startIso) . '">'
|
|
. hsc($event->time) . '</span> - ';
|
|
}
|
|
|
|
return '<li class="' . hsc($classes) . '"' . $dataAttrs . '>'
|
|
. $timeHtml
|
|
. '<span class="luxtools-event-summary">' . $summaryHtml . '</span> '
|
|
. $buttonHtml
|
|
. '</li>';
|
|
}
|
|
|
|
/**
|
|
* Handle AJAX requests for marking maintenance tasks complete/reopen.
|
|
*
|
|
* @param Event $event
|
|
* @param mixed $param
|
|
* @return void
|
|
*/
|
|
public function handleMaintenanceTaskAction(Event $event, $param)
|
|
{
|
|
if ($event->data !== 'luxtools_maintenance_task') return;
|
|
|
|
$event->preventDefault();
|
|
$event->stopPropagation();
|
|
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
$this->sendNoStoreHeaders();
|
|
|
|
global $INPUT;
|
|
|
|
// Verify security token
|
|
if (!checkSecurityToken()) {
|
|
http_status(403);
|
|
echo json_encode(['ok' => false, 'error' => 'Security token mismatch']);
|
|
return;
|
|
}
|
|
|
|
$action = $INPUT->str('action'); // 'complete' or 'reopen'
|
|
$uid = $INPUT->str('uid');
|
|
$dateIso = $INPUT->str('date');
|
|
$recurrence = $INPUT->str('recurrence');
|
|
|
|
if (!in_array($action, ['complete', 'reopen'], true)) {
|
|
http_status(400);
|
|
echo json_encode(['ok' => false, 'error' => 'Invalid action']);
|
|
return;
|
|
}
|
|
if ($uid === '' || !ChronoID::isIsoDate($dateIso)) {
|
|
http_status(400);
|
|
echo json_encode(['ok' => false, 'error' => 'Missing uid or date']);
|
|
return;
|
|
}
|
|
|
|
$slots = CalendarSlot::loadAll($this);
|
|
$maintenanceSlot = $slots['maintenance'] ?? null;
|
|
if ($maintenanceSlot === null || !$maintenanceSlot->isEnabled()) {
|
|
http_status(400);
|
|
echo json_encode(['ok' => false, 'error' => 'Maintenance calendar not configured']);
|
|
return;
|
|
}
|
|
|
|
$newStatus = ($action === 'complete') ? 'COMPLETED' : 'TODO';
|
|
|
|
// Update local ICS file
|
|
$localOk = false;
|
|
$file = $maintenanceSlot->getFile();
|
|
if ($file !== '' && is_file($file)) {
|
|
$localOk = IcsWriter::updateEventStatus($file, $uid, $recurrence, $newStatus, $dateIso);
|
|
}
|
|
|
|
if (!$localOk) {
|
|
http_status(500);
|
|
echo json_encode(['ok' => false, 'error' => $this->getLang('maintenance_complete_error')]);
|
|
return;
|
|
}
|
|
|
|
// Clear caches so next render picks up changes
|
|
CalendarService::clearCache();
|
|
|
|
// Remote CalDAV write-back if configured
|
|
$remoteOk = true;
|
|
$remoteError = '';
|
|
if ($maintenanceSlot->hasRemoteSource()) {
|
|
try {
|
|
$caldavResult = CalDavClient::updateEventStatus(
|
|
$maintenanceSlot->getCaldavUrl(),
|
|
$maintenanceSlot->getUsername(),
|
|
$maintenanceSlot->getPassword(),
|
|
$uid,
|
|
$recurrence,
|
|
$newStatus,
|
|
$dateIso
|
|
);
|
|
if ($caldavResult !== '') {
|
|
$remoteOk = false;
|
|
$remoteError = $this->getLang('maintenance_remote_write_failed') . ': ' . $caldavResult;
|
|
}
|
|
} catch (Throwable $e) {
|
|
$remoteOk = false;
|
|
$remoteError = $this->getLang('maintenance_remote_write_failed') . ': ' . $e->getMessage();
|
|
}
|
|
}
|
|
|
|
$msg = ($action === 'complete')
|
|
? $this->getLang('maintenance_complete_success')
|
|
: $this->getLang('maintenance_reopen_success');
|
|
|
|
echo json_encode([
|
|
'ok' => true,
|
|
'message' => $msg,
|
|
'remoteOk' => $remoteOk,
|
|
'remoteError' => $remoteError,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Handle AJAX requests for manual calendar sync.
|
|
*
|
|
* @param Event $event
|
|
* @param mixed $param
|
|
* @return void
|
|
*/
|
|
public function handleCalendarSyncAction(Event $event, $param)
|
|
{
|
|
if ($event->data !== 'luxtools_calendar_sync') return;
|
|
|
|
$event->preventDefault();
|
|
$event->stopPropagation();
|
|
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
$this->sendNoStoreHeaders();
|
|
|
|
global $INPUT;
|
|
|
|
if (!checkSecurityToken()) {
|
|
http_status(403);
|
|
echo json_encode(['ok' => false, 'error' => 'Security token mismatch']);
|
|
return;
|
|
}
|
|
|
|
if (empty($_SERVER['REMOTE_USER'])) {
|
|
http_status(403);
|
|
echo json_encode(['ok' => false, 'error' => 'Authentication required']);
|
|
return;
|
|
}
|
|
|
|
$slots = CalendarSlot::loadEnabled($this);
|
|
$result = CalendarSyncService::syncAll($slots);
|
|
|
|
$msg = $result['ok']
|
|
? $this->getLang('calendar_sync_success')
|
|
: $this->getLang('calendar_sync_partial');
|
|
|
|
echo json_encode([
|
|
'ok' => $result['ok'],
|
|
'message' => $msg,
|
|
'results' => $result['results'],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Return available calendar slots as JSON for the event creation form.
|
|
*
|
|
* @param Event $event
|
|
* @param mixed $param
|
|
* @return void
|
|
*/
|
|
public function handleCalendarSlotsAction(Event $event, $param)
|
|
{
|
|
if ($event->data !== 'luxtools_calendar_slots') return;
|
|
|
|
$event->preventDefault();
|
|
$event->stopPropagation();
|
|
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
$this->sendNoStoreHeaders();
|
|
|
|
$slots = CalendarSlot::loadEnabled($this);
|
|
$result = [];
|
|
foreach ($slots as $slot) {
|
|
$result[] = [
|
|
'key' => $slot->getKey(),
|
|
'label' => $slot->getLabel(),
|
|
];
|
|
}
|
|
|
|
echo json_encode(['ok' => true, 'slots' => $result]);
|
|
}
|
|
|
|
/**
|
|
* Handle AJAX requests for creating, editing, and deleting calendar events.
|
|
*
|
|
* @param Event $event
|
|
* @param mixed $param
|
|
* @return void
|
|
*/
|
|
public function handleCalendarEventAction(Event $event, $param)
|
|
{
|
|
if ($event->data !== 'luxtools_calendar_event') return;
|
|
|
|
$event->preventDefault();
|
|
$event->stopPropagation();
|
|
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
$this->sendNoStoreHeaders();
|
|
|
|
global $INPUT;
|
|
|
|
// Require security token
|
|
if (!checkSecurityToken()) {
|
|
http_status(403);
|
|
echo json_encode(['ok' => false, 'error' => 'Security token mismatch']);
|
|
return;
|
|
}
|
|
|
|
// Require authenticated user
|
|
if (!isset($_SERVER['REMOTE_USER']) || $_SERVER['REMOTE_USER'] === '') {
|
|
http_status(403);
|
|
echo json_encode(['ok' => false, 'error' => 'Authentication required']);
|
|
return;
|
|
}
|
|
|
|
$action = $INPUT->str('action');
|
|
if (!in_array($action, ['create', 'edit', 'delete'], true)) {
|
|
http_status(400);
|
|
echo json_encode(['ok' => false, 'error' => 'Invalid action']);
|
|
return;
|
|
}
|
|
|
|
if ($action === 'create') {
|
|
$this->handleEventCreate($INPUT);
|
|
} elseif ($action === 'edit') {
|
|
$this->handleEventEdit($INPUT);
|
|
} elseif ($action === 'delete') {
|
|
$this->handleEventDelete($INPUT);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle event creation.
|
|
*
|
|
* @param \dokuwiki\Input\Input $INPUT
|
|
* @return void
|
|
*/
|
|
protected function handleEventCreate($INPUT): void
|
|
{
|
|
$slotKey = $INPUT->str('slot');
|
|
$summary = trim($INPUT->str('summary'));
|
|
$dateIso = $INPUT->str('date');
|
|
|
|
if ($summary === '') {
|
|
http_status(400);
|
|
echo json_encode(['ok' => false, 'error' => 'Summary is required']);
|
|
return;
|
|
}
|
|
if (!ChronoID::isIsoDate($dateIso)) {
|
|
http_status(400);
|
|
echo json_encode(['ok' => false, 'error' => 'Invalid date']);
|
|
return;
|
|
}
|
|
|
|
$slots = CalendarSlot::loadAll($this);
|
|
$slot = $slots[$slotKey] ?? null;
|
|
if ($slot === null || !$slot->isEnabled()) {
|
|
http_status(400);
|
|
echo json_encode(['ok' => false, 'error' => 'Invalid calendar slot']);
|
|
return;
|
|
}
|
|
|
|
$file = $slot->getFile();
|
|
if ($file === '') {
|
|
http_status(400);
|
|
echo json_encode(['ok' => false, 'error' => 'No local file configured for this slot']);
|
|
return;
|
|
}
|
|
|
|
$eventData = [
|
|
'summary' => $summary,
|
|
'date' => $dateIso,
|
|
'allDay' => $INPUT->bool('allday'),
|
|
'startTime' => $INPUT->str('start_time'),
|
|
'endTime' => $INPUT->str('end_time'),
|
|
'location' => trim($INPUT->str('location')),
|
|
'description' => trim($INPUT->str('description')),
|
|
];
|
|
|
|
$uid = IcsWriter::createEvent($file, $eventData);
|
|
if ($uid === '') {
|
|
http_status(500);
|
|
echo json_encode(['ok' => false, 'error' => 'Failed to create event']);
|
|
return;
|
|
}
|
|
|
|
CalendarService::clearCache();
|
|
|
|
// CalDAV write-back if configured
|
|
$remoteOk = true;
|
|
$remoteError = '';
|
|
if ($slot->hasRemoteSource()) {
|
|
$remoteOk = $this->pushEventToCalDav($slot, $file, $uid);
|
|
if (!$remoteOk) {
|
|
$remoteError = 'Local event created, but CalDAV upload failed.';
|
|
}
|
|
}
|
|
|
|
echo json_encode([
|
|
'ok' => true,
|
|
'message' => 'Event created.',
|
|
'uid' => $uid,
|
|
'remoteOk' => $remoteOk,
|
|
'remoteError' => $remoteError,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Handle event editing.
|
|
*
|
|
* @param \dokuwiki\Input\Input $INPUT
|
|
* @return void
|
|
*/
|
|
protected function handleEventEdit($INPUT): void
|
|
{
|
|
$uid = $INPUT->str('uid');
|
|
$recurrence = $INPUT->str('recurrence');
|
|
$slotKey = $INPUT->str('slot');
|
|
$summary = trim($INPUT->str('summary'));
|
|
$dateIso = $INPUT->str('date');
|
|
$scope = $INPUT->str('scope', 'all');
|
|
|
|
if (!in_array($scope, ['all', 'this', 'future'], true)) {
|
|
$scope = 'all';
|
|
}
|
|
|
|
if ($uid === '') {
|
|
http_status(400);
|
|
echo json_encode(['ok' => false, 'error' => 'Missing event UID']);
|
|
return;
|
|
}
|
|
if ($summary === '') {
|
|
http_status(400);
|
|
echo json_encode(['ok' => false, 'error' => 'Summary is required']);
|
|
return;
|
|
}
|
|
if (!ChronoID::isIsoDate($dateIso)) {
|
|
http_status(400);
|
|
echo json_encode(['ok' => false, 'error' => 'Invalid date']);
|
|
return;
|
|
}
|
|
|
|
$slots = CalendarSlot::loadAll($this);
|
|
$slot = $slots[$slotKey] ?? null;
|
|
if ($slot === null || !$slot->isEnabled()) {
|
|
http_status(400);
|
|
echo json_encode(['ok' => false, 'error' => 'Invalid calendar slot']);
|
|
return;
|
|
}
|
|
|
|
$file = $slot->getFile();
|
|
if ($file === '' || !is_file($file)) {
|
|
http_status(400);
|
|
echo json_encode(['ok' => false, 'error' => 'No local file for this slot']);
|
|
return;
|
|
}
|
|
|
|
$eventData = [
|
|
'summary' => $summary,
|
|
'date' => $dateIso,
|
|
'allDay' => $INPUT->bool('allday'),
|
|
'startTime' => $INPUT->str('start_time'),
|
|
'endTime' => $INPUT->str('end_time'),
|
|
'location' => trim($INPUT->str('location')),
|
|
'description' => trim($INPUT->str('description')),
|
|
];
|
|
|
|
$ok = IcsWriter::editEvent($file, $uid, $recurrence, $eventData, $scope);
|
|
if (!$ok) {
|
|
http_status(500);
|
|
echo json_encode(['ok' => false, 'error' => 'Failed to update event']);
|
|
return;
|
|
}
|
|
|
|
CalendarService::clearCache();
|
|
|
|
// CalDAV write-back if configured
|
|
$remoteOk = true;
|
|
$remoteError = '';
|
|
if ($slot->hasRemoteSource()) {
|
|
$remoteOk = $this->pushEventToCalDav($slot, $file, $uid);
|
|
if (!$remoteOk) {
|
|
$remoteError = 'Local event updated, but CalDAV upload failed.';
|
|
}
|
|
}
|
|
|
|
echo json_encode([
|
|
'ok' => true,
|
|
'message' => 'Event updated.',
|
|
'remoteOk' => $remoteOk,
|
|
'remoteError' => $remoteError,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Handle event deletion.
|
|
*
|
|
* @param \dokuwiki\Input\Input $INPUT
|
|
* @return void
|
|
*/
|
|
protected function handleEventDelete($INPUT): void
|
|
{
|
|
$uid = $INPUT->str('uid');
|
|
$recurrence = $INPUT->str('recurrence');
|
|
$slotKey = $INPUT->str('slot');
|
|
$dateIso = $INPUT->str('date');
|
|
$scope = $INPUT->str('scope');
|
|
|
|
if ($uid === '') {
|
|
http_status(400);
|
|
echo json_encode(['ok' => false, 'error' => 'Missing event UID']);
|
|
return;
|
|
}
|
|
if (!in_array($scope, ['all', 'this', 'future'], true)) {
|
|
$scope = 'all';
|
|
}
|
|
|
|
$slots = CalendarSlot::loadAll($this);
|
|
$slot = $slots[$slotKey] ?? null;
|
|
if ($slot === null || !$slot->isEnabled()) {
|
|
http_status(400);
|
|
echo json_encode(['ok' => false, 'error' => 'Invalid calendar slot']);
|
|
return;
|
|
}
|
|
|
|
$file = $slot->getFile();
|
|
if ($file === '' || !is_file($file)) {
|
|
http_status(400);
|
|
echo json_encode(['ok' => false, 'error' => 'No local file for this slot']);
|
|
return;
|
|
}
|
|
|
|
$ok = IcsWriter::deleteEvent($file, $uid, $recurrence, $dateIso, $scope);
|
|
if (!$ok) {
|
|
http_status(500);
|
|
echo json_encode(['ok' => false, 'error' => 'Failed to delete event']);
|
|
return;
|
|
}
|
|
|
|
CalendarService::clearCache();
|
|
|
|
// CalDAV write-back: push updated file for this UID
|
|
$remoteOk = true;
|
|
$remoteError = '';
|
|
if ($slot->hasRemoteSource()) {
|
|
if ($scope === 'all') {
|
|
$remoteOk = $this->deleteEventFromCalDav($slot, $uid);
|
|
} else {
|
|
$remoteOk = $this->pushEventToCalDav($slot, $file, $uid);
|
|
}
|
|
if (!$remoteOk) {
|
|
$remoteError = 'Local event deleted, but CalDAV update failed.';
|
|
}
|
|
}
|
|
|
|
echo json_encode([
|
|
'ok' => true,
|
|
'message' => 'Event deleted.',
|
|
'remoteOk' => $remoteOk,
|
|
'remoteError' => $remoteError,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Push a single event to CalDAV by reading it from the local file
|
|
* and PUTting it to the server.
|
|
*
|
|
* @param CalendarSlot $slot
|
|
* @param string $file
|
|
* @param string $uid
|
|
* @return bool
|
|
*/
|
|
protected function pushEventToCalDav(CalendarSlot $slot, string $file, string $uid): bool
|
|
{
|
|
try {
|
|
$raw = @file_get_contents($file);
|
|
if (!is_string($raw) || trim($raw) === '') return false;
|
|
|
|
$calendar = \Sabre\VObject\Reader::read($raw, \Sabre\VObject\Reader::OPTION_FORGIVING);
|
|
if (!($calendar instanceof \Sabre\VObject\Component\VCalendar)) return false;
|
|
|
|
// Extract just the components for this UID into a new calendar
|
|
$eventCal = new \Sabre\VObject\Component\VCalendar();
|
|
$eventCal->PRODID = '-//LuxTools DokuWiki Plugin//EN';
|
|
$found = false;
|
|
|
|
// Copy relevant VTIMEZONE
|
|
foreach ($calendar->select('VTIMEZONE') as $tz) {
|
|
$eventCal->add(clone $tz);
|
|
}
|
|
|
|
foreach ($calendar->select('VEVENT') as $component) {
|
|
if (trim((string)($component->UID ?? '')) === $uid) {
|
|
$eventCal->add(clone $component);
|
|
$found = true;
|
|
}
|
|
}
|
|
|
|
if (!$found) return false;
|
|
|
|
$icsData = $eventCal->serialize();
|
|
|
|
// Find existing object on server or create new
|
|
$objectInfo = CalDavClient::findObjectByUidPublic(
|
|
$slot->getCaldavUrl(),
|
|
$slot->getUsername(),
|
|
$slot->getPassword(),
|
|
$uid
|
|
);
|
|
|
|
if ($objectInfo !== null) {
|
|
// Update existing object
|
|
$error = CalDavClient::putCalendarObjectPublic(
|
|
$objectInfo['href'],
|
|
$slot->getUsername(),
|
|
$slot->getPassword(),
|
|
$icsData,
|
|
$objectInfo['etag']
|
|
);
|
|
return $error === '';
|
|
}
|
|
|
|
// Create new object
|
|
$href = rtrim($slot->getCaldavUrl(), '/') . '/' . $uid . '.ics';
|
|
$error = CalDavClient::putCalendarObjectPublic(
|
|
$href,
|
|
$slot->getUsername(),
|
|
$slot->getPassword(),
|
|
$icsData,
|
|
''
|
|
);
|
|
return $error === '';
|
|
} catch (\Throwable $e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete an event from CalDAV by UID.
|
|
*
|
|
* @param CalendarSlot $slot
|
|
* @param string $uid
|
|
* @return bool
|
|
*/
|
|
protected function deleteEventFromCalDav(CalendarSlot $slot, string $uid): bool
|
|
{
|
|
try {
|
|
$objectInfo = CalDavClient::findObjectByUidPublic(
|
|
$slot->getCaldavUrl(),
|
|
$slot->getUsername(),
|
|
$slot->getPassword(),
|
|
$uid
|
|
);
|
|
|
|
if ($objectInfo === null) return true; // Already gone
|
|
|
|
return CalDavClient::deleteCalendarObject(
|
|
$objectInfo['href'],
|
|
$slot->getUsername(),
|
|
$slot->getPassword(),
|
|
$objectInfo['etag']
|
|
);
|
|
} catch (\Throwable $e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build wiki bullet list for local calendar events.
|
|
*
|
|
* @param string $dateIso
|
|
* @return string
|
|
*/
|
|
protected function buildChronologicalEventsWiki(string $dateIso): string
|
|
{
|
|
$slots = CalendarSlot::loadEnabled($this);
|
|
if ($slots === []) return '';
|
|
|
|
$events = CalendarService::eventsForDate($slots, $dateIso);
|
|
if ($events === []) return '';
|
|
|
|
$lines = [];
|
|
foreach ($events as $event) {
|
|
$summary = str_replace(["\n", "\r"], ' ', $event->summary);
|
|
|
|
if ($event->allDay || $event->time === '') {
|
|
$lines[] = ' * ' . $summary;
|
|
} else {
|
|
$lines[] = ' * ' . $event->time . ' - ' . $summary;
|
|
}
|
|
}
|
|
|
|
return implode("\n", $lines);
|
|
}
|
|
|
|
/**
|
|
* Add custom toolbar button for code blocks.
|
|
*
|
|
* @param Event $event
|
|
* @param mixed $param
|
|
* @return void
|
|
*/
|
|
public function addToolbarButton(Event $event, $param)
|
|
{
|
|
$event->data[] = [
|
|
"type" => "format",
|
|
"title" => $this->getLang("toolbar_code_title"),
|
|
"icon" => "../../plugins/luxtools/images/code.png",
|
|
"key" => "C",
|
|
"open" => "<code>",
|
|
"sample" => $this->getLang("toolbar_code_sample"),
|
|
"close" => "</code>",
|
|
"block" => false,
|
|
];
|
|
|
|
// Date Fix: normalize selected timestamp
|
|
$event->data[] = [
|
|
"type" => "LuxtoolsDatefix",
|
|
"title" => $this->getLang("toolbar_datefix_title"),
|
|
"icon" => "../../plugins/luxtools/images/date-fix.svg",
|
|
"key" => "t",
|
|
"block" => false,
|
|
];
|
|
|
|
// Date Fix All: normalize all timestamps on page
|
|
$event->data[] = [
|
|
"type" => "LuxtoolsDatefixAll",
|
|
"title" => $this->getLang("toolbar_datefix_all_title"),
|
|
"icon" => "../../plugins/luxtools/images/date-fix-all.svg",
|
|
"block" => false,
|
|
];
|
|
|
|
// Movie Import: fetch movie metadata from OMDb
|
|
$event->data[] = [
|
|
"type" => "LuxtoolsMovieImport",
|
|
"title" => $this->getLang("toolbar_movie_title"),
|
|
"icon" => "../../plugins/luxtools/images/movie.svg",
|
|
"block" => false,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Add admin-only page-tools item to invalidate luxtools caches.
|
|
*
|
|
* @param Event $event
|
|
* @param mixed $param
|
|
* @return void
|
|
*/
|
|
public function addInvalidateCacheMenuItem(Event $event, $param)
|
|
{
|
|
if (!is_array($event->data)) return;
|
|
if (($event->data['view'] ?? '') !== 'page') return;
|
|
if (!function_exists('auth_isadmin') || !auth_isadmin()) return;
|
|
if (!isset($event->data['items']) || !is_array($event->data['items'])) return;
|
|
|
|
$label = (string)$this->getLang('cache_invalidate_button');
|
|
if ($label === '') $label = 'Invalidate Cache';
|
|
|
|
$title = (string)$this->getLang('cache_invalidate_button_title');
|
|
if ($title === '') $title = 'Invalidate luxtools cache for this page';
|
|
|
|
$event->data['items'][] = new InvalidateCache($label, $title);
|
|
}
|
|
|
|
/**
|
|
* Handle manual cache invalidation action.
|
|
*
|
|
* @param Event $event
|
|
* @param mixed $param
|
|
* @return void
|
|
*/
|
|
public function handleInvalidateCacheAction(Event $event, $param)
|
|
{
|
|
if (!is_string($event->data) || $event->data !== 'show') return;
|
|
|
|
global $INPUT;
|
|
if (!$INPUT->bool('luxtools_invalidate_cache')) return;
|
|
|
|
global $ID;
|
|
$id = is_string($ID) ? $ID : '';
|
|
if (function_exists('cleanID')) {
|
|
$id = (string)cleanID($id);
|
|
}
|
|
|
|
if (!function_exists('auth_isadmin') || !auth_isadmin()) {
|
|
$message = (string)$this->getLang('cache_invalidate_denied');
|
|
if ($message === '') $message = 'Only admins can invalidate cache.';
|
|
msg($message, -1);
|
|
$this->redirectToShow($id);
|
|
return;
|
|
}
|
|
|
|
if (!checkSecurityToken()) {
|
|
$message = (string)$this->getLang('cache_invalidate_badtoken');
|
|
if ($message === '') $message = 'Security token mismatch. Please retry.';
|
|
msg($message, -1);
|
|
$this->redirectToShow($id);
|
|
return;
|
|
}
|
|
|
|
$result = CacheInvalidation::purgeSelected(
|
|
$INPUT->bool('luxtools_purge_pagelinks'),
|
|
$INPUT->bool('luxtools_purge_thumbs')
|
|
);
|
|
|
|
$parts = [];
|
|
|
|
$dokuwikiMsg = (string)$this->getLang('cache_invalidate_success');
|
|
if ($dokuwikiMsg === '') $dokuwikiMsg = 'DokuWiki cache invalidated.';
|
|
$parts[] = $dokuwikiMsg . ' (' . $result['dokuwiki'] . ')';
|
|
|
|
if ($result['pagelinks'] !== null) {
|
|
$msg = (string)$this->getLang('cache_purge_pagelinks_success');
|
|
if ($msg === '') $msg = 'Pagelinks cache purged.';
|
|
$parts[] = $msg . ' (' . $result['pagelinks'] . ')';
|
|
}
|
|
|
|
if ($result['thumbs'] !== null) {
|
|
$msg = (string)$this->getLang('cache_purge_thumbs_success');
|
|
if ($msg === '') $msg = 'Thumbnail cache purged.';
|
|
$parts[] = $msg . ' (' . $result['thumbs'] . ')';
|
|
}
|
|
|
|
msg(implode(' ', $parts), 1);
|
|
|
|
$this->redirectToShow($id);
|
|
}
|
|
|
|
/**
|
|
* Redirect to normal show view for the given page.
|
|
*
|
|
* @param string $id
|
|
* @return void
|
|
*/
|
|
protected function redirectToShow(string $id): void
|
|
{
|
|
$params = ['do' => 'show'];
|
|
send_redirect(wl($id, $params, true, '&'));
|
|
}
|
|
|
|
/**
|
|
* Send no-store cache headers for highly dynamic responses.
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function sendNoStoreHeaders(): void
|
|
{
|
|
if (headers_sent()) return;
|
|
|
|
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
|
header('Pragma: no-cache');
|
|
header('Expires: 0');
|
|
}
|
|
}
|