www/static/js
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Hauke Mehrtens 6d99a39190 Do not list a moved event occurrence twice
An occurrence of a recurring event that was changed on its own carries a
RECURRENCE-ID and is stored as an additional VEVENT next to the event it
belongs to. `getAllSubcomponents("vevent")` returns those components as well,
and since they have no RRULE of their own they were handled as separate single
events. The occurrence therefore ended up in the list twice: once from
expanding the recurring event, which resolves the modified time through the
exception, and once more from the extra VEVENT.

To make it worse the two rows disagreed, because name and URL were taken from
the recurring event while the time came from the modification, so the first row
showed the new time under the old name.

Skip components that are a recurrence exception, they are already covered by
the event they modify, and take name and URL from the occurrence details, which
point at the modification where there is one and at the event itself otherwise.
Events whose RECURRENCE-ID refers to an event that is not in the file are
dropped by this, which cannot happen in a full calendar export.

With a recurring Plenum whose 25.08. occurrence is moved two hours earlier and
renamed:

  before: 25.08. 18:00  CCCB Plenum
          25.08. 18:00  CCCB Plenum (verschoben)
  after:  25.08. 18:00  CCCB Plenum (verschoben)

Leaning on the resolution ical.js does here needs two more things to be right.

The first is which modifications an event is asked to resolve. Unless it is
told, `ICAL.Event` relates every VEVENT with a RECURRENCE-ID in the file to
every recurring event and keys them by the recurrence id alone; the UID is only
compared with `strictExceptions`, which then throws instead of skipping. A
modification would therefore also override the occurrence another series holds
at the same instant, so the wrong entry is shown and the modified one twice
over. Group the modifications by the UID of the event they belong to and hand
each event its own, which also turns the relating off for events that have
none. Recognise a modification on the component instead of on the event,
because relating exceptions to an exception throws. With two unrelated weekly
series that both meet on Thursday at 19:00, of which only A has its occurrence
of 03.09. moved to 17:00:

  before: 27.08. 19:00 Serie A          after: 27.08. 19:00 Serie A
          27.08. 19:00 Serie B                 27.08. 19:00 Serie B
          03.09. 17:00 Serie A (versch.)       03.09. 17:00 Serie A (versch.)
          03.09. 17:00 Serie A (versch.)       03.09. 19:00 Serie B

The second is how far the expansion has to run. It walks the unmodified
recurrence times and stopped at the end of the window, but the occurrence
handed to `getOccurrenceDetails()` may have been moved somewhere else entirely.
Both directions were wrong: an occurrence pulled forward from beyond the window
was never reached, because the walk had already stopped at its original time,
and one pushed out of the window was still listed, because only its original
time was ever compared against the window. Iterate far enough that the largest
move towards the past can still reach the window, and decide by the time the
occurrence really takes place at. The stop condition keeps using the raw
recurrence time, which stays monotonic, so the iteration still terminates. With
a weekly series whose occurrence of 05.10. is pulled forward to 25.08. and
whose occurrence of 31.08. is pushed to 02.11., seen from 23.08. through the 20
day window:

  before: 24.08. Serie                 after: 24.08. Serie
          31.08. Serie                        25.08. Serie (vorgezogen)
          07.09. Serie                        07.09. Serie

Reading the URL becomes a function of its own while name and URL move to the
occurrence details, and it now looks at what it reads. The calendar is exported
from a CalDAV server, so whoever may write to it decides what ends up in the
href of the link, and `URL:javascript:alert(1)` on an event would run that
script when a visitor clicks the entry. Pass on nothing but http and https; a
value that is not a URL at all no longer reaches the href either.

The published calendar contains no RECURRENCE-ID at all today, so nothing about
the modifications changes for it. It is exported from a CalDAV server though,
and moving a single Club Discordia or Plenum out of the way of a holiday is
exactly what creates such a modification. Checked against
https://berlin.ccc.de/calendars/all.ics: the list and the links of the 30
events that carry a URL are unchanged.

Fixes: c28f04c6e8 ("switch to ics files; make calendars work; fix some minor issues")
Assisted-by: Claude:claude-opus-5
Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de>
2026-08-23 01:07:25 +02:00
..
upcoming.js Do not list a moved event occurrence twice 2026-08-23 01:07:25 +02:00