eventRepository->findByCalendarId($config->calendarId); $localEvents = array_values(array_filter( $allLocalEvents, fn (array $event): bool => $config->shouldManageEventForThisRemoteCalendar((string) ($event['caldavCalendarHref'] ?? '')), )); $knownHrefEtags = []; foreach ($localEvents as $localEvent) { $href = trim((string) ($localEvent['caldavHref'] ?? '')); if ('' === $href) { continue; } $knownHrefEtags[$href] = trim((string) ($localEvent['caldavEtag'] ?? '')); } $remoteData = $this->remoteReader->readEvents($config, $knownHrefEtags, false); $remoteEvents = $remoteData->events; $remotePseudoLocalRows = $this->buildRemotePseudoLocalRows($remoteEvents, $remoteData->hrefEtags); $localHrefs = []; $localUids = []; foreach ($localEvents as $localEvent) { if (!$this->isEventWithinConfiguredWindow($localEvent, $config)) { ++$result->skipped; continue; } $targetCalendarUrl = $config->resolveTargetCalendarForLocalEvent((string) ($localEvent['caldavCalendarHref'] ?? '')); if (null === $targetCalendarUrl || $targetCalendarUrl !== $config->caldavUrl) { ++$result->skipped; continue; } $href = (string) ($localEvent['caldavHref'] ?? ''); $uid = (string) ($localEvent['caldavUid'] ?? ''); if ('' !== $href) { $localHrefs[$href] = true; } if ('' !== $uid) { $localUids[$uid] = true; } $localSyncFields = $this->fieldExtractor->extractFromLocalEvent($localEvent, $config->timezoneOrDefault()); $currentHash = $this->hashGenerator->generate($localSyncFields); $storedHash = (string) ($localEvent['caldavSyncHash'] ?? ''); $localChanged = '' === $storedHash || $storedHash !== $currentHash; if (!$localChanged) { ++$result->skipped; continue; } $remoteMatch = $this->matchResolver->resolve($remotePseudoLocalRows, $this->fieldExtractor->toRemoteEvent($localEvent, $config->timezoneOrDefault()), $config->calendarId); $matchingRemoteEvent = $this->resolveRemoteFromMatch($remoteEvents, $remoteMatch); if (null !== $matchingRemoteEvent && '' !== $storedHash) { $remoteSyncFields = $this->fieldExtractor->extractFromRemoteEvent($matchingRemoteEvent); $remoteHash = $this->hashGenerator->generate($remoteSyncFields); if ($remoteHash !== $storedHash) { $localModifiedAt = (int) ($localEvent['tstamp'] ?? 0); $remoteModifiedAt = $matchingRemoteEvent->lastModifiedAt; if ($this->preferLocalVersion($localModifiedAt, $remoteModifiedAt)) { $this->logger->info('CalDAV conflict detected while pushing. Local newer timestamp wins.', [ 'calendarId' => $config->calendarId, 'eventId' => (int) $localEvent['id'], 'href' => $matchingRemoteEvent->href, 'uid' => $matchingRemoteEvent->uid, 'localModifiedAt' => $localModifiedAt, 'remoteModifiedAt' => $remoteModifiedAt, ]); } else { ++$result->conflicts; $this->logger->warning('CalDAV conflict detected while pushing. Remote newer timestamp wins.', [ 'calendarId' => $config->calendarId, 'eventId' => (int) $localEvent['id'], 'href' => $matchingRemoteEvent->href, 'uid' => $matchingRemoteEvent->uid, 'localModifiedAt' => $localModifiedAt, 'remoteModifiedAt' => $remoteModifiedAt, ]); ++$result->skipped; continue; } } } $payloadEvent = $this->fieldExtractor->toRemoteEvent($localEvent, $config->timezoneOrDefault()); if (null !== $matchingRemoteEvent) { $payloadEvent = new RemoteEvent( $payloadEvent->href, $payloadEvent->uid, $payloadEvent->etag, $payloadEvent->lastModifiedAt, $matchingRemoteEvent->hasTitle, $matchingRemoteEvent->hasDescription, $matchingRemoteEvent->hasLocation, $matchingRemoteEvent->hasUrl, $matchingRemoteEvent->hasTitle ? $payloadEvent->title : $matchingRemoteEvent->title, $matchingRemoteEvent->hasDescription ? $payloadEvent->description : $matchingRemoteEvent->description, $matchingRemoteEvent->hasLocation ? $payloadEvent->location : $matchingRemoteEvent->location, $matchingRemoteEvent->hasUrl ? $payloadEvent->url : $matchingRemoteEvent->url, $payloadEvent->startAt, $payloadEvent->endAt, $payloadEvent->allDay, $payloadEvent->timezone, ); } $targetHref = null !== $matchingRemoteEvent ? $matchingRemoteEvent->href : ('' !== trim((string) ($localEvent['caldavHref'] ?? '')) ? (string) $localEvent['caldavHref'] : null) ; $targetEtag = null !== $matchingRemoteEvent ? $matchingRemoteEvent->etag : ('' !== trim((string) ($localEvent['caldavEtag'] ?? '')) ? (string) $localEvent['caldavEtag'] : null) ; $upsertResult = $this->remoteWriter->upsertEvent( $config, $payloadEvent, $targetHref, $targetEtag, $dryRun, ); if (!$dryRun) { $this->eventRepository->update((int) $localEvent['id'], [ 'tstamp' => time(), 'caldavCalendarHref' => $config->caldavUrl, 'caldavUid' => $payloadEvent->uid, 'caldavHref' => $upsertResult['href'], 'caldavEtag' => $upsertResult['etag'], 'caldavSyncHash' => $currentHash, 'caldavLastSync' => time(), 'caldavOrigin' => 'local', 'caldavSyncState' => 'synced', ]); } if ('' !== $upsertResult['href']) { $localHrefs[$upsertResult['href']] = true; } $localUids[$payloadEvent->uid] = true; if (null === $matchingRemoteEvent && null === $remoteMatch) { ++$result->created; } else { ++$result->updated; } } foreach ($remoteData->hrefEtags as $remoteHref => $remoteEtag) { if (isset($localHrefs[$remoteHref])) { continue; } $this->remoteWriter->deleteEvent($config, $remoteHref, $remoteEtag, $dryRun); ++$result->deleted; } return $result; } /** * @param list $remoteEvents * @param array $hrefEtags * * @return list> */ private function buildRemotePseudoLocalRows(array $remoteEvents, array $hrefEtags): array { $rows = []; foreach ($hrefEtags as $href => $_etag) { $rows[] = [ 'caldavHref' => $href, 'caldavUid' => '', ]; } foreach ($remoteEvents as $remoteEvent) { $rows[] = [ 'caldavHref' => $remoteEvent->href, 'caldavUid' => $remoteEvent->uid, ]; } return $rows; } /** * @param list $remoteEvents */ private function resolveRemoteFromMatch(array $remoteEvents, ?array $match): ?RemoteEvent { if (null === $match) { return null; } foreach ($remoteEvents as $remoteEvent) { if ( ((string) ($match['caldavHref'] ?? '') !== '' && $remoteEvent->href === (string) $match['caldavHref']) || ((string) ($match['caldavUid'] ?? '') !== '' && $remoteEvent->uid === (string) $match['caldavUid']) ) { return $remoteEvent; } } return null; } private function preferLocalVersion(int $localModifiedAt, int $remoteModifiedAt): bool { if ($localModifiedAt > 0 && $remoteModifiedAt <= 0) { return true; } if ($localModifiedAt <= 0 && $remoteModifiedAt > 0) { return false; } if ($localModifiedAt <= 0 && $remoteModifiedAt <= 0) { // Be conservative and keep local edits when remote recency is unknown. return true; } return $localModifiedAt > ($remoteModifiedAt + self::MODIFIED_TIME_SKEW_SECONDS); } private function isEventWithinConfiguredWindow(array $localEvent, CalendarSyncConfig $config): bool { if (!$config->hasTimeWindow()) { return true; } $start = (int) ($localEvent['startTime'] ?? 0); if ($start <= 0) { $start = (int) ($localEvent['startDate'] ?? 0); } if ($start <= 0) { return true; } if (null !== $config->syncFromTimestamp && $start < $config->syncFromTimestamp) { return false; } if (null !== $config->syncUntilTimestamp && $start > $config->syncUntilTimestamp) { return false; } return true; } }