diff --git a/src/parser/calendar/ics/PhutilICSParser.php b/src/parser/calendar/ics/PhutilICSParser.php --- a/src/parser/calendar/ics/PhutilICSParser.php +++ b/src/parser/calendar/ics/PhutilICSParser.php @@ -28,12 +28,13 @@ const PARSE_EMPTY_DATETIME = 'empty-datetime'; const PARSE_MANY_DATETIME = 'many-datetime'; const PARSE_BAD_DATETIME = 'bad-datetime'; - const PARSE_BAD_TZID = 'bad-tzid'; const PARSE_EMPTY_DURATION = 'empty-duration'; const PARSE_MANY_DURATION = 'many-duration'; const PARSE_BAD_DURATION = 'bad-duration'; const WARN_TZID_UTC = 'warn-tzid-utc'; + const WARN_TZID_GUESS = 'warn-tzid-guess'; + const WARN_TZID_IGNORED = 'warn-tzid-ignored'; public function parseICSData($data) { $this->stack = array(); @@ -617,6 +618,10 @@ return $this; } + public function getWarnings() { + return $this->warnings; + } + private function didParseEventProperty( PhutilCalendarEventNode $node, $name, @@ -736,15 +741,7 @@ } $tzid = 'UTC'; } else if ($tzid !== null) { - $map = DateTimeZone::listIdentifiers(); - $map = array_fuse($map); - if (empty($map[$tzid])) { - $this->raiseParseFailure( - self::PARSE_BAD_TZID, - pht( - 'Timezone "%s" is not a recognized timezone.', - $tzid)); - } + $tzid = $this->guessTimezone($tzid); } try { @@ -835,5 +832,67 @@ return idx(head($value), 'value'); } + private function guessTimezone($tzid) { + $map = DateTimeZone::listIdentifiers(); + $map = array_fuse($map); + if (isset($map[$tzid])) { + // This is a real timezone we recognize, so just use it as provided. + return $tzid; + } + + // Look for something that looks like "UTC+3" or "GMT -05.00". If we find + // anything + $offset_pattern = + '/'. + '(?:UTC|GMT)'. + '\s*'. + '(?P[+-])'. + '\s*'. + '(?P\d+)'. + '(?:'. + '[:.](?P\d+)'. + ')?'. + '/i'; + + $matches = null; + if (preg_match($offset_pattern, $tzid, $matches)) { + $hours = (int)$matches['h']; + $minutes = (int)idx($matches, 'm'); + $offset = ($hours * 60 * 60) + ($minutes * 60); + + if (idx($matches, 'sign') == '-') { + $offset = -$offset; + } + + // NOTE: We could possibly do better than this, by using the event start + // time to guess a timezone. However, that won't work for recurring + // events and would require us to do this work after finishing initial + // parsing. Since these unusual offset-based timezones appear to be rare, + // the benefit may not be worth the complexity. + $now = new DateTime('@'.time()); + + foreach ($map as $identifier) { + $zone = new DateTimeZone($identifier); + if ($zone->getOffset($now) == $offset) { + $this->raiseWarning( + self::WARN_TZID_GUESS, + pht( + 'TZID "%s" is unknown, guessing "%s" based on pattern "%s".', + $tzid, + $identifier, + $matches[0])); + return $identifier; + } + } + } + + $this->raiseWarning( + self::WARN_TZID_IGNORED, + pht( + 'TZID "%s" is unknown, using UTC instead.', + $tzid)); + + return 'UTC'; + } } diff --git a/src/parser/calendar/ics/__tests__/PhutilICSParserTestCase.php b/src/parser/calendar/ics/__tests__/PhutilICSParserTestCase.php --- a/src/parser/calendar/ics/__tests__/PhutilICSParserTestCase.php +++ b/src/parser/calendar/ics/__tests__/PhutilICSParserTestCase.php @@ -113,6 +113,16 @@ $event->getEndDateTime()->getEpoch()); } + public function testICSOddTimezone() { + $event = $this->parseICSSingleEvent('zimbra-timezone.ics'); + + $start = $event->getStartDateTime(); + + $this->assertEqual( + '20170303T140000Z', + $start->getISO8601()); + } + public function testICSFloatingTime() { // This tests "floating" event times, which have no absolute time and are // supposed to be interpreted using the viewer's timezone. It also uses @@ -234,8 +244,6 @@ PhutilICSParser::PARSE_MANY_DATETIME, 'err-bad-datetime.ics' => PhutilICSParser::PARSE_BAD_DATETIME, - 'err-bad-tzid.ics' => - PhutilICSParser::PARSE_BAD_TZID, 'err-empty-duration.ics' => PhutilICSParser::PARSE_EMPTY_DURATION, 'err-many-duration.ics' => diff --git a/src/parser/calendar/ics/__tests__/data/err-bad-tzid.ics b/src/parser/calendar/ics/__tests__/data/err-bad-tzid.ics deleted file mode 100644 --- a/src/parser/calendar/ics/__tests__/data/err-bad-tzid.ics +++ /dev/null @@ -1,5 +0,0 @@ -BEGIN:VCALENDAR -BEGIN:VEVENT -DTSTART;TZID=quack:20130101 -END:VEVENT -END:VCALENDAR diff --git a/src/parser/calendar/ics/__tests__/data/zimbra-timezone.ics b/src/parser/calendar/ics/__tests__/data/zimbra-timezone.ics new file mode 100644 --- /dev/null +++ b/src/parser/calendar/ics/__tests__/data/zimbra-timezone.ics @@ -0,0 +1,12 @@ +BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +BEGIN:VEVENT +CREATED:20161104T220244Z +UID:zimbra-timezone +SUMMARY:Zimbra Timezone +DTSTART;TZID="(GMT-05.00) Auto-Detected":20170303T090000 +DTSTAMP:20161104T220244Z +SEQUENCE:0 +END:VEVENT +END:VCALENDAR