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'])) {