Architecture

LH DAV is a data plugin: it owns the DAV protocol and the sync change log, and DAV itself (vCard and iCalendar, readable by any standard client) is the portability mechanism. It registers no MCP abilities; tooling plugins use its plain PHP service API (see Provider API).

URL layout

Everything is under /wp-json/lh-dav/v1/ on each site:

/.well-known/carddav and /.well-known/caldav are matched on parse_request (no rewrite rules, so nothing to flush across the network) and 301 to the root, only while a provider serves that component.

Serving DAV through REST

Taken from LH Vcard 1.11.0’s CardDAV handler. The REST server only carries the request: LH_DAV_Server builds the full response, returns a WP_REST_Response whose data is a short summary (what REST logs record), and sends the real XML, vCard or iCalendar body on rest_pre_serve_request. OPTIONS on any DAV path, and every method on the namespace root, are answered on rest_pre_dispatch (priority 5) before core’s own handling.

Authentication

Core’s application password check does the login. On DAV paths rest_authentication_errors (priority 99) skips core’s errors so the server can send its own WWW-Authenticate: Basic challenge, which contacts apps need. The server then refuses any request not authenticated by an application password (rest_get_authenticated_app_password()), so a browser cookie alone gets a 401. Users also need the access capability (read by default).

Making and changing collections

Since 0.3.0, MKCALENDAR and extended MKCOL on a free name in a home make a collection, PROPPATCH on a collection renames, recolours or reorders it, and DELETE on a collection removes it, all through a provider implementing LH_DAV_Collection_Manager (see Provider API). The DAV header then includes extended-mkcol. LH Calendars is the first such provider.

Change log and sync tokens

Table {base_prefix}lh_dav_changes (network-wide): id (the sync sequence), site_id, collection (key {component}/{owner user ID, 0 if shared}/{collection id}), resource, operation (modified or deleted), user_id, changed_gmt. Created and upgraded on schema_version() (stored in site option lh_dav-schema_version), never on plugin_version(). Since 0.3.0 changes are recorded and read against the collection’s site_id, which defaults to the current site.

Sync tokens are {base}sync/{sequence}/{state hash}. A token whose state hash no longer matches the collection’s state gets a full resend; one below the pruned-through marker gets 403 valid-sync-token, telling the client to resync. A daily cron on the main site prunes rows older than the retention (90 days) and records the highest pruned ID in site option lh_dav-pruned_through.

Known limits