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,201 @@
|
||||
// Package icalfix normalizes non-standard timezone identifiers in raw
|
||||
// iCalendar data before it's handed to go-ical's decoder.
|
||||
//
|
||||
// go-ical resolves a DTSTART/DTEND's TZID parameter (and, for recurring
|
||||
// events, the RRULE expansion's start time) via a direct
|
||||
// time.LoadLocation(tzid) call — see
|
||||
// github.com/emersion/go-ical@.../ical.go's Prop.DateTime and
|
||||
// components.go's Component.RecurrenceSet. That only understands IANA
|
||||
// zone names ("Europe/Berlin"), but Windows/Outlook (and anything built
|
||||
// on the Windows timezone database, e.g. Exchange, Thunderbird's Lightning
|
||||
// on some setups) commonly emits Windows zone names instead
|
||||
// ("W. Europe Standard Time"), which time.LoadLocation doesn't recognize.
|
||||
// A recurring event stored with such a TZID decodes fine (go-ical doesn't
|
||||
// eagerly parse RRULE dates), but fails later — as a 500 — the moment a
|
||||
// CalDAV client issues a time-range REPORT that has to expand its
|
||||
// recurrence, since that's when RecurrenceSet actually calls
|
||||
// time.LoadLocation.
|
||||
package icalfix
|
||||
|
||||
import "regexp"
|
||||
|
||||
// tzidParamRe matches a "TZID=<name>" (optionally quoted) parameter value
|
||||
// as it appears attached to any property (e.g. "DTSTART;TZID=W. Europe
|
||||
// Standard Time:..."). The name runs until the next ':' or ';' delimiter
|
||||
// or line break, so it captures the parameter's full value even though it
|
||||
// contains spaces and periods.
|
||||
var tzidParamRe = regexp.MustCompile(`TZID=("?)([^";:\r\n]+)("?)`)
|
||||
|
||||
// tzidLineRe matches a standalone "TZID:<name>" property line, as found
|
||||
// inside a VTIMEZONE component.
|
||||
var tzidLineRe = regexp.MustCompile(`(?m)^TZID:([^\r\n]+)$`)
|
||||
|
||||
// NormalizeTimeZones rewrites any recognized Windows timezone identifier
|
||||
// in data to its IANA equivalent, leaving everything else (including
|
||||
// unrecognized TZIDs) untouched. It's safe to call on data that has no
|
||||
// Windows TZIDs at all — such data passes through unchanged.
|
||||
func NormalizeTimeZones(data []byte) []byte {
|
||||
data = tzidParamRe.ReplaceAllFunc(data, func(m []byte) []byte {
|
||||
sub := tzidParamRe.FindSubmatch(m)
|
||||
name := string(sub[2])
|
||||
iana, ok := windowsToIANA[name]
|
||||
if !ok {
|
||||
return m
|
||||
}
|
||||
return []byte("TZID=" + string(sub[1]) + iana + string(sub[3]))
|
||||
})
|
||||
data = tzidLineRe.ReplaceAllFunc(data, func(m []byte) []byte {
|
||||
sub := tzidLineRe.FindSubmatch(m)
|
||||
name := string(sub[1])
|
||||
iana, ok := windowsToIANA[name]
|
||||
if !ok {
|
||||
return m
|
||||
}
|
||||
return []byte("TZID:" + iana)
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
// windowsToIANA maps common Windows timezone identifiers (as found in the
|
||||
// CLDR windowsZones.xml "001"/default territory mapping) to an IANA zone
|
||||
// name recognized by Go's tzdata. It's not exhaustive, but covers the
|
||||
// zones seen in practice from Outlook/Exchange-generated ICS data.
|
||||
var windowsToIANA = map[string]string{
|
||||
"Dateline Standard Time": "Etc/GMT+12",
|
||||
"UTC-11": "Etc/GMT+11",
|
||||
"Aleutian Standard Time": "America/Adak",
|
||||
"Hawaiian Standard Time": "Pacific/Honolulu",
|
||||
"Marquesas Standard Time": "Pacific/Marquesas",
|
||||
"Alaskan Standard Time": "America/Anchorage",
|
||||
"UTC-09": "Etc/GMT+9",
|
||||
"Pacific Standard Time (Mexico)": "America/Tijuana",
|
||||
"UTC-08": "Etc/GMT+8",
|
||||
"Pacific Standard Time": "America/Los_Angeles",
|
||||
"US Mountain Standard Time": "America/Phoenix",
|
||||
"Mountain Standard Time (Mexico)": "America/Chihuahua",
|
||||
"Mountain Standard Time": "America/Denver",
|
||||
"Central America Standard Time": "America/Guatemala",
|
||||
"Central Standard Time": "America/Chicago",
|
||||
"Central Standard Time (Mexico)": "America/Mexico_City",
|
||||
"Canada Central Standard Time": "America/Regina",
|
||||
"SA Pacific Standard Time": "America/Bogota",
|
||||
"Eastern Standard Time (Mexico)": "America/Cancun",
|
||||
"Eastern Standard Time": "America/New_York",
|
||||
"Haiti Standard Time": "America/Port-au-Prince",
|
||||
"Cuba Standard Time": "America/Havana",
|
||||
"US Eastern Standard Time": "America/Indianapolis",
|
||||
"Turks And Caicos Standard Time": "America/Grand_Turk",
|
||||
"Paraguay Standard Time": "America/Asuncion",
|
||||
"Atlantic Standard Time": "America/Halifax",
|
||||
"Venezuela Standard Time": "America/Caracas",
|
||||
"Central Brazilian Standard Time": "America/Cuiaba",
|
||||
"SA Western Standard Time": "America/La_Paz",
|
||||
"Pacific SA Standard Time": "America/Santiago",
|
||||
"Newfoundland Standard Time": "America/St_Johns",
|
||||
"Tocantins Standard Time": "America/Araguaina",
|
||||
"E. South America Standard Time": "America/Sao_Paulo",
|
||||
"SA Eastern Standard Time": "America/Cayenne",
|
||||
"Argentina Standard Time": "America/Buenos_Aires",
|
||||
"Greenland Standard Time": "America/Godthab",
|
||||
"Montevideo Standard Time": "America/Montevideo",
|
||||
"Magallanes Standard Time": "America/Punta_Arenas",
|
||||
"Saint Pierre Standard Time": "America/Miquelon",
|
||||
"Bahia Standard Time": "America/Bahia",
|
||||
"UTC-02": "Etc/GMT+2",
|
||||
"Azores Standard Time": "Atlantic/Azores",
|
||||
"Cape Verde Standard Time": "Atlantic/Cape_Verde",
|
||||
"UTC": "Etc/UTC",
|
||||
"GMT Standard Time": "Europe/London",
|
||||
"Greenwich Standard Time": "Atlantic/Reykjavik",
|
||||
"Sao Tome Standard Time": "Africa/Sao_Tome",
|
||||
"Morocco Standard Time": "Africa/Casablanca",
|
||||
"W. Europe Standard Time": "Europe/Berlin",
|
||||
"Central Europe Standard Time": "Europe/Budapest",
|
||||
"Romance Standard Time": "Europe/Paris",
|
||||
"Central European Standard Time": "Europe/Warsaw",
|
||||
"W. Central Africa Standard Time": "Africa/Lagos",
|
||||
"Jordan Standard Time": "Asia/Amman",
|
||||
"GTB Standard Time": "Europe/Bucharest",
|
||||
"Middle East Standard Time": "Asia/Beirut",
|
||||
"Egypt Standard Time": "Africa/Cairo",
|
||||
"E. Europe Standard Time": "Europe/Chisinau",
|
||||
"Syria Standard Time": "Asia/Damascus",
|
||||
"West Bank Standard Time": "Asia/Hebron",
|
||||
"South Africa Standard Time": "Africa/Johannesburg",
|
||||
"FLE Standard Time": "Europe/Kiev",
|
||||
"Israel Standard Time": "Asia/Jerusalem",
|
||||
"Kaliningrad Standard Time": "Europe/Kaliningrad",
|
||||
"Sudan Standard Time": "Africa/Khartoum",
|
||||
"Libya Standard Time": "Africa/Tripoli",
|
||||
"Namibia Standard Time": "Africa/Windhoek",
|
||||
"Arabic Standard Time": "Asia/Baghdad",
|
||||
"Turkey Standard Time": "Europe/Istanbul",
|
||||
"Arab Standard Time": "Asia/Riyadh",
|
||||
"Belarus Standard Time": "Europe/Minsk",
|
||||
"Russian Standard Time": "Europe/Moscow",
|
||||
"E. Africa Standard Time": "Africa/Nairobi",
|
||||
"Volgograd Standard Time": "Europe/Volgograd",
|
||||
"Iran Standard Time": "Asia/Tehran",
|
||||
"Arabian Standard Time": "Asia/Dubai",
|
||||
"Astrakhan Standard Time": "Europe/Astrakhan",
|
||||
"Azerbaijan Standard Time": "Asia/Baku",
|
||||
"Russia Time Zone 3": "Europe/Samara",
|
||||
"Mauritius Standard Time": "Indian/Mauritius",
|
||||
"Saratov Standard Time": "Europe/Saratov",
|
||||
"Georgian Standard Time": "Asia/Tbilisi",
|
||||
"Caucasus Standard Time": "Asia/Yerevan",
|
||||
"Afghanistan Standard Time": "Asia/Kabul",
|
||||
"West Asia Standard Time": "Asia/Tashkent",
|
||||
"Ekaterinburg Standard Time": "Asia/Yekaterinburg",
|
||||
"Pakistan Standard Time": "Asia/Karachi",
|
||||
"Qyzylorda Standard Time": "Asia/Qyzylorda",
|
||||
"India Standard Time": "Asia/Calcutta",
|
||||
"Sri Lanka Standard Time": "Asia/Colombo",
|
||||
"Nepal Standard Time": "Asia/Katmandu",
|
||||
"Central Asia Standard Time": "Asia/Almaty",
|
||||
"Bangladesh Standard Time": "Asia/Dhaka",
|
||||
"Omsk Standard Time": "Asia/Omsk",
|
||||
"Myanmar Standard Time": "Asia/Rangoon",
|
||||
"SE Asia Standard Time": "Asia/Bangkok",
|
||||
"Altai Standard Time": "Asia/Barnaul",
|
||||
"W. Mongolia Standard Time": "Asia/Hovd",
|
||||
"North Asia Standard Time": "Asia/Krasnoyarsk",
|
||||
"N. Central Asia Standard Time": "Asia/Novosibirsk",
|
||||
"Tomsk Standard Time": "Asia/Tomsk",
|
||||
"China Standard Time": "Asia/Shanghai",
|
||||
"North Asia East Standard Time": "Asia/Irkutsk",
|
||||
"Singapore Standard Time": "Asia/Singapore",
|
||||
"W. Australia Standard Time": "Australia/Perth",
|
||||
"Taipei Standard Time": "Asia/Taipei",
|
||||
"Ulaanbaatar Standard Time": "Asia/Ulaanbaatar",
|
||||
"Aus Central W. Standard Time": "Australia/Eucla",
|
||||
"Transbaikal Standard Time": "Asia/Chita",
|
||||
"Tokyo Standard Time": "Asia/Tokyo",
|
||||
"North Korea Standard Time": "Asia/Pyongyang",
|
||||
"Korea Standard Time": "Asia/Seoul",
|
||||
"Yakutsk Standard Time": "Asia/Yakutsk",
|
||||
"Cen. Australia Standard Time": "Australia/Adelaide",
|
||||
"AUS Central Standard Time": "Australia/Darwin",
|
||||
"E. Australia Standard Time": "Australia/Brisbane",
|
||||
"AUS Eastern Standard Time": "Australia/Sydney",
|
||||
"West Pacific Standard Time": "Pacific/Port_Moresby",
|
||||
"Tasmania Standard Time": "Australia/Hobart",
|
||||
"Vladivostok Standard Time": "Asia/Vladivostok",
|
||||
"Lord Howe Standard Time": "Australia/Lord_Howe",
|
||||
"Bougainville Standard Time": "Pacific/Bougainville",
|
||||
"Russia Time Zone 10": "Asia/Srednekolymsk",
|
||||
"Magadan Standard Time": "Asia/Magadan",
|
||||
"Norfolk Standard Time": "Pacific/Norfolk",
|
||||
"Sakhalin Standard Time": "Asia/Sakhalin",
|
||||
"Central Pacific Standard Time": "Pacific/Guadalcanal",
|
||||
"Russia Time Zone 11": "Asia/Kamchatka",
|
||||
"New Zealand Standard Time": "Pacific/Auckland",
|
||||
"UTC+12": "Etc/GMT-12",
|
||||
"Fiji Standard Time": "Pacific/Fiji",
|
||||
"Kamchatka Standard Time": "Asia/Kamchatka",
|
||||
"Chatham Islands Standard Time": "Pacific/Chatham",
|
||||
"UTC+13": "Etc/GMT-13",
|
||||
"Tonga Standard Time": "Pacific/Tongatapu",
|
||||
"Samoa Standard Time": "Pacific/Apia",
|
||||
"Line Islands Standard Time": "Pacific/Kiritimati",
|
||||
}
|
||||
Reference in New Issue
Block a user