Calendar V3

This commit is contained in:
2026-03-11 13:15:20 +01:00
parent a4815fc672
commit 94215fdd65
13 changed files with 190 additions and 61 deletions

View File

@@ -20,6 +20,9 @@ class CalDavClient
/** @var int HTTP timeout in seconds */
protected const TIMEOUT = 30;
/** @var string Last request error message for diagnostics */
protected static string $lastRequestError = '';
/**
* Update the STATUS of a specific event or task on the remote CalDAV server.
*
@@ -33,7 +36,7 @@ class CalDavClient
* @param string $recurrenceId Recurrence ID (empty for non-recurring)
* @param string $newStatus New status value (e.g. COMPLETED, TODO)
* @param string $dateIso Occurrence date YYYY-MM-DD
* @return bool True if the remote update succeeded
* @return string Empty string on success, error message on failure
*/
public static function updateEventStatus(
string $caldavUrl,
@@ -43,13 +46,17 @@ class CalDavClient
string $recurrenceId,
string $newStatus,
string $dateIso
): bool {
if ($caldavUrl === '' || $uid === '') return false;
): string {
if ($caldavUrl === '' || $uid === '') return 'Missing CalDAV URL or UID';
try {
// Find the calendar object href for this UID via REPORT
$objectInfo = self::findObjectByUid($caldavUrl, $username, $password, $uid);
if ($objectInfo === null) return false;
if ($objectInfo === null) {
$msg = "CalDAV: Could not find object with UID '$uid' on server";
dbglog($msg);
return $msg;
}
$objectHref = $objectInfo['href'];
$etag = $objectInfo['etag'];
@@ -57,19 +64,35 @@ class CalDavClient
// Parse and update the status
$calendar = Reader::read($calendarData, Reader::OPTION_FORGIVING);
if (!($calendar instanceof VCalendar)) return false;
if (!($calendar instanceof VCalendar)) {
$msg = "CalDAV: Failed to parse calendar data for UID '$uid'";
dbglog($msg);
return $msg;
}
$updated = IcsWriter::applyStatusUpdateToCalendar(
$calendar, $uid, $recurrenceId, $newStatus, $dateIso
);
if (!$updated) return false;
if (!$updated) {
$msg = "CalDAV: applyStatusUpdateToCalendar failed for UID '$uid'";
dbglog($msg);
return $msg;
}
$newData = $calendar->serialize();
// PUT the updated object back with If-Match for conflict detection
return self::putCalendarObject($objectHref, $username, $password, $newData, $etag);
$putError = self::putCalendarObject($objectHref, $username, $password, $newData, $etag);
if ($putError !== '') {
dbglog($putError);
return $putError;
}
return '';
} catch (Throwable $e) {
return false;
$msg = 'CalDAV: Exception during updateEventStatus: ' . $e->getMessage();
dbglog($msg);
return $msg;
}
}
@@ -243,7 +266,7 @@ class CalDavClient
* @param string $password
* @param string $data ICS data to write
* @param string $etag ETag for If-Match header (empty to skip)
* @return bool
* @return string Empty string on success, error message on failure
*/
protected static function putCalendarObject(
string $href,
@@ -251,7 +274,7 @@ class CalDavClient
string $password,
string $data,
string $etag
): bool {
): string {
$headers = [
'Content-Type: text/calendar; charset=utf-8',
];
@@ -260,9 +283,10 @@ class CalDavClient
}
$response = self::request('PUT', $href, $username, $password, $data, $headers);
// PUT returns null body but we check by HTTP status via the request method
// A successful PUT returns 2xx
return $response !== null;
if ($response === null) {
return self::$lastRequestError ?: 'CalDAV PUT failed (unknown error)';
}
return '';
}
/**
@@ -293,8 +317,6 @@ class CalDavClient
$etags = $resp->xpath('.//d:getetag');
$etag = ($etags && count($etags) > 0) ? trim((string)$etags[0]) : '';
// Strip surrounding quotes from etag if present
$etag = trim($etag, '"');
$caldata = $resp->xpath('.//cal:calendar-data');
$data = ($caldata && count($caldata) > 0) ? trim((string)$caldata[0]) : '';
@@ -392,10 +414,18 @@ class CalDavClient
string $body = '',
array $headers = []
): ?string {
if (!function_exists('curl_init')) return null;
self::$lastRequestError = '';
if (!function_exists('curl_init')) {
self::$lastRequestError = 'CalDAV: curl extension not available';
return null;
}
$ch = curl_init();
if ($ch === false) return null;
if ($ch === false) {
self::$lastRequestError = 'CalDAV: curl_init() failed';
return null;
}
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
@@ -423,9 +453,13 @@ class CalDavClient
// Capture HTTP status code
$responseBody = curl_exec($ch);
$httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if (!is_string($responseBody)) return null;
if (!is_string($responseBody)) {
self::$lastRequestError = "CalDAV $method failed: curl error: $curlError";
return null;
}
// Accept 2xx and 207 (multistatus) responses
if ($httpCode >= 200 && $httpCode < 300) {
@@ -435,6 +469,7 @@ class CalDavClient
return $responseBody;
}
self::$lastRequestError = "CalDAV $method failed: HTTP $httpCode";
return null;
}
}

View File

@@ -6,13 +6,17 @@ namespace dokuwiki\plugin\luxtools;
* Represents one calendar slot configuration.
*
* Each slot has a stable key, a human-readable label, local/remote source
* configuration, a display color, and a derived enabled state.
* configuration, a display color, an optional widget indicator position,
* and a derived enabled state.
*/
class CalendarSlot
{
/** @var string[] Ordered list of all supported slot keys */
public const SLOT_KEYS = ['general', 'maintenance', 'slot3', 'slot4'];
/** @var string[] Allowed widget indicator display positions */
public const INDICATOR_DISPLAYS = ['none', 'top-left', 'top-right', 'bottom-left', 'bottom-right'];
/** @var array<string,string> Human-readable labels for slot keys */
public const SLOT_LABELS = [
'general' => 'General',
@@ -42,6 +46,9 @@ class CalendarSlot
/** @var string CSS color for widget indicators */
protected $color;
/** @var string Widget indicator display position */
protected $display;
/**
* @param string $key
* @param string $file
@@ -49,6 +56,7 @@ class CalendarSlot
* @param string $username
* @param string $password
* @param string $color
* @param string $display
*/
public function __construct(
string $key,
@@ -56,7 +64,8 @@ class CalendarSlot
string $caldavUrl = '',
string $username = '',
string $password = '',
string $color = ''
string $color = '',
string $display = 'none'
) {
$this->key = $key;
$this->label = self::SLOT_LABELS[$key] ?? $key;
@@ -65,6 +74,7 @@ class CalendarSlot
$this->username = trim($username);
$this->password = trim($password);
$this->color = trim($color);
$this->display = self::normalizeIndicatorDisplay($display);
}
public function getKey(): string
@@ -102,6 +112,16 @@ class CalendarSlot
return $this->color;
}
public function getDisplay(): string
{
return $this->display;
}
public function shouldDisplayIndicator(): bool
{
return $this->display !== 'none';
}
/**
* A slot is enabled if it has a local file path or a CalDAV URL.
*
@@ -149,7 +169,8 @@ class CalendarSlot
(string)$plugin->getConf('calendar_' . $key . '_caldav_url'),
(string)$plugin->getConf('calendar_' . $key . '_username'),
(string)$plugin->getConf('calendar_' . $key . '_password'),
(string)$plugin->getConf('calendar_' . $key . '_color')
(string)$plugin->getConf('calendar_' . $key . '_color'),
(string)$plugin->getConf('calendar_' . $key . '_display')
);
}
return $slots;
@@ -168,4 +189,14 @@ class CalendarSlot
return $slot->isEnabled();
});
}
protected static function normalizeIndicatorDisplay(string $display): string
{
$display = strtolower(trim($display));
$display = str_replace(['_', ' '], '-', $display);
if (!in_array($display, self::INDICATOR_DISPLAYS, true)) {
return 'none';
}
return $display;
}
}

View File

@@ -15,6 +15,7 @@ class ChronologicalCalendarWidget
* @param string $baseNs
* @param array<string,string[]> $indicators date => [slotKey, ...] from CalendarService::monthIndicators()
* @param array<string,string> $slotColors slotKey => CSS color
* @param array<string,string> $slotDisplays slotKey => configured indicator position
* @return string
*/
public static function render(
@@ -22,7 +23,8 @@ class ChronologicalCalendarWidget
int $month,
string $baseNs = 'chronological',
array $indicators = [],
array $slotColors = []
array $slotColors = [],
array $slotDisplays = []
): string {
if (!self::isValidMonth($year, $month)) return '';
@@ -120,13 +122,19 @@ class ChronologicalCalendarWidget
// Render slot indicators if any
$dayIndicators = $indicators[$date] ?? [];
if ($dayIndicators !== []) {
$html .= '<div class="luxtools-calendar-indicators">';
$indicatorHtml = '';
foreach ($dayIndicators as $slotKey) {
$display = $slotDisplays[$slotKey] ?? 'none';
if ($display === 'none') continue;
$color = $slotColors[$slotKey] ?? '';
$style = ($color !== '') ? ' style="background-color:' . hsc($color) . '"' : '';
$html .= '<span class="luxtools-calendar-indicator luxtools-indicator-' . hsc($slotKey) . '"' . $style . '></span>';
$indicatorHtml .= '<span class="luxtools-calendar-indicator luxtools-indicator-' . hsc($display) . '"' . $style . '></span>';
}
if ($indicatorHtml !== '') {
$html .= '<div class="luxtools-calendar-indicators">' . $indicatorHtml . '</div>';
}
$html .= '</div>';
}
if ($dayId !== null && function_exists('html_wikilink')) {