improve calendar features
This commit is contained in:
453
action.php
453
action.php
@@ -8,6 +8,7 @@ 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;
|
||||
@@ -87,6 +88,18 @@ class action_plugin_luxtools extends ActionPlugin
|
||||
$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",
|
||||
@@ -642,6 +655,15 @@ class action_plugin_luxtools extends ActionPlugin
|
||||
}
|
||||
$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>';
|
||||
@@ -838,37 +860,440 @@ class action_plugin_luxtools extends ActionPlugin
|
||||
return;
|
||||
}
|
||||
|
||||
if (!function_exists('auth_isadmin') || !auth_isadmin()) {
|
||||
if (empty($_SERVER['REMOTE_USER'])) {
|
||||
http_status(403);
|
||||
echo json_encode(['ok' => false, 'error' => 'Admin access required']);
|
||||
echo json_encode(['ok' => false, 'error' => 'Authentication required']);
|
||||
return;
|
||||
}
|
||||
|
||||
$slots = CalendarSlot::loadEnabled($this);
|
||||
$results = [];
|
||||
$hasErrors = false;
|
||||
$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) {
|
||||
if (!$slot->hasRemoteSource()) continue;
|
||||
$result[] = [
|
||||
'key' => $slot->getKey(),
|
||||
'label' => $slot->getLabel(),
|
||||
];
|
||||
}
|
||||
|
||||
$ok = CalDavClient::syncSlot($slot);
|
||||
$results[$slot->getKey()] = $ok;
|
||||
if (!$ok) $hasErrors = true;
|
||||
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();
|
||||
|
||||
$msg = $hasErrors
|
||||
? $this->getLang('calendar_sync_partial')
|
||||
: $this->getLang('calendar_sync_success');
|
||||
// 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' => !$hasErrors,
|
||||
'message' => $msg,
|
||||
'results' => $results,
|
||||
'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.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user