From 1b803f077b129b0724497199677c61eed3d0443b Mon Sep 17 00:00:00 2001 From: Adrian Stephens Date: Fri, 3 Apr 2026 10:52:15 +0100 Subject: [PATCH] Improve Thunderbird sync deletes and gate CalDAV tracing by diagnostics --- code/src/Plugin.php | 129 +++++++++++++++++- .../calendar-plugin-0.1.15.manifest.sha256 | 2 +- package/calendar-plugin-0.1.15.zip | Bin 53958 -> 54859 bytes .../staging/calendar-plugin/src/Plugin.php | 129 +++++++++++++++++- 4 files changed, 249 insertions(+), 11 deletions(-) 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 d501ca0a4ed68042cf6228cdcafb4ecc6217a784..1c3630852b410ac01426fa7bfb889d477ac5c2e8 100644 GIT binary patch delta 20843 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 L63-u3f8c)r$Mw0H delta 19922 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 z#e#cafa(9rq6#i7xresolveCalDavUserForRequest(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'])) {