Skip to content

[CalendarLink] Random UID per build creates duplicate events on re-add #3810

Description

@ker0x

Component: CalendarLink
Version: v3.4.0

Problem

IcsBuilder::build() mints a fresh UID on every call:

'UID:'.$this->uuidFactory->create()->toRfc4122(),

UID is the property calendar clients use to decide whether an incoming
VEVENT is a new event or an update to one they already hold
(RFC 5545 §3.8.4.7). Because it is random per build, the same
CalendarEvent serializes to a different identity every time.

In the exact flow this component targets — an "Add to calendar" button on a
booking/event page — that means:

  • a user who clicks the .ics link, then reloads and clicks again, gets two
    copies
    of the event rather than one;
  • the same event rendered on two pages (confirmation page + reminder page)
    yields two unrelated entries;
  • an event whose details change cannot be re-issued as a correction, since the
    new file looks like an unrelated event.

Current status

This is currently pinned as intended behaviour:

// tests/Ics/IcsBuilderTest.php
public function testUidIsUniquePerBuild()
{
    // ...
    $this->assertNotSame($first[1], $second[1]);
}

IcsBuilder does accept a UuidFactory, but UuidFactory::create() takes no
arguments, so the UID cannot be derived from the event — and the service is
registered as ux_calendar_link.ics.builder with no class alias, so it is not
autowirable for a userland override either (see separate issue).

Suggested fix

Either would resolve it:

  1. Optional explicit UID — add ?string $uid = null to CalendarEvent,
    letting the application pass its own stable domain key (a booking UUID,
    typically). Least surprising, and matches how RFC 5545 expects UIDs to be
    assigned.
  2. Deterministic derivation — default the UID to a UUIDv5 over the event's
    own content (title, start, end, location). Keeps the zero-config path
    working while making identical events identical.

Option 1 alone would be enough for most applications; option 2 makes the
default behaviour correct for everyone.

Happy to open a PR if a direction is preferred.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions