Release: LMW delta sync hardening, CTAG windowing, calendar matching guard
This commit is contained in:
@@ -34,13 +34,30 @@ final readonly class LocalToRemoteSynchronizer
|
||||
$allLocalEvents,
|
||||
fn (array $event): bool => $config->shouldManageEventForThisRemoteCalendar((string) ($event['caldavCalendarHref'] ?? '')),
|
||||
));
|
||||
$remoteEvents = $this->remoteReader->readEvents($config);
|
||||
$remotePseudoLocalRows = $this->buildRemotePseudoLocalRows($remoteEvents);
|
||||
|
||||
$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;
|
||||
@@ -59,7 +76,7 @@ final readonly class LocalToRemoteSynchronizer
|
||||
$localUids[$uid] = true;
|
||||
}
|
||||
|
||||
$localSyncFields = $this->fieldExtractor->extractFromLocalEvent($localEvent);
|
||||
$localSyncFields = $this->fieldExtractor->extractFromLocalEvent($localEvent, $config->timezoneOrDefault());
|
||||
$currentHash = $this->hashGenerator->generate($localSyncFields);
|
||||
$storedHash = (string) ($localEvent['caldavSyncHash'] ?? '');
|
||||
$localChanged = '' === $storedHash || $storedHash !== $currentHash;
|
||||
@@ -69,7 +86,7 @@ final readonly class LocalToRemoteSynchronizer
|
||||
continue;
|
||||
}
|
||||
|
||||
$remoteMatch = $this->matchResolver->resolve($remotePseudoLocalRows, $this->fieldExtractor->toRemoteEvent($localEvent, $config->timezoneOrDefault()));
|
||||
$remoteMatch = $this->matchResolver->resolve($remotePseudoLocalRows, $this->fieldExtractor->toRemoteEvent($localEvent, $config->timezoneOrDefault()), $config->calendarId);
|
||||
$matchingRemoteEvent = $this->resolveRemoteFromMatch($remoteEvents, $remoteMatch);
|
||||
|
||||
if (null !== $matchingRemoteEvent && '' !== $storedHash) {
|
||||
@@ -164,19 +181,19 @@ final readonly class LocalToRemoteSynchronizer
|
||||
}
|
||||
$localUids[$payloadEvent->uid] = true;
|
||||
|
||||
if (null === $matchingRemoteEvent) {
|
||||
if (null === $matchingRemoteEvent && null === $remoteMatch) {
|
||||
++$result->created;
|
||||
} else {
|
||||
++$result->updated;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($remoteEvents as $remoteEvent) {
|
||||
if (isset($localHrefs[$remoteEvent->href]) || isset($localUids[$remoteEvent->uid])) {
|
||||
foreach ($remoteData->hrefEtags as $remoteHref => $remoteEtag) {
|
||||
if (isset($localHrefs[$remoteHref])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->remoteWriter->deleteEvent($config, $remoteEvent->href, $remoteEvent->etag, $dryRun);
|
||||
$this->remoteWriter->deleteEvent($config, $remoteHref, $remoteEtag, $dryRun);
|
||||
++$result->deleted;
|
||||
}
|
||||
|
||||
@@ -185,13 +202,21 @@ final readonly class LocalToRemoteSynchronizer
|
||||
|
||||
/**
|
||||
* @param list<RemoteEvent> $remoteEvents
|
||||
* @param array<string,string> $hrefEtags
|
||||
*
|
||||
* @return list<array<string,mixed>>
|
||||
*/
|
||||
private function buildRemotePseudoLocalRows(array $remoteEvents): array
|
||||
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,
|
||||
@@ -240,4 +265,30 @@ final readonly class LocalToRemoteSynchronizer
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user