Skip to content

Commit

Permalink
When we encounter a fanciful timezone identifier, try to guess what i…
Browse files Browse the repository at this point in the history
…t might mean

Summary:
Ref T11816. My read of RFC 5545 is that applications can do whatever they want here. Although most applications use legal timezonedb values like "America/Los_Angeles", at least one ("Zimbra") has at least one event with a weird value (`TZID="(GMT-05.00) Auto-Detected"`).

Try to puzzle out what these mysterious identifiers might intend. For now, I added a rule to look for "UTC+3", "GMT-2:30", etc.

If we don't have any guesses, just use UTC. If we guess or fall back, raise a warning so the user can see what happened.

Test Plan: Added a unit test. See also next change.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T11816

Differential Revision: https://secure.phabricator.com/D16800
  • Loading branch information
epriestley committed Nov 4, 2016
1 parent 2b7b100 commit e409df2
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 17 deletions.
79 changes: 69 additions & 10 deletions src/parser/calendar/ics/PhutilICSParser.php
Expand Up @@ -28,12 +28,13 @@ final class PhutilICSParser extends Phobject {
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();
Expand Down Expand Up @@ -617,6 +618,10 @@ private function raiseWarning($code, $message) {
return $this;
}

public function getWarnings() {
return $this->warnings;
}

private function didParseEventProperty(
PhutilCalendarEventNode $node,
$name,
Expand Down Expand Up @@ -736,15 +741,7 @@ private function newDateTimeFromProperty(array $parameters, array $value) {
}
$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 {
Expand Down Expand Up @@ -835,5 +832,67 @@ private function getScalarParameterValue(
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<sign>[+-])'.
'\s*'.
'(?P<h>\d+)'.
'(?:'.
'[:.](?P<m>\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';
}

}
12 changes: 10 additions & 2 deletions src/parser/calendar/ics/__tests__/PhutilICSParserTestCase.php
Expand Up @@ -113,6 +113,16 @@ public function testICSParser() {
$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
Expand Down Expand Up @@ -234,8 +244,6 @@ public function testICSParserErrors() {
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' =>
Expand Down
5 changes: 0 additions & 5 deletions src/parser/calendar/ics/__tests__/data/err-bad-tzid.ics

This file was deleted.

12 changes: 12 additions & 0 deletions 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

0 comments on commit e409df2

Please sign in to comment.