Release: LMW delta sync hardening, CTAG windowing, calendar matching guard

This commit is contained in:
Jürgen Mummert
2026-03-28 16:59:58 +01:00
parent c6f63a56a9
commit c0d3bf4c82
15 changed files with 775 additions and 171 deletions
+134 -95
View File
@@ -1,118 +1,157 @@
# CalDAV Sync Bundle (Contao 5.7)
Produktionsreifes Grundgeruest fuer einen 2-Way-CalDAV-Sync (V1) zwischen `tl_calendar_events` und einem CalDAV-Kalender.
Produktionsreifes 2-Way-CalDAV-Sync-Bundle fuer Contao 5.7 (PHP 8.4).
Es synchronisiert Events zwischen `tl_calendar_events` und einem oder mehreren CalDAV-Kalendern.
## V1-Scope
## Umfang (V1)
- Kein RRULE/EXDATE/RECURRENCE-ID
- Kein Backend-Button, nur Command
- Harte Loeschung bei fehlendem Gegenstueck
- Konfliktregel: remote gewinnt
- Contao-only-Felder werden niemals remote geschrieben
- Kein RRULE/EXDATE/RECURRENCE-ID-Support
- Kein Backend-Button, Sync nur ueber Command/Cron
- Harte Loeschung auf beiden Seiten, wenn das Gegenstueck fehlt
- Konfliktaufloesung pro Event ueber Last-Modified-Wins (mit Toleranz)
- Contao-only-Felder werden nicht in den Sync-Hash aufgenommen
## Verzeichnisstruktur
## Voraussetzungen
```text
caldav-sync-bundle/
composer.json
config/
services.yaml
contao/
dca/
tl_calendar.php
tl_calendar_events.php
languages/
de/
tl_calendar.php
tl_calendar_events.php
src/
CalDavSyncBundle.php
Command/
CalDavSyncCommand.php
Config/
CalendarSyncConfig.php
Contao/Manager/
Plugin.php
DependencyInjection/
CalDavSyncExtension.php
Repository/
ContaoCalendarRepository.php
ContaoCalendarEventRepository.php
Migration/
Version20260327000000.php
Service/
CalendarConfigProvider.php
CalDav/
Transport/
CalDavTransportInterface.php
CalDavTransport.php
TransportResponse.php
Parser/
CalDavXmlParser.php
ICalendarParser.php
ICalendarSerializer.php
Remote/
RemoteCalendarReader.php
RemoteCalendarWriter.php
Sync/
RemoteEvent.php
EventMatchResolver.php
SyncFieldExtractor.php
SyncHashGenerator.php
SyncResult.php
RemoteToLocalSynchronizer.php
LocalToRemoteSynchronizer.php
SyncRunner.php
- PHP `^8.4`
- Contao `^5.7`
- `symfony/http-client` `^7.3`
- `sabre/vobject` `^4.5`
## Installation
### 1) Bundle als Composer-Abhaengigkeit einbinden
Bei lokalem Bundle-Checkout z. B. im Projekt-Composer:
```json
{
"repositories": [
{
"type": "path",
"url": "bundles/caldav-sync-bundle",
"options": { "symlink": true }
}
],
"require": {
"mummert/caldav-sync-bundle": "*@dev"
}
}
```
## Klassenuebersicht
Dann installieren:
- `CalendarConfigProvider`: Liefert pro `tl_calendar` die Sync-Konfiguration.
- `ContaoCalendarRepository`: Zugriff auf Sync-faehige Kalender.
- `ContaoCalendarEventRepository`: CRUD fuer `tl_calendar_events`.
- `CalDavTransport`: HTTP/WebDAV via Symfony HttpClient (`PROPFIND`, `REPORT`, `GET`, `PUT`, `DELETE`).
- `CalDavXmlParser`: Parsing von WebDAV-Multistatus XML.
- `ICalendarParser`: VEVENT-Parsing via `sabre/vobject`.
- `ICalendarSerializer`: VEVENT-Serialisierung via `sabre/vobject`.
- `RemoteCalendarReader`: Liest Remote-Kalender.
- `RemoteCalendarWriter`: Schreibt/loescht Remote-Events mit ETag-Unterstuetzung.
- `EventMatchResolver`: Matching zuerst ueber `caldavHref`, dann `caldavUid`.
- `SyncFieldExtractor`: Trennt bidirektionale Sync-Felder von lokalen Contao-only-Feldern.
- `SyncHashGenerator`: SHA-256 ueber kanonische Sync-Felder.
- `RemoteToLocalSynchronizer`: Pull inkl. Konfliktregel und Hard-Delete lokal.
- `LocalToRemoteSynchronizer`: Push inkl. Konfliktregel und Hard-Delete remote.
- `SyncRunner`: Orchestrierung je Kalender und Richtung.
- `CalDavSyncCommand`: CLI-Einstiegspunkt.
```bash
composer update mummert/caldav-sync-bundle
php vendor/bin/contao-console contao:migrate
php vendor/bin/contao-console cache:clear
```
## Mapping-Strategie
## Backend-Konfiguration (Kalender)
Bidirektionale Felder (sync-relevant):
In `System -> Kalender` den gewuenschten Kalender oeffnen und unter CalDAV konfigurieren:
- `title`
- `start/end`
- `all-day / with-time` (`addTime` Mapping)
- `description` (`details`)
- `location`
- optional `url`
- `CalDAV Sync aktivieren`
- `CalDAV URL`
- `CalDAV Benutzername`
- `CalDAV Passwort`
- `CalDAV Autor` (Pflichtfeld, wird fuer importierte Events gesetzt)
- optional `CalDAV Zeitzone` (Fallback: `UTC`)
- `Remote-Kalender` als Mehrfachauswahl (CheckboxWizard)
Contao-only-Felder sind explizit nicht Teil des Sync-Hashes und loesen damit keinen Push aus.
Hinweis zur Mehrfachauswahl:
## Logging-Strategie
- Es koennen mehrere Remote-Kalender unter einer Verbindung ausgewaehlt werden.
- Events aus abgewaehlten Remote-Kalendern werden beim naechsten Pull fuer den betroffenen Contao-Kalender entfernt.
- `info`: erfolgreicher Abschluss pro Kalender inkl. Zaehler
- `warning`: Konflikte (`remote wins`) mit Kontext (`calendarId`, `eventId`, `href`, `uid`)
- Fehler laufen als Exceptions hoch und werden im Command als `FAILURE` signalisiert
## Command
## Sync ausfuehren
```bash
php bin/console contao:caldav:sync --direction=both
php bin/console contao:caldav:sync --calendar=3 --direction=pull
php bin/console contao:caldav:sync --calendar=4 --direction=pull
php bin/console contao:caldav:sync --direction=push --dry-run
```
Optionen:
- `--calendar=ID`
- `--direction=pull|push|both`
- `--dry-run`
- `--calendar=ID`: nur einen `tl_calendar` synchronisieren
- `--direction=pull|push|both`: Richtung waehlen (Default `both`)
- `--dry-run`: keine Schreiboperationen lokal/remote
## Sync-Verhalten im Detail
### Reihenfolge bei `--direction=both`
1. Push (lokal -> remote)
2. Pull (remote -> lokal)
### Konfliktregel (Last-Modified-Wins)
- Lokal wird ueber `tl_calendar_events.tstamp` bewertet.
- Remote wird ueber `LAST-MODIFIED` bzw. `DTSTAMP` bewertet.
- Es gilt eine Toleranz von 120 Sekunden.
- Wenn Remote-Zeitstempel fehlen, wird konservativ lokal bevorzugt.
### Loeschverhalten
- Pull: lokale Gegenstuecke ohne Remote-Pendant werden geloescht.
- Push: Remote-Events ohne lokales Pendant werden geloescht.
- Zusaetzlich entfernt der Runner bei Pull importierte Events aus inzwischen abgewaehlten Remote-Kalendern.
### Publikationsverhalten
- Remote-importierte Events werden auf `published = 1` gesetzt.
## Feldmapping
Bidirektional (hash-relevant):
- `title`
- `start`/`end`
- `all-day` (`addTime`)
- `description` <-> `teaser` (Plaintext/HTML-Konvertierung)
- `location`
- `url` (optional)
Technische Sync-Felder in `tl_calendar_events`:
- `caldavCalendarHref`
- `caldavUid`
- `caldavHref`
- `caldavEtag`
- `caldavSyncHash`
- `caldavLastSync`
- `caldavOrigin`
- `caldavSyncState`
Alias-Verhalten:
- Alias wird bei Remote-Neuanlage als `YYYY-MM-DD_slug` erzeugt.
- Maximale Laenge: 40 Zeichen.
- Kollisionen werden mit Suffix (`_2`, `_3`, ...) aufgeloest.
## Wichtige Architekturpunkte
- `SyncRunner`: Orchestrierung pro Kalender und Richtung
- `RemoteToLocalSynchronizer`: Pull + lokale Upserts/Deletes
- `LocalToRemoteSynchronizer`: Push + remote Upserts/Deletes
- `SyncFieldExtractor`: Mapping, Datumslogik, Teaser-Konvertierung, Aliasbildung
- `RemoteCalendarReader` / `RemoteCalendarWriter`: CalDAV Lesen/Schreiben
- `ContaoCalendarEventRepository`: DB-Zugriff inkl. schema-toleranter Writes
## Grenzen und Hinweise
- V1 behandelt keine Serienereignisse.
- Sync ist command-getrieben; ein Cronjob sollte das Command zyklisch starten.
- CalDAV-Passwort wird als Klartext fuer Authentifizierung verwendet.
## Troubleshooting
- Keine Kalender in der Mehrfachauswahl:
- URL, Benutzername, Passwort pruefen
- Server muss CalDAV-Discovery/PROPFIND erlauben
- Unerwartete Konflikte:
- `tstamp` lokal und `LAST-MODIFIED` remote vergleichen
- Zeitzonen-Setup im Kalender pruefen
- Schreibfehler beim Push:
- ETag-Precondition und Zugriffsrechte am CalDAV-Server pruefen