diff --git a/code/src/Plugin.php b/code/src/Plugin.php
index 8b573ea..b328951 100644
--- a/code/src/Plugin.php
+++ b/code/src/Plugin.php
@@ -2014,12 +2014,20 @@ HTML
private function serveCalDavPath(string $path, string $method): void
{
+ $userAgent = (string) ($_SERVER['HTTP_USER_AGENT'] ?? '');
$caldavUser = $this->resolveCalDavUserForRequest(null);
+ $this->caldavTrace('request', [
+ 'method' => $method,
+ 'path' => $path,
+ 'user_agent' => $userAgent,
+ 'authorized' => $caldavUser !== null,
+ ]);
if ($caldavUser === null) {
http_response_code(401);
header('WWW-Authenticate: Basic realm="Calendar CalDAV"');
header('Content-Type: application/xml; charset=utf-8');
echo 'auth required';
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 401, 'reason' => 'auth_required']);
return;
}
@@ -2046,6 +2054,7 @@ HTML
header('Allow: OPTIONS, PROPFIND, REPORT, GET, PUT, DELETE, HEAD');
header('DAV: 1, calendar-access');
http_response_code(200);
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 200, 'kind' => 'collection_head']);
return;
}
if (str_starts_with($path, $resourcePrefix) && str_ends_with($path, '.ics')) {
@@ -2053,11 +2062,13 @@ HTML
$obj = $this->calDavService->getObject($resource);
if ($obj === null) {
http_response_code(404);
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 404, 'resource' => $resource]);
return;
}
header('Content-Type: text/calendar; charset=utf-8');
header('ETag: ' . (string) ($obj['etag'] ?? ''));
http_response_code(200);
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 200, 'resource' => $resource]);
return;
}
}
@@ -2078,6 +2089,7 @@ HTML
header('Allow: OPTIONS, PROPFIND, REPORT, GET, PUT, DELETE, HEAD');
header('DAV: 1, calendar-access');
http_response_code(200);
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 200, 'kind' => 'collection_get']);
return;
}
}
@@ -2086,6 +2098,7 @@ HTML
header('Allow: OPTIONS, PROPFIND, REPORT, GET, PUT, DELETE, HEAD');
header('DAV: 1, calendar-access');
http_response_code(200);
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 200, 'kind' => 'options']);
return;
}
@@ -2094,18 +2107,22 @@ HTML
http_response_code(207);
if ($path === $root || $path === $root . '/') {
echo $this->caldavPropfindRootXml($root, $principal, $calendarsRoot, $collection);
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 207, 'kind' => 'propfind_root']);
return;
}
if ($path === $principalCollection || $path === rtrim($principalCollection, '/')) {
echo $this->caldavPropfindPrincipalCollectionXml($principalCollection, $principal);
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 207, 'kind' => 'propfind_principal_collection']);
return;
}
if ($path === $principal || $path === rtrim($principal, '/')) {
echo $this->caldavPropfindPrincipalXml($principal, $calendarsRoot);
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 207, 'kind' => 'propfind_principal']);
return;
}
if ($path === $calendarsRoot || $path === rtrim($calendarsRoot, '/')) {
echo $this->caldavPropfindCalendarsRootXml($calendarsRoot, $collection);
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 207, 'kind' => 'propfind_calendars_root']);
return;
}
if ($path === rtrim($collection, '/')) {
@@ -2113,6 +2130,7 @@ HTML
}
if ($path === $collection) {
echo $this->caldavPropfindCollectionXml($collection, $this->caldavSyncToken(), true);
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 207, 'kind' => 'propfind_collection']);
return;
}
if (str_starts_with($path, $resourcePrefix) && str_ends_with($path, '.ics')) {
@@ -2121,21 +2139,33 @@ HTML
if ($obj === null) {
http_response_code(404);
echo 'not found';
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 404, 'resource' => $resource, 'kind' => 'propfind_object']);
return;
}
echo $this->caldavPropfindObjectXml($collection . $resource, (string) ($obj['etag'] ?? ''));
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 207, 'resource' => $resource, 'kind' => 'propfind_object']);
return;
}
http_response_code(404);
echo 'not found';
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 404, 'kind' => 'propfind_not_found']);
return;
}
if ($method === 'REPORT' && ($path === $collection || $path === rtrim($collection, '/'))) {
$body = (string) file_get_contents('php://input');
+ $reportType = $this->caldavReportType($body);
header('Content-Type: application/xml; charset=utf-8');
http_response_code(207);
echo $this->caldavReportXml($collection, $body, $this->caldavSyncToken());
+ $this->caldavTrace('response', [
+ 'method' => $method,
+ 'path' => $path,
+ 'status' => 207,
+ 'kind' => 'report',
+ 'report_type' => $reportType,
+ 'body_bytes' => strlen($body),
+ ]);
return;
}
@@ -2145,12 +2175,14 @@ HTML
$obj = $this->calDavService->getObject($resource);
if ($obj === null) {
http_response_code(404);
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 404, 'resource' => $resource, 'kind' => 'object_get']);
return;
}
header('Content-Type: text/calendar; charset=utf-8');
header('ETag: ' . (string) ($obj['etag'] ?? ''));
http_response_code(200);
echo (string) ($obj['ics'] ?? '');
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 200, 'resource' => $resource, 'kind' => 'object_get']);
return;
}
if ($method === 'PUT') {
@@ -2169,6 +2201,14 @@ HTML
http_response_code((int) ($error['status'] ?? 500));
header('Content-Type: application/xml; charset=utf-8');
echo '' . esc_html((string) ($error['message'] ?? 'error')) . '';
+ $this->caldavTrace('response', [
+ 'method' => $method,
+ 'path' => $path,
+ 'status' => (int) ($error['status'] ?? 500),
+ 'resource' => $resource,
+ 'kind' => 'object_put',
+ 'body_bytes' => strlen($raw),
+ ]);
return;
}
$status = (int) ($result['status'] ?? 204);
@@ -2177,6 +2217,14 @@ HTML
header('ETag: ' . (string) $event['etag']);
}
http_response_code($status);
+ $this->caldavTrace('response', [
+ 'method' => $method,
+ 'path' => $path,
+ 'status' => $status,
+ 'resource' => $resource,
+ 'kind' => 'object_put',
+ 'body_bytes' => strlen($raw),
+ ]);
return;
}
if ($method === 'DELETE') {
@@ -2184,15 +2232,24 @@ HTML
if (isset($result['error'])) {
$error = (array) $result['error'];
http_response_code((int) ($error['status'] ?? 500));
+ $this->caldavTrace('response', [
+ 'method' => $method,
+ 'path' => $path,
+ 'status' => (int) ($error['status'] ?? 500),
+ 'resource' => $resource,
+ 'kind' => 'object_delete',
+ ]);
return;
}
http_response_code(204);
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 204, 'resource' => $resource, 'kind' => 'object_delete']);
return;
}
}
http_response_code(405);
header('Allow: OPTIONS, PROPFIND, REPORT, GET, PUT, DELETE, HEAD');
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 405, 'reason' => 'method_not_allowed']);
}
private function caldavPropfindRootXml(string $root, string $principal, string $calendarsRoot, string $collection): string
@@ -2319,6 +2376,10 @@ HTML
$bodyLower = strtolower($xmlBody);
$resources = array_map(static fn(array $r): string => (string) ($r['resource'] ?? ''), $this->calDavService->listResources());
$items = [];
+ $reportType = $this->caldavReportType($xmlBody);
+ $includeDeleted = false;
+ $deletedCount = 0;
+ $clientSyncToken = '';
if (str_contains($bodyLower, 'sync-collection')) {
$clientSyncToken = $this->extractSyncCollectionToken($xmlBody);
@@ -2326,9 +2387,18 @@ HTML
if ($clientSyncToken !== '' && $clientSyncToken === $syncToken) {
$items = [];
} else {
- // Fallback implementation: emit current objects for initial/out-of-date tokens.
- // Avoid emitting historical tombstones because many clients treat large 404 sets as transient failures.
+ // Emit current objects for initial/out-of-date tokens.
$items = $this->calDavService->multiget($resources);
+ // For incremental syncs, include a bounded set of deleted hrefs so clients can remove local copies.
+ if ($clientSyncToken !== '') {
+ $includeDeleted = true;
+ // Keep incremental deletes bounded to avoid overwhelming strict clients.
+ $deletedResources = $this->calDavService->listDeletedResources(20);
+ $deletedCount = count($deletedResources);
+ foreach ($deletedResources as $deletedResource) {
+ $items[] = ['resource' => $deletedResource, 'status' => 404];
+ }
+ }
}
} elseif (str_contains($bodyLower, 'calendar-query')) {
$items = $this->calDavService->multiget($resources);
@@ -2359,17 +2429,38 @@ HTML
}
$responses = '';
+ $okCount = 0;
+ $notFoundCount = 0;
foreach ($items as $item) {
$status = (int) ($item['status'] ?? 404);
$resource = (string) ($item['resource'] ?? '');
- $responses .= '' . htmlspecialchars($collection . $resource, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '';
+ $responses .= '' . htmlspecialchars($collection . $resource, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '';
if ($status === 200) {
+ $okCount++;
+ $responses .= '';
$responses .= '' . htmlspecialchars((string) ($item['etag'] ?? ''), ENT_XML1 | ENT_QUOTES, 'UTF-8') . ''
. '' . htmlspecialchars((string) ($item['ics'] ?? ''), ENT_XML1 | ENT_QUOTES, 'UTF-8') . '';
+ $responses .= 'HTTP/1.1 200 OK';
+ } else {
+ $notFoundCount++;
+ // For sync-collection deletions, clients expect bare status (no propstat block).
+ if ($reportType === 'sync-collection') {
+ $responses .= 'HTTP/1.1 404 Not Found';
+ } else {
+ $responses .= 'HTTP/1.1 404 Not Found';
+ }
}
- $responses .= 'HTTP/1.1 ' . $status . ($status === 200 ? ' OK' : ' Not Found')
- . '';
+ $responses .= '';
}
+ $this->caldavTrace('report', [
+ 'report_type' => $reportType,
+ 'client_sync_token_present' => $clientSyncToken !== '',
+ 'client_sync_token_matches' => $clientSyncToken !== '' && $clientSyncToken === $syncToken,
+ 'include_deleted' => $includeDeleted,
+ 'deleted_included_count' => $deletedCount,
+ 'response_ok_count' => $okCount,
+ 'response_not_found_count' => $notFoundCount,
+ ]);
return ''
. htmlspecialchars($syncToken, ENT_XML1 | ENT_QUOTES, 'UTF-8')
. ''
@@ -2425,6 +2516,34 @@ HTML
return $token;
}
+ private function caldavReportType(string $xmlBody): string
+ {
+ $bodyLower = strtolower($xmlBody);
+ if (str_contains($bodyLower, 'sync-collection')) {
+ return 'sync-collection';
+ }
+ if (str_contains($bodyLower, 'calendar-query')) {
+ return 'calendar-query';
+ }
+ if (str_contains($bodyLower, 'calendar-multiget')) {
+ return 'calendar-multiget';
+ }
+ return 'other';
+ }
+
+ private function caldavTrace(string $event, array $context = []): void
+ {
+ if (!$this->isDiagnosticsEnabled()) {
+ return;
+ }
+ $payload = [
+ 'event' => $event,
+ 'at' => gmdate('c'),
+ 'context' => $context,
+ ];
+ error_log('[calendar-plugin][caldav] ' . wp_json_encode($payload));
+ }
+
private function unwrapServiceResult(array $result): array|\WP_Error
{
if (isset($result['error']) && is_array($result['error'])) {
diff --git a/package/calendar-plugin-0.1.15.manifest.sha256 b/package/calendar-plugin-0.1.15.manifest.sha256
index 99df439..6c1e8a9 100644
--- a/package/calendar-plugin-0.1.15.manifest.sha256
+++ b/package/calendar-plugin-0.1.15.manifest.sha256
@@ -15,6 +15,6 @@ e65577c707c5a66e2097faa7720170180c79a9d9df219f0e1061fdcec55be744 ./src/Infrastr
68c0ca15ad2c8b6363a2578b85f8daf0d3a094e612a120a2cdd2a2bfd8fe5e3c ./src/Infrastructure/WordPress/WordPressDatabaseAdapter.php
8da85db3c1e69c2c5f01aaa2f558aa8f0323446d8af0aec5b34d4607cd4afe1b ./src/Infrastructure/WordPress/WordPressHttpAdapter.php
cf9fddcecb07af2c03ad2c0e448be12a6b45dd936efc8bc1fd46a52b4af864ea ./src/Infrastructure/WordPress/WordPressOptionsAdapter.php
-0cc96b252ac86e91a724e73beecfe6f57a1afdb6f5d9a26e44aed00a3f0e20f0 ./src/Plugin.php
+b0e95460e705b9059bf2f197c17f17c51c9bd5492ca69eaa073ebbb808542fe0 ./src/Plugin.php
4e6930c79d9ce1045be6863ddd7546e19f7ef7e839d41a41014a3efb3183eaf0 ./src/bootstrap.php
893c6df62beed87a981d372c473e5012d1b5d1c254d23b39cda44ae8a08cd16c ./uninstall.php
diff --git a/package/calendar-plugin-0.1.15.zip b/package/calendar-plugin-0.1.15.zip
index d501ca0..1c36308 100644
Binary files a/package/calendar-plugin-0.1.15.zip and b/package/calendar-plugin-0.1.15.zip differ
diff --git a/package/staging/calendar-plugin/src/Plugin.php b/package/staging/calendar-plugin/src/Plugin.php
index 8b573ea..b328951 100644
--- a/package/staging/calendar-plugin/src/Plugin.php
+++ b/package/staging/calendar-plugin/src/Plugin.php
@@ -2014,12 +2014,20 @@ HTML
private function serveCalDavPath(string $path, string $method): void
{
+ $userAgent = (string) ($_SERVER['HTTP_USER_AGENT'] ?? '');
$caldavUser = $this->resolveCalDavUserForRequest(null);
+ $this->caldavTrace('request', [
+ 'method' => $method,
+ 'path' => $path,
+ 'user_agent' => $userAgent,
+ 'authorized' => $caldavUser !== null,
+ ]);
if ($caldavUser === null) {
http_response_code(401);
header('WWW-Authenticate: Basic realm="Calendar CalDAV"');
header('Content-Type: application/xml; charset=utf-8');
echo 'auth required';
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 401, 'reason' => 'auth_required']);
return;
}
@@ -2046,6 +2054,7 @@ HTML
header('Allow: OPTIONS, PROPFIND, REPORT, GET, PUT, DELETE, HEAD');
header('DAV: 1, calendar-access');
http_response_code(200);
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 200, 'kind' => 'collection_head']);
return;
}
if (str_starts_with($path, $resourcePrefix) && str_ends_with($path, '.ics')) {
@@ -2053,11 +2062,13 @@ HTML
$obj = $this->calDavService->getObject($resource);
if ($obj === null) {
http_response_code(404);
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 404, 'resource' => $resource]);
return;
}
header('Content-Type: text/calendar; charset=utf-8');
header('ETag: ' . (string) ($obj['etag'] ?? ''));
http_response_code(200);
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 200, 'resource' => $resource]);
return;
}
}
@@ -2078,6 +2089,7 @@ HTML
header('Allow: OPTIONS, PROPFIND, REPORT, GET, PUT, DELETE, HEAD');
header('DAV: 1, calendar-access');
http_response_code(200);
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 200, 'kind' => 'collection_get']);
return;
}
}
@@ -2086,6 +2098,7 @@ HTML
header('Allow: OPTIONS, PROPFIND, REPORT, GET, PUT, DELETE, HEAD');
header('DAV: 1, calendar-access');
http_response_code(200);
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 200, 'kind' => 'options']);
return;
}
@@ -2094,18 +2107,22 @@ HTML
http_response_code(207);
if ($path === $root || $path === $root . '/') {
echo $this->caldavPropfindRootXml($root, $principal, $calendarsRoot, $collection);
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 207, 'kind' => 'propfind_root']);
return;
}
if ($path === $principalCollection || $path === rtrim($principalCollection, '/')) {
echo $this->caldavPropfindPrincipalCollectionXml($principalCollection, $principal);
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 207, 'kind' => 'propfind_principal_collection']);
return;
}
if ($path === $principal || $path === rtrim($principal, '/')) {
echo $this->caldavPropfindPrincipalXml($principal, $calendarsRoot);
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 207, 'kind' => 'propfind_principal']);
return;
}
if ($path === $calendarsRoot || $path === rtrim($calendarsRoot, '/')) {
echo $this->caldavPropfindCalendarsRootXml($calendarsRoot, $collection);
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 207, 'kind' => 'propfind_calendars_root']);
return;
}
if ($path === rtrim($collection, '/')) {
@@ -2113,6 +2130,7 @@ HTML
}
if ($path === $collection) {
echo $this->caldavPropfindCollectionXml($collection, $this->caldavSyncToken(), true);
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 207, 'kind' => 'propfind_collection']);
return;
}
if (str_starts_with($path, $resourcePrefix) && str_ends_with($path, '.ics')) {
@@ -2121,21 +2139,33 @@ HTML
if ($obj === null) {
http_response_code(404);
echo 'not found';
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 404, 'resource' => $resource, 'kind' => 'propfind_object']);
return;
}
echo $this->caldavPropfindObjectXml($collection . $resource, (string) ($obj['etag'] ?? ''));
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 207, 'resource' => $resource, 'kind' => 'propfind_object']);
return;
}
http_response_code(404);
echo 'not found';
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 404, 'kind' => 'propfind_not_found']);
return;
}
if ($method === 'REPORT' && ($path === $collection || $path === rtrim($collection, '/'))) {
$body = (string) file_get_contents('php://input');
+ $reportType = $this->caldavReportType($body);
header('Content-Type: application/xml; charset=utf-8');
http_response_code(207);
echo $this->caldavReportXml($collection, $body, $this->caldavSyncToken());
+ $this->caldavTrace('response', [
+ 'method' => $method,
+ 'path' => $path,
+ 'status' => 207,
+ 'kind' => 'report',
+ 'report_type' => $reportType,
+ 'body_bytes' => strlen($body),
+ ]);
return;
}
@@ -2145,12 +2175,14 @@ HTML
$obj = $this->calDavService->getObject($resource);
if ($obj === null) {
http_response_code(404);
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 404, 'resource' => $resource, 'kind' => 'object_get']);
return;
}
header('Content-Type: text/calendar; charset=utf-8');
header('ETag: ' . (string) ($obj['etag'] ?? ''));
http_response_code(200);
echo (string) ($obj['ics'] ?? '');
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 200, 'resource' => $resource, 'kind' => 'object_get']);
return;
}
if ($method === 'PUT') {
@@ -2169,6 +2201,14 @@ HTML
http_response_code((int) ($error['status'] ?? 500));
header('Content-Type: application/xml; charset=utf-8');
echo '' . esc_html((string) ($error['message'] ?? 'error')) . '';
+ $this->caldavTrace('response', [
+ 'method' => $method,
+ 'path' => $path,
+ 'status' => (int) ($error['status'] ?? 500),
+ 'resource' => $resource,
+ 'kind' => 'object_put',
+ 'body_bytes' => strlen($raw),
+ ]);
return;
}
$status = (int) ($result['status'] ?? 204);
@@ -2177,6 +2217,14 @@ HTML
header('ETag: ' . (string) $event['etag']);
}
http_response_code($status);
+ $this->caldavTrace('response', [
+ 'method' => $method,
+ 'path' => $path,
+ 'status' => $status,
+ 'resource' => $resource,
+ 'kind' => 'object_put',
+ 'body_bytes' => strlen($raw),
+ ]);
return;
}
if ($method === 'DELETE') {
@@ -2184,15 +2232,24 @@ HTML
if (isset($result['error'])) {
$error = (array) $result['error'];
http_response_code((int) ($error['status'] ?? 500));
+ $this->caldavTrace('response', [
+ 'method' => $method,
+ 'path' => $path,
+ 'status' => (int) ($error['status'] ?? 500),
+ 'resource' => $resource,
+ 'kind' => 'object_delete',
+ ]);
return;
}
http_response_code(204);
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 204, 'resource' => $resource, 'kind' => 'object_delete']);
return;
}
}
http_response_code(405);
header('Allow: OPTIONS, PROPFIND, REPORT, GET, PUT, DELETE, HEAD');
+ $this->caldavTrace('response', ['method' => $method, 'path' => $path, 'status' => 405, 'reason' => 'method_not_allowed']);
}
private function caldavPropfindRootXml(string $root, string $principal, string $calendarsRoot, string $collection): string
@@ -2319,6 +2376,10 @@ HTML
$bodyLower = strtolower($xmlBody);
$resources = array_map(static fn(array $r): string => (string) ($r['resource'] ?? ''), $this->calDavService->listResources());
$items = [];
+ $reportType = $this->caldavReportType($xmlBody);
+ $includeDeleted = false;
+ $deletedCount = 0;
+ $clientSyncToken = '';
if (str_contains($bodyLower, 'sync-collection')) {
$clientSyncToken = $this->extractSyncCollectionToken($xmlBody);
@@ -2326,9 +2387,18 @@ HTML
if ($clientSyncToken !== '' && $clientSyncToken === $syncToken) {
$items = [];
} else {
- // Fallback implementation: emit current objects for initial/out-of-date tokens.
- // Avoid emitting historical tombstones because many clients treat large 404 sets as transient failures.
+ // Emit current objects for initial/out-of-date tokens.
$items = $this->calDavService->multiget($resources);
+ // For incremental syncs, include a bounded set of deleted hrefs so clients can remove local copies.
+ if ($clientSyncToken !== '') {
+ $includeDeleted = true;
+ // Keep incremental deletes bounded to avoid overwhelming strict clients.
+ $deletedResources = $this->calDavService->listDeletedResources(20);
+ $deletedCount = count($deletedResources);
+ foreach ($deletedResources as $deletedResource) {
+ $items[] = ['resource' => $deletedResource, 'status' => 404];
+ }
+ }
}
} elseif (str_contains($bodyLower, 'calendar-query')) {
$items = $this->calDavService->multiget($resources);
@@ -2359,17 +2429,38 @@ HTML
}
$responses = '';
+ $okCount = 0;
+ $notFoundCount = 0;
foreach ($items as $item) {
$status = (int) ($item['status'] ?? 404);
$resource = (string) ($item['resource'] ?? '');
- $responses .= '' . htmlspecialchars($collection . $resource, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '';
+ $responses .= '' . htmlspecialchars($collection . $resource, ENT_XML1 | ENT_QUOTES, 'UTF-8') . '';
if ($status === 200) {
+ $okCount++;
+ $responses .= '';
$responses .= '' . htmlspecialchars((string) ($item['etag'] ?? ''), ENT_XML1 | ENT_QUOTES, 'UTF-8') . ''
. '' . htmlspecialchars((string) ($item['ics'] ?? ''), ENT_XML1 | ENT_QUOTES, 'UTF-8') . '';
+ $responses .= 'HTTP/1.1 200 OK';
+ } else {
+ $notFoundCount++;
+ // For sync-collection deletions, clients expect bare status (no propstat block).
+ if ($reportType === 'sync-collection') {
+ $responses .= 'HTTP/1.1 404 Not Found';
+ } else {
+ $responses .= 'HTTP/1.1 404 Not Found';
+ }
}
- $responses .= 'HTTP/1.1 ' . $status . ($status === 200 ? ' OK' : ' Not Found')
- . '';
+ $responses .= '';
}
+ $this->caldavTrace('report', [
+ 'report_type' => $reportType,
+ 'client_sync_token_present' => $clientSyncToken !== '',
+ 'client_sync_token_matches' => $clientSyncToken !== '' && $clientSyncToken === $syncToken,
+ 'include_deleted' => $includeDeleted,
+ 'deleted_included_count' => $deletedCount,
+ 'response_ok_count' => $okCount,
+ 'response_not_found_count' => $notFoundCount,
+ ]);
return ''
. htmlspecialchars($syncToken, ENT_XML1 | ENT_QUOTES, 'UTF-8')
. ''
@@ -2425,6 +2516,34 @@ HTML
return $token;
}
+ private function caldavReportType(string $xmlBody): string
+ {
+ $bodyLower = strtolower($xmlBody);
+ if (str_contains($bodyLower, 'sync-collection')) {
+ return 'sync-collection';
+ }
+ if (str_contains($bodyLower, 'calendar-query')) {
+ return 'calendar-query';
+ }
+ if (str_contains($bodyLower, 'calendar-multiget')) {
+ return 'calendar-multiget';
+ }
+ return 'other';
+ }
+
+ private function caldavTrace(string $event, array $context = []): void
+ {
+ if (!$this->isDiagnosticsEnabled()) {
+ return;
+ }
+ $payload = [
+ 'event' => $event,
+ 'at' => gmdate('c'),
+ 'context' => $context,
+ ];
+ error_log('[calendar-plugin][caldav] ' . wp_json_encode($payload));
+ }
+
private function unwrapServiceResult(array $result): array|\WP_Error
{
if (isset($result['error']) && is_array($result['error'])) {