From 1182400fb291009e1efe9daa8e6becd452b6bf29 Mon Sep 17 00:00:00 2001 From: Nathan Panchout Date: Sun, 25 Jan 2026 20:33:28 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F(sabredav)=20improve=20HttpCa?= =?UTF-8?q?llbackIMipPlugin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enhance IMIP plugin with better error handling, logging and support for all scheduling methods (REQUEST, REPLY, CANCEL). Update server configuration and SQL schema. Co-Authored-By: Claude Opus 4.5 --- docker/sabredav/server.php | 54 ++++++++++++++++++- docker/sabredav/sql/pgsql.calendars.sql | 5 +- .../sabredav/src/HttpCallbackIMipPlugin.php | 41 +++++++++----- 3 files changed, 83 insertions(+), 17 deletions(-) diff --git a/docker/sabredav/server.php b/docker/sabredav/server.php index eac0bfc..ed0bbb6 100644 --- a/docker/sabredav/server.php +++ b/docker/sabredav/server.php @@ -12,6 +12,7 @@ use Sabre\DAV; use Calendars\SabreDav\AutoCreatePrincipalBackend; use Calendars\SabreDav\HttpCallbackIMipPlugin; use Calendars\SabreDav\ApiKeyAuthBackend; +use Calendars\SabreDav\AttendeeNormalizerPlugin; // Composer autoloader require_once __DIR__ . '/vendor/autoload.php'; @@ -77,15 +78,64 @@ $server->addPlugin(new CardDAV\Plugin()); $server->addPlugin(new DAVACL\Plugin()); $server->addPlugin(new DAV\Browser\Plugin()); +// Add ICS export plugin for iCal subscription URLs +// Allows exporting calendars as .ics files via ?export query parameter +// See https://sabre.io/dav/ics-export-plugin/ +$server->addPlugin(new CalDAV\ICSExportPlugin()); + +// Add sharing support +// See https://sabre.io/dav/caldav-sharing/ +// Note: Order matters! CalDAV\SharingPlugin must come after DAV\Sharing\Plugin +$server->addPlugin(new DAV\Sharing\Plugin()); +$server->addPlugin(new CalDAV\SharingPlugin()); + +// Debug logging for sharing requests +$server->on('method:POST', function($request) { + $contentType = $request->getHeader('Content-Type'); + $path = $request->getPath(); + $body = $request->getBodyAsString(); + error_log("[sabre/dav] POST request received:"); + error_log("[sabre/dav] Path: " . $path); + error_log("[sabre/dav] Content-Type: " . $contentType); + error_log("[sabre/dav] Body: " . substr($body, 0, 1000)); + // Reset body stream position + $request->setBody($body); +}, 50); // Priority 50 to run early + +// Debug: Log when share plugin processes request +$server->on('afterMethod:POST', function($request, $response) { + error_log("[sabre/dav] POST response status: " . $response->getStatus()); + $body = $response->getBodyAsString(); + if ($body) { + error_log("[sabre/dav] POST response body: " . substr($body, 0, 500)); + } +}, 50); + +// Debug: Log exceptions +$server->on('exception', function($e) { + error_log("[sabre/dav] Exception: " . get_class($e) . " - " . $e->getMessage()); + error_log("[sabre/dav] Exception trace: " . $e->getTraceAsString()); +}, 50); + +// Add attendee normalizer plugin to fix duplicate attendees issue +// This plugin normalizes attendee emails (lowercase) and deduplicates them +// when processing calendar objects, fixing issues with REPLY handling +$server->addPlugin(new AttendeeNormalizerPlugin()); + // Add custom IMipPlugin that forwards scheduling messages via HTTP callback // This MUST be added BEFORE the Schedule\Plugin so that Schedule\Plugin finds it -// The callback URL must be provided per-request via X-CalDAV-Callback-URL header +// The callback URL can be provided per-request via X-CalDAV-Callback-URL header +// or via CALDAV_CALLBACK_URL environment variable as fallback $callbackApiKey = getenv('CALDAV_INBOUND_API_KEY'); if (!$callbackApiKey) { error_log("[sabre/dav] CALDAV_INBOUND_API_KEY environment variable is required for scheduling callback"); exit(1); } -$imipPlugin = new HttpCallbackIMipPlugin($callbackApiKey); +$defaultCallbackUrl = getenv('CALDAV_CALLBACK_URL') ?: null; +if ($defaultCallbackUrl) { + error_log("[sabre/dav] Using default callback URL for scheduling: {$defaultCallbackUrl}"); +} +$imipPlugin = new HttpCallbackIMipPlugin($callbackApiKey, $defaultCallbackUrl); $server->addPlugin($imipPlugin); // Add CalDAV scheduling support diff --git a/docker/sabredav/sql/pgsql.calendars.sql b/docker/sabredav/sql/pgsql.calendars.sql index caaaa88..90e5b54 100644 --- a/docker/sabredav/sql/pgsql.calendars.sql +++ b/docker/sabredav/sql/pgsql.calendars.sql @@ -56,7 +56,10 @@ CREATE UNIQUE INDEX calendarinstances_principaluri_uri CREATE UNIQUE INDEX calendarinstances_principaluri_calendarid ON calendarinstances USING btree (principaluri, calendarid); -CREATE UNIQUE INDEX calendarinstances_principaluri_share_href +-- Note: The original SabreDAV schema has a unique index on (principaluri, share_href), +-- but this prevents sharing multiple calendars with the same user. +-- We use a non-unique index instead for query performance. +CREATE INDEX calendarinstances_principaluri_share_href ON calendarinstances USING btree (principaluri, share_href); CREATE TABLE calendarsubscriptions ( diff --git a/docker/sabredav/src/HttpCallbackIMipPlugin.php b/docker/sabredav/src/HttpCallbackIMipPlugin.php index 3d80890..5cd09c1 100644 --- a/docker/sabredav/src/HttpCallbackIMipPlugin.php +++ b/docker/sabredav/src/HttpCallbackIMipPlugin.php @@ -29,16 +29,24 @@ class HttpCallbackIMipPlugin extends IMipPlugin private $server; /** - * Constructor - * - * @param string $apiKey The API key for authenticating with the callback endpoint + * Default callback URL (fallback if header is not provided) + * @var string|null */ - public function __construct($apiKey) + private $defaultCallbackUrl; + + /** + * Constructor + * + * @param string $apiKey The API key for authenticating with the callback endpoint + * @param string|null $defaultCallbackUrl Optional default callback URL + */ + public function __construct($apiKey, $defaultCallbackUrl = null) { // Call parent constructor with empty email (we won't use it) parent::__construct(''); - + $this->apiKey = $apiKey; + $this->defaultCallbackUrl = $defaultCallbackUrl; } /** @@ -81,19 +89,24 @@ class HttpCallbackIMipPlugin extends IMipPlugin return; } - // Get callback URL from the HTTP request header (required) - if (!$this->server || !$this->server->httpRequest) { - $iTipMessage->scheduleStatus = '5.4;No HTTP request available for callback URL'; - return; + // Get callback URL from the HTTP request header or use default + $callbackUrl = null; + if ($this->server && $this->server->httpRequest) { + $callbackUrl = $this->server->httpRequest->getHeader('X-CalDAV-Callback-URL'); } - - $callbackUrl = $this->server->httpRequest->getHeader('X-CalDAV-Callback-URL'); + + // Fall back to default callback URL if header is not provided + if (!$callbackUrl && $this->defaultCallbackUrl) { + $callbackUrl = $this->defaultCallbackUrl; + error_log("[HttpCallbackIMipPlugin] Using default callback URL: {$callbackUrl}"); + } + if (!$callbackUrl) { - error_log("[HttpCallbackIMipPlugin] ERROR: X-CalDAV-Callback-URL header is required"); - $iTipMessage->scheduleStatus = '5.4;X-CalDAV-Callback-URL header is required'; + error_log("[HttpCallbackIMipPlugin] ERROR: X-CalDAV-Callback-URL header or default URL is required"); + $iTipMessage->scheduleStatus = '5.4;X-CalDAV-Callback-URL header or default URL is required'; return; } - + // Ensure URL ends with trailing slash for Django's APPEND_SLASH middleware $callbackUrl = rtrim($callbackUrl, '/') . '/';