♻️(sabredav) improve HttpCallbackIMipPlugin
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 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ use Sabre\DAV;
|
|||||||
use Calendars\SabreDav\AutoCreatePrincipalBackend;
|
use Calendars\SabreDav\AutoCreatePrincipalBackend;
|
||||||
use Calendars\SabreDav\HttpCallbackIMipPlugin;
|
use Calendars\SabreDav\HttpCallbackIMipPlugin;
|
||||||
use Calendars\SabreDav\ApiKeyAuthBackend;
|
use Calendars\SabreDav\ApiKeyAuthBackend;
|
||||||
|
use Calendars\SabreDav\AttendeeNormalizerPlugin;
|
||||||
|
|
||||||
// Composer autoloader
|
// Composer autoloader
|
||||||
require_once __DIR__ . '/vendor/autoload.php';
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
@@ -77,15 +78,64 @@ $server->addPlugin(new CardDAV\Plugin());
|
|||||||
$server->addPlugin(new DAVACL\Plugin());
|
$server->addPlugin(new DAVACL\Plugin());
|
||||||
$server->addPlugin(new DAV\Browser\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
|
// Add custom IMipPlugin that forwards scheduling messages via HTTP callback
|
||||||
// This MUST be added BEFORE the Schedule\Plugin so that Schedule\Plugin finds it
|
// 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');
|
$callbackApiKey = getenv('CALDAV_INBOUND_API_KEY');
|
||||||
if (!$callbackApiKey) {
|
if (!$callbackApiKey) {
|
||||||
error_log("[sabre/dav] CALDAV_INBOUND_API_KEY environment variable is required for scheduling callback");
|
error_log("[sabre/dav] CALDAV_INBOUND_API_KEY environment variable is required for scheduling callback");
|
||||||
exit(1);
|
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);
|
$server->addPlugin($imipPlugin);
|
||||||
|
|
||||||
// Add CalDAV scheduling support
|
// Add CalDAV scheduling support
|
||||||
|
|||||||
@@ -56,7 +56,10 @@ CREATE UNIQUE INDEX calendarinstances_principaluri_uri
|
|||||||
CREATE UNIQUE INDEX calendarinstances_principaluri_calendarid
|
CREATE UNIQUE INDEX calendarinstances_principaluri_calendarid
|
||||||
ON calendarinstances USING btree (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);
|
ON calendarinstances USING btree (principaluri, share_href);
|
||||||
|
|
||||||
CREATE TABLE calendarsubscriptions (
|
CREATE TABLE calendarsubscriptions (
|
||||||
|
|||||||
@@ -29,16 +29,24 @@ class HttpCallbackIMipPlugin extends IMipPlugin
|
|||||||
private $server;
|
private $server;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Default callback URL (fallback if header is not provided)
|
||||||
*
|
* @var string|null
|
||||||
* @param string $apiKey The API key for authenticating with the callback endpoint
|
|
||||||
*/
|
*/
|
||||||
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)
|
// Call parent constructor with empty email (we won't use it)
|
||||||
parent::__construct('');
|
parent::__construct('');
|
||||||
|
|
||||||
$this->apiKey = $apiKey;
|
$this->apiKey = $apiKey;
|
||||||
|
$this->defaultCallbackUrl = $defaultCallbackUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,19 +89,24 @@ class HttpCallbackIMipPlugin extends IMipPlugin
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get callback URL from the HTTP request header (required)
|
// Get callback URL from the HTTP request header or use default
|
||||||
if (!$this->server || !$this->server->httpRequest) {
|
$callbackUrl = null;
|
||||||
$iTipMessage->scheduleStatus = '5.4;No HTTP request available for callback URL';
|
if ($this->server && $this->server->httpRequest) {
|
||||||
return;
|
$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) {
|
if (!$callbackUrl) {
|
||||||
error_log("[HttpCallbackIMipPlugin] ERROR: 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 is required';
|
$iTipMessage->scheduleStatus = '5.4;X-CalDAV-Callback-URL header or default URL is required';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure URL ends with trailing slash for Django's APPEND_SLASH middleware
|
// Ensure URL ends with trailing slash for Django's APPEND_SLASH middleware
|
||||||
$callbackUrl = rtrim($callbackUrl, '/') . '/';
|
$callbackUrl = rtrim($callbackUrl, '/') . '/';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user