Fix 500 on REPORT for recurring events with Windows timezone IDs
DAVx5 reported a 500 Internal Server Error on a time-range REPORT against a calendar containing a recurring event whose DTSTART/DTEND used a Windows-style TZID (e.g. "W. Europe Standard Time", as commonly written by Outlook/Exchange and some Thunderbird/Lightning setups) instead of an IANA zone name. go-ical resolves TZID via a plain time.LoadLocation call, which only understands IANA names. A simple decode of such an event succeeds (RRULE dates aren't parsed eagerly), but expanding its recurrence - which go-webdav's caldav.Filter does for every time-range REPORT - calls Component.RecurrenceSet, which does call time.LoadLocation(tzid) and fails with "ical: error parsing start time: unknown time zone ...". Add internal/icalfix, a small shared helper that rewrites recognized Windows timezone identifiers (TZID parameters and VTIMEZONE TZID: lines) to their IANA equivalent in raw ICS bytes before decoding. Wire it into every ical.NewDecoder call site: internal/caldav/backend.go's decodeObject (fixes the reported bug), internal/web/calendar.go's event rendering/ICS import, and internal/icssub's remote feed fetching, so a subscribed feed with the same issue doesn't hit it either. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
package icalfix
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
ical "github.com/emersion/go-ical"
|
||||
)
|
||||
|
||||
// TestNormalizeTimeZonesFixesRecurringEvent reproduces the bug reported by
|
||||
// a DAVx5 client: a recurring VEVENT with a Windows-style TZID
|
||||
// ("W. Europe Standard Time") decodes fine, but expanding its recurrence
|
||||
// via RecurrenceSet (as go-webdav's caldav.Filter does for a time-range
|
||||
// REPORT) fails with "unknown time zone" because go-ical resolves TZID
|
||||
// via a plain time.LoadLocation call. Normalizing the TZID to its IANA
|
||||
// equivalent before decoding must make that expansion succeed.
|
||||
func TestNormalizeTimeZonesFixesRecurringEvent(t *testing.T) {
|
||||
const raw = "BEGIN:VCALENDAR\r\n" +
|
||||
"VERSION:2.0\r\n" +
|
||||
"PRODID:-//Test//Test//EN\r\n" +
|
||||
"BEGIN:VTIMEZONE\r\n" +
|
||||
"TZID:W. Europe Standard Time\r\n" +
|
||||
"END:VTIMEZONE\r\n" +
|
||||
"BEGIN:VEVENT\r\n" +
|
||||
"UID:recurring@example.com\r\n" +
|
||||
"DTSTAMP:20240101T000000Z\r\n" +
|
||||
"DTSTART;TZID=W. Europe Standard Time:20260522T220000\r\n" +
|
||||
"DTEND;TZID=W. Europe Standard Time:20260522T230000\r\n" +
|
||||
"RRULE:FREQ=WEEKLY;COUNT=5\r\n" +
|
||||
"SUMMARY:Weekly sync\r\n" +
|
||||
"END:VEVENT\r\n" +
|
||||
"END:VCALENDAR\r\n"
|
||||
|
||||
normalized := NormalizeTimeZones([]byte(raw))
|
||||
|
||||
cal, err := ical.NewDecoder(bytes.NewReader(normalized)).Decode()
|
||||
if err != nil {
|
||||
t.Fatalf("decoding normalized calendar: %v", err)
|
||||
}
|
||||
events := cal.Events()
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(events))
|
||||
}
|
||||
|
||||
if _, err := events[0].RecurrenceSet(nil); err != nil {
|
||||
t.Fatalf("expanding recurrence after normalization: %v", err)
|
||||
}
|
||||
|
||||
// The un-normalized original must reproduce the reported failure, so
|
||||
// this test actually demonstrates the fix rather than trivially
|
||||
// passing regardless.
|
||||
origCal, err := ical.NewDecoder(bytes.NewReader([]byte(raw))).Decode()
|
||||
if err != nil {
|
||||
t.Fatalf("decoding original calendar: %v", err)
|
||||
}
|
||||
if _, err := origCal.Events()[0].RecurrenceSet(nil); err == nil {
|
||||
t.Fatalf("expected RecurrenceSet to fail on un-normalized Windows TZID, it didn't")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeTimeZonesLeavesUnknownAndIANAZonesAlone(t *testing.T) {
|
||||
const raw = "BEGIN:VCALENDAR\r\n" +
|
||||
"DTSTART;TZID=Europe/Berlin:20260522T220000\r\n" +
|
||||
"DTEND;TZID=Some/Unknown-Zone:20260522T230000\r\n" +
|
||||
"END:VCALENDAR\r\n"
|
||||
|
||||
got := string(NormalizeTimeZones([]byte(raw)))
|
||||
if got != raw {
|
||||
t.Fatalf("expected data to be unchanged, got:\n%s", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user