Page MenuHomePhabricator

D16548.id39831.diff
No OneTemporary

D16548.id39831.diff

diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -132,11 +132,16 @@
'PhutilCIDRBlock' => 'ip/PhutilCIDRBlock.php',
'PhutilCIDRList' => 'ip/PhutilCIDRList.php',
'PhutilCLikeCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilCLikeCodeSnippetContextFreeGrammar.php',
+ 'PhutilCalendarAbsoluteDateTime' => 'parser/calendar/data/PhutilCalendarAbsoluteDateTime.php',
'PhutilCalendarContainerNode' => 'parser/calendar/data/PhutilCalendarContainerNode.php',
+ 'PhutilCalendarDateTime' => 'parser/calendar/data/PhutilCalendarDateTime.php',
'PhutilCalendarDocumentNode' => 'parser/calendar/data/PhutilCalendarDocumentNode.php',
+ 'PhutilCalendarDuration' => 'parser/calendar/data/PhutilCalendarDuration.php',
'PhutilCalendarEventNode' => 'parser/calendar/data/PhutilCalendarEventNode.php',
'PhutilCalendarNode' => 'parser/calendar/data/PhutilCalendarNode.php',
+ 'PhutilCalendarProxyDateTime' => 'parser/calendar/data/PhutilCalendarProxyDateTime.php',
'PhutilCalendarRawNode' => 'parser/calendar/data/PhutilCalendarRawNode.php',
+ 'PhutilCalendarRelativeDateTime' => 'parser/calendar/data/PhutilCalendarRelativeDateTime.php',
'PhutilCalendarRootNode' => 'parser/calendar/data/PhutilCalendarRootNode.php',
'PhutilCallbackFilterIterator' => 'utils/PhutilCallbackFilterIterator.php',
'PhutilCallbackSignalHandler' => 'future/exec/PhutilCallbackSignalHandler.php',
@@ -708,11 +713,16 @@
'PhutilCIDRBlock' => 'Phobject',
'PhutilCIDRList' => 'Phobject',
'PhutilCLikeCodeSnippetContextFreeGrammar' => 'PhutilCodeSnippetContextFreeGrammar',
+ 'PhutilCalendarAbsoluteDateTime' => 'PhutilCalendarDateTime',
'PhutilCalendarContainerNode' => 'PhutilCalendarNode',
+ 'PhutilCalendarDateTime' => 'Phobject',
'PhutilCalendarDocumentNode' => 'PhutilCalendarContainerNode',
+ 'PhutilCalendarDuration' => 'Phobject',
'PhutilCalendarEventNode' => 'PhutilCalendarNode',
'PhutilCalendarNode' => 'Phobject',
+ 'PhutilCalendarProxyDateTime' => 'PhutilCalendarDateTime',
'PhutilCalendarRawNode' => 'PhutilCalendarContainerNode',
+ 'PhutilCalendarRelativeDateTime' => 'PhutilCalendarProxyDateTime',
'PhutilCalendarRootNode' => 'PhutilCalendarContainerNode',
'PhutilCallbackFilterIterator' => 'FilterIterator',
'PhutilCallbackSignalHandler' => 'PhutilSignalHandler',
diff --git a/src/parser/calendar/data/PhutilCalendarAbsoluteDateTime.php b/src/parser/calendar/data/PhutilCalendarAbsoluteDateTime.php
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/data/PhutilCalendarAbsoluteDateTime.php
@@ -0,0 +1,115 @@
+<?php
+
+final class PhutilCalendarAbsoluteDateTime
+ extends PhutilCalendarDateTime {
+
+ private $year;
+ private $month;
+ private $day;
+ private $hour = 0;
+ private $minute = 0;
+ private $second = 0;
+ private $timezone;
+ private $viewerTimezone;
+
+ public function setYear($year) {
+ $this->year = $year;
+ return $this;
+ }
+
+ public function getYear() {
+ return $this->year;
+ }
+
+ public function setMonth($month) {
+ $this->month = $month;
+ return $this;
+ }
+
+ public function getMonth() {
+ return $this->month;
+ }
+
+ public function setDay($day) {
+ $this->day = $day;
+ return $this;
+ }
+
+ public function getDay() {
+ return $this->day;
+ }
+
+ public function setHour($hour) {
+ $this->hour = $hour;
+ return $this;
+ }
+
+ public function getHour() {
+ return $this->hour;
+ }
+
+ public function setMinute($minute) {
+ $this->minute = $minute;
+ return $this;
+ }
+
+ public function getMinute() {
+ return $this->minute;
+ }
+
+ public function setSecond($second) {
+ $this->second = $second;
+ return $this;
+ }
+
+ public function getSecond() {
+ return $this->second;
+ }
+
+ public function setTimezone($timezone) {
+ $this->timezone = $timezone;
+ return $this;
+ }
+
+ public function getTimezone() {
+ return $this->timezone;
+ }
+
+ private function getEffectiveTimezone() {
+ $zone = $this->getTimezone();
+ if ($zone !== null) {
+ return $zone;
+ }
+
+ $zone = $this->getViewerTimezone();
+ if ($zone !== null) {
+ return $zone;
+ }
+
+ throw new Exception(
+ pht(
+ 'Datetime has no timezone or viewer timezone.'));
+ }
+
+ protected function newPHPDateTimeZone() {
+ $zone = $this->getEffectiveTimezone();
+ return new DateTimeZone($zone);
+ }
+
+ protected function newPHPDateTime() {
+ $zone = $this->newPHPDateTimeZone();
+
+ $y = $this->getYear();
+ $m = $this->getMonth();
+ $d = $this->getDay();
+
+ $h = $this->getHour();
+ $i = $this->getMinute();
+ $s = $this->getSecond();
+
+ $format = sprintf('%04d-%02d-%02d %02d:%02d:%02d', $y, $m, $d, $h, $i, $s);
+
+ return new DateTime($format, $zone);
+ }
+
+}
diff --git a/src/parser/calendar/data/PhutilCalendarDateTime.php b/src/parser/calendar/data/PhutilCalendarDateTime.php
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/data/PhutilCalendarDateTime.php
@@ -0,0 +1,25 @@
+<?php
+
+abstract class PhutilCalendarDateTime
+ extends Phobject {
+
+ private $viewerTimezone;
+
+ public function setViewerTimezone($viewer_timezone) {
+ $this->viewerTimezone = $viewer_timezone;
+ return $this;
+ }
+
+ public function getViewerTimezone() {
+ return $this->viewerTimezone;
+ }
+
+ public function getEpoch() {
+ $datetime = $this->newPHPDateTime();
+ return (int)$datetime->format('U');
+ }
+
+ abstract protected function newPHPDateTimeZone();
+ abstract protected function newPHPDateTime();
+
+}
diff --git a/src/parser/calendar/data/PhutilCalendarDuration.php b/src/parser/calendar/data/PhutilCalendarDuration.php
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/data/PhutilCalendarDuration.php
@@ -0,0 +1,66 @@
+<?php
+
+final class PhutilCalendarDuration extends Phobject {
+
+ private $isNegative = false;
+ private $days = 0;
+ private $weeks = 0;
+ private $hours = 0;
+ private $minutes = 0;
+ private $seconds = 0;
+
+ public function setIsNegative($is_negative) {
+ $this->isNegative = $is_negative;
+ return $this;
+ }
+
+ public function getIsNegative() {
+ return $this->isNegative;
+ }
+
+ public function setDays($days) {
+ $this->days = $days;
+ return $this;
+ }
+
+ public function getDays() {
+ return $this->days;
+ }
+
+ public function setWeeks($weeks) {
+ $this->weeks = $weeks;
+ return $this;
+ }
+
+ public function getWeeks() {
+ return $this->weeks;
+ }
+
+ public function setHours($hours) {
+ $this->hours = $hours;
+ return $this;
+ }
+
+ public function getHours() {
+ return $this->hours;
+ }
+
+ public function setMinutes($minutes) {
+ $this->minutes = $minutes;
+ return $this;
+ }
+
+ public function getMinutes() {
+ return $this->minutes;
+ }
+
+ public function setSeconds($seconds) {
+ $this->seconds = $seconds;
+ return $this;
+ }
+
+ public function getSeconds() {
+ return $this->seconds;
+ }
+
+}
diff --git a/src/parser/calendar/data/PhutilCalendarEventNode.php b/src/parser/calendar/data/PhutilCalendarEventNode.php
--- a/src/parser/calendar/data/PhutilCalendarEventNode.php
+++ b/src/parser/calendar/data/PhutilCalendarEventNode.php
@@ -5,4 +5,69 @@
const NODETYPE = 'event';
+ private $name;
+ private $description;
+ private $startDateTime;
+ private $endDateTime;
+ private $duration;
+
+ public function setName($name) {
+ $this->name = $name;
+ return $this;
+ }
+
+ public function getName() {
+ return $this->name;
+ }
+
+ public function setDescription($description) {
+ $this->description = $description;
+ return $this;
+ }
+
+ public function getDescription() {
+ return $this->description;
+ }
+
+ public function setStartDateTime(PhutilCalendarDateTime $start_date_time) {
+ $this->startDateTime = $start_date_time;
+ return $this;
+ }
+
+ public function getStartDateTime() {
+ return $this->startDateTime;
+ }
+
+ public function setEndDateTime(PhutilCalendarDateTime $end_date_time) {
+ $this->endDateTime = $end_date_time;
+ return $this;
+ }
+
+ public function getEndDateTime() {
+ $end = $this->endDateTime;
+ if ($end) {
+ return $end;
+ }
+
+ $start = $this->getStartDateTime();
+ $duration = $this->getDuration();
+ if ($start && $duration) {
+ return id(new PhutilCalendarRelativeDateTime())
+ ->setOrigin($start)
+ ->setDuration($duration);
+ }
+
+ return null;
+ }
+
+ public function setDuration(PhutilCalendarDuration $duration) {
+ $this->duration = $duration;
+ return $this;
+ }
+
+ public function getDuration() {
+ return $this->duration;
+ }
+
+
}
diff --git a/src/parser/calendar/data/PhutilCalendarProxyDateTime.php b/src/parser/calendar/data/PhutilCalendarProxyDateTime.php
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/data/PhutilCalendarProxyDateTime.php
@@ -0,0 +1,34 @@
+<?php
+
+abstract class PhutilCalendarProxyDateTime
+ extends PhutilCalendarDateTime {
+
+ private $proxy;
+
+ final protected function setProxy(PhutilCalendarDateTime $proxy) {
+ $this->proxy = $proxy;
+ return $this;
+ }
+
+ final protected function getProxy() {
+ return $this->proxy;
+ }
+
+ public function setViewerTimezone($timezone) {
+ $this->getProxy()->setViewerTimezone($timezone);
+ return $this;
+ }
+
+ public function getViewerTimezone() {
+ return $this->getProxy()->getViewerTimezone();
+ }
+
+ protected function newPHPDateTimezone() {
+ return $this->getProxy()->newPHPDateTimezone();
+ }
+
+ protected function newPHPDateTime() {
+ return $this->getProxy()->newPHPDateTime();
+ }
+
+}
diff --git a/src/parser/calendar/data/PhutilCalendarRelativeDateTime.php b/src/parser/calendar/data/PhutilCalendarRelativeDateTime.php
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/data/PhutilCalendarRelativeDateTime.php
@@ -0,0 +1,53 @@
+<?php
+
+final class PhutilCalendarRelativeDateTime
+ extends PhutilCalendarProxyDateTime {
+
+ private $duration;
+
+ public function setOrigin(PhutilCalendarDateTime $origin) {
+ return $this->setProxy($origin);
+ }
+
+ public function getOrigin() {
+ return $this->getProxy();
+ }
+
+ public function setDuration(PhutilCalendarDuration $duration) {
+ $this->duration = $duration;
+ return $this;
+ }
+
+ public function getDuration() {
+ return $this->duration;
+ }
+
+ protected function newPHPDateTime() {
+ $datetime = parent::newPHPDateTime();
+ $duration = $this->getDuration();
+
+ if ($duration->getIsNegative()) {
+ $sign = '-';
+ } else {
+ $sign = '+';
+ }
+
+ $map = array(
+ 'weeks' => $duration->getWeeks(),
+ 'days' => $duration->getDays(),
+ 'hours' => $duration->getHours(),
+ 'minutes' => $duration->getMinutes(),
+ 'seconds' => $duration->getSeconds(),
+ );
+
+ foreach ($map as $unit => $value) {
+ if (!$value) {
+ continue;
+ }
+ $datetime->modify("{$sign}{$value} {$unit}");
+ }
+
+ return $datetime;
+ }
+
+}
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
@@ -8,6 +8,8 @@
private $lines;
private $cursor;
+ private $warnings;
+
const PARSE_MISSING_END = 'missing-end';
const PARSE_INITIAL_UNFOLD = 'initial-unfold';
const PARSE_UNEXPECTED_CHILD = 'unexpected-child';
@@ -22,11 +24,22 @@
const PARSE_MALFORMED_PROPERTY = 'malformed-property';
const PARSE_MISSING_VALUE = 'missing-value';
const PARSE_UNESCAPED_BACKSLASH = 'unescaped-backslash';
+ const PARSE_MULTIPLE_PARAMETERS = 'multiple-parameters';
+ 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';
public function parseICSData($data) {
$this->stack = array();
$this->node = null;
$this->cursor = null;
+ $this->warnings = array();
$lines = $this->unfoldICSLines($data);
$this->lines = $lines;
@@ -302,6 +315,7 @@
$node = $this->getNode();
+
$raw = $node->getAttribute('ics.properties', array());
$raw[] = array(
'name' => $name,
@@ -309,6 +323,12 @@
'value' => $value,
);
$node->setAttribute('ics.properties', $raw);
+
+ switch ($node->getAttribute('ics.type')) {
+ case 'VEVENT':
+ $this->didParseEventProperty($node, $name, $parameters, $value);
+ break;
+ }
}
private function unescapeParameterValue($data) {
@@ -465,10 +485,18 @@
$result = explode(',', $data);
break;
case 'DATE-TIME':
- $result = explode(',', $data);
+ if (!strlen($data)) {
+ $result = array();
+ } else {
+ $result = explode(',', $data);
+ }
break;
case 'DURATION':
- $result = explode(',', $data);
+ if (!strlen($data)) {
+ $result = array();
+ } else {
+ $result = explode(',', $data);
+ }
break;
case 'FLOAT':
$result = explode(',', $data);
@@ -572,4 +600,227 @@
->setParserFailureCode($code);
}
+ private function raiseWarning($code, $message) {
+ $this->warnings[] = array(
+ 'code' => $code,
+ 'line' => $this->cursor,
+ 'text' => $this->lines[$this->cursor],
+ 'message' => $message,
+ );
+
+ return $this;
+ }
+
+ private function didParseEventProperty(
+ PhutilCalendarEventNode $node,
+ $name,
+ array $parameters,
+ array $value) {
+
+ switch ($name) {
+ case 'SUMMARY':
+ $text = $this->newTextFromProperty($parameters, $value);
+ $node->setName($text);
+ break;
+ case 'DESCRIPTION':
+ $text = $this->newTextFromProperty($parameters, $value);
+ $node->setDescription($text);
+ break;
+ case 'DTSTART':
+ $datetime = $this->newDateTimeFromProperty($parameters, $value);
+ $node->setStartDateTime($datetime);
+ break;
+ case 'DTEND':
+ $datetime = $this->newDateTimeFromProperty($parameters, $value);
+ $node->setEndDateTime($datetime);
+ break;
+ case 'DURATION':
+ $duration = $this->newDurationFromProperty($parameters, $value);
+ $node->setDuration($duration);
+ break;
+ }
+
+ }
+
+ private function newTextFromProperty(array $parameters, array $value) {
+ $value = $value['value'];
+ return implode("\n\n", $value);
+ }
+
+ private function newDateTimeFromProperty(array $parameters, array $value) {
+ $value = $value['value'];
+
+ if (!$value) {
+ $this->raiseParseFailure(
+ self::PARSE_EMPTY_DATETIME,
+ pht(
+ 'Expected DATE-TIME to have exactly one value, found none.'));
+
+ }
+
+ if (count($value) > 1) {
+ $this->raiseParseFailure(
+ self::PARSE_MANY_DATETIME,
+ pht(
+ 'Expected DATE-TIME to have exactly one value, found more than '.
+ 'one.'));
+ }
+
+ $value = head($value);
+
+ $pattern =
+ '/^'.
+ '(?P<y>\d{4})(?P<m>\d{2})(?P<d>\d{2})'.
+ '(?:'.
+ 'T(?P<h>\d{2})(?P<i>\d{2})(?P<s>\d{2})(?<z>Z)?'.
+ ')?'.
+ '\z/';
+
+ $matches = null;
+ $ok = preg_match($pattern, $value, $matches);
+ if (!$ok) {
+ $this->raiseParseFailure(
+ self::PARSE_BAD_DATETIME,
+ pht(
+ 'Expected DATE-TIME in the format "19990105T112233Z", found '.
+ '"%s".',
+ $value));
+ }
+
+ $tzid = $this->getScalarParameterValue($parameters, 'TZID');
+
+ if (isset($matches['z'])) {
+ if ($tzid) {
+ $this->raiseWarning(
+ self::WARN_TZID_UTC,
+ pht(
+ 'DATE-TIME "%s" uses "Z" to specify UTC, but also has a TZID '.
+ 'parameter with value "%s". This violates RFC5545. The TZID '.
+ 'will be ignored, and the value will be interpreted as UTC.',
+ $value,
+ $tzid));
+ }
+ $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));
+ }
+ }
+
+ $datetime = id(new PhutilCalendarAbsoluteDateTime())
+ ->setYear((int)$matches['y'])
+ ->setMonth((int)$matches['m'])
+ ->setDay((int)$matches['d'])
+ ->setTimezone($tzid);
+
+ if (isset($matches['h'])) {
+ $datetime
+ ->setHour((int)$matches['h'])
+ ->setMinute((int)$matches['i'])
+ ->setSecond((int)$matches['s']);
+ }
+
+ return $datetime;
+ }
+
+ private function newDurationFromProperty(array $parameters, array $value) {
+ $value = $value['value'];
+
+ if (!$value) {
+ $this->raiseParseFailure(
+ self::PARSE_EMPTY_DURATION,
+ pht(
+ 'Expected DURATION to have exactly one value, found none.'));
+
+ }
+
+ if (count($value) > 1) {
+ $this->raiseParseFailure(
+ self::PARSE_MANY_DURATION,
+ pht(
+ 'Expected DURATION to have exactly one value, found more than '.
+ 'one.'));
+ }
+
+ $value = head($value);
+
+ $pattern =
+ '/^'.
+ '(?P<sign>[+-])?'.
+ 'P'.
+ '(?:'.
+ '(?P<W>\d+)W'.
+ '|'.
+ '(?:(?:(?P<D>\d+)D)?'.
+ '(?:T(?:(?P<H>\d+)H)?(?:(?P<M>\d+)M)?(?:(?P<S>\d+)S)?)?'.
+ ')'.
+ ')'.
+ '\z/';
+
+ $matches = null;
+ $ok = preg_match($pattern, $value, $matches);
+ if (!$ok) {
+ $this->raiseParseFailure(
+ self::PARSE_BAD_DURATION,
+ pht(
+ 'Expected DURATION in the format "P12DT3H4M5S", found '.
+ '"%s".',
+ $value));
+ }
+
+ $is_negative = (idx($matches, 'sign') == '-');
+
+ $duration = id(new PhutilCalendarDuration())
+ ->setIsNegative($is_negative)
+ ->setWeeks((int)idx($matches, 'W', 0))
+ ->setDays((int)idx($matches, 'D', 0))
+ ->setHours((int)idx($matches, 'H', 0))
+ ->setMinutes((int)idx($matches, 'M', 0))
+ ->setSeconds((int)idx($matches, 'S', 0));
+
+ return $duration;
+ }
+
+ private function getScalarParameterValue(
+ array $parameters,
+ $name,
+ $default = null) {
+
+ $match = null;
+ foreach ($parameters as $parameter) {
+ if ($parameter['name'] == $name) {
+ $match = $parameter;
+ }
+ }
+
+ if ($match === null) {
+ return $default;
+ }
+
+ $value = $match['values'];
+ if (!$value) {
+ // Parameter is specified, but with no value, like "KEY=". Just return
+ // the default, as though the parameter was not specified.
+ return $default;
+ }
+
+ if (count($value) > 1) {
+ $this->raiseParseFailure(
+ self::PARSE_MULTIPLE_PARAMETERS,
+ pht(
+ 'Expected parameter "%s" to have at most one value, but found '.
+ 'more than one.',
+ $name));
+ }
+
+ return idx(head($value), 'value');
+ }
+
+
}
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
@@ -3,16 +3,8 @@
final class PhutilICSParserTestCase extends PhutilTestCase {
public function testICSParser() {
- $root = $this->parseICSDocument('simple.ics');
+ $event = $this->parseICSSingleEvent('simple.ics');
- $documents = $root->getDocuments();
- $this->assertEqual(1, count($documents));
- $document = head($documents);
-
- $events = $document->getEvents();
- $this->assertEqual(1, count($events));
-
- $event = head($events);
$this->assertEqual(
array(
array(
@@ -38,6 +30,27 @@
),
),
array(
+ 'name' => 'DTSTART',
+ 'parameters' => array(
+ array(
+ 'name' => 'TZID',
+ 'values' => array(
+ array(
+ 'value' => 'America/Los_Angeles',
+ 'quoted' => false,
+ ),
+ ),
+ ),
+ ),
+ 'value' => array(
+ 'type' => 'DATE-TIME',
+ 'value' => array(
+ '20160915T090000',
+ ),
+ 'raw' => '20160915T090000',
+ ),
+ ),
+ array(
'name' => 'DTEND',
'parameters' => array(
array(
@@ -64,13 +77,119 @@
'value' => array(
'type' => 'TEXT',
'value' => array(
- 'Example Event',
+ 'Simple Event',
+ ),
+ 'raw' => 'Simple Event',
+ ),
+ ),
+ array(
+ 'name' => 'DESCRIPTION',
+ 'parameters' => array(),
+ 'value' => array(
+ 'type' => 'TEXT',
+ 'value' => array(
+ 'This is a simple event.',
),
- 'raw' => 'Example Event',
+ 'raw' => 'This is a simple event.',
),
),
),
$event->getAttribute('ics.properties'));
+
+ $this->assertEqual(
+ 'Simple Event',
+ $event->getName());
+
+ $this->assertEqual(
+ 'This is a simple event.',
+ $event->getDescription());
+
+ $this->assertEqual(
+ 1473955200,
+ $event->getStartDateTime()->getEpoch());
+
+ $this->assertEqual(
+ 1473955200 + phutil_units('1 hour in seconds'),
+ $event->getEndDateTime()->getEpoch());
+ }
+
+ 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
+ // a duration, and the duration needs to float along with the viewer
+ // timezone.
+
+ $event = $this->parseICSSingleEvent('floating.ics');
+
+ $start = $event->getStartDateTime();
+
+ $caught = null;
+ try {
+ $start->getEpoch();
+ } catch (Exception $ex) {
+ $caught = $ex;
+ }
+
+ $this->assertTrue(
+ ($caught instanceof Exception),
+ pht('Expected exception for floating time with no viewer timezone.'));
+
+ $newyears_utc = strtotime('2015-01-01 00:00:00 UTC');
+ $this->assertEqual(1420070400, $newyears_utc);
+
+ $start->setViewerTimezone('UTC');
+ $this->assertEqual(
+ $newyears_utc,
+ $start->getEpoch());
+
+ $start->setViewerTimezone('America/Los_Angeles');
+ $this->assertEqual(
+ $newyears_utc + phutil_units('8 hours in seconds'),
+ $start->getEpoch());
+
+ $start->setViewerTimezone('America/New_York');
+ $this->assertEqual(
+ $newyears_utc + phutil_units('5 hours in seconds'),
+ $start->getEpoch());
+
+ $end = $event->getEndDateTime();
+ $end->setViewerTimezone('UTC');
+ $this->assertEqual(
+ $newyears_utc + phutil_units('24 hours in seconds'),
+ $end->getEpoch());
+
+ $end->setViewerTimezone('America/Los_Angeles');
+ $this->assertEqual(
+ $newyears_utc + phutil_units('32 hours in seconds'),
+ $end->getEpoch());
+
+ $end->setViewerTimezone('America/New_York');
+ $this->assertEqual(
+ $newyears_utc + phutil_units('29 hours in seconds'),
+ $end->getEpoch());
+ }
+
+ public function testICSDuration() {
+ $event = $this->parseICSSingleEvent('duration.ics');
+
+ // Raw value is "20160719T095722Z".
+ $start_epoch = strtotime('2016-07-19 09:57:22 UTC');
+ $this->assertEqual(1468922242, $start_epoch);
+
+ // Raw value is "P1DT17H4M23S".
+ $duration =
+ phutil_units('1 day in seconds') +
+ phutil_units('17 hours in seconds') +
+ phutil_units('4 minutes in seconds') +
+ phutil_units('23 seconds in seconds');
+
+ $this->assertEqual(
+ $start_epoch,
+ $event->getStartDateTime()->getEpoch());
+
+ $this->assertEqual(
+ $start_epoch + $duration,
+ $event->getEndDateTime()->getEpoch());
}
public function testICSParserErrors() {
@@ -94,6 +213,22 @@
PhutilICSParser::PARSE_UNESCAPED_BACKSLASH,
'err-unexpected-child.ics' => PhutilICSParser::PARSE_UNEXPECTED_CHILD,
'err-unexpected-text.ics' => PhutilICSParser::PARSE_UNEXPECTED_TEXT,
+ 'err-multiple-parameters.ics' =>
+ PhutilICSParser::PARSE_MULTIPLE_PARAMETERS,
+ 'err-empty-datetime.ics' =>
+ PhutilICSParser::PARSE_EMPTY_DATETIME,
+ 'err-many-datetime.ics' =>
+ 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' =>
+ PhutilICSParser::PARSE_MANY_DURATION,
+ 'err-bad-duration.ics' =>
+ PhutilICSParser::PARSE_BAD_DURATION,
'simple.ics' => null,
'good-boolean.ics' => null,
@@ -135,6 +270,19 @@
}
}
+ private function parseICSSingleEvent($name) {
+ $root = $this->parseICSDocument($name);
+
+ $documents = $root->getDocuments();
+ $this->assertEqual(1, count($documents));
+ $document = head($documents);
+
+ $events = $document->getEvents();
+ $this->assertEqual(1, count($events));
+
+ return head($events);
+ }
+
private function parseICSDocument($name) {
$path = dirname(__FILE__).'/data/'.$name;
$data = Filesystem::readFile($path);
diff --git a/src/parser/calendar/ics/__tests__/data/duration.ics b/src/parser/calendar/ics/__tests__/data/duration.ics
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/ics/__tests__/data/duration.ics
@@ -0,0 +1,8 @@
+BEGIN:VCALENDAR
+BEGIN:VEVENT
+DTSTART:20160719T095722Z
+DURATION:P1DT17H4M23S
+SUMMARY:Duration Event
+DESCRIPTION:This is an event with a complex duration.
+END:VEVENT
+END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-bad-datetime.ics b/src/parser/calendar/ics/__tests__/data/err-bad-datetime.ics
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/ics/__tests__/data/err-bad-datetime.ics
@@ -0,0 +1,5 @@
+BEGIN:VCALENDAR
+BEGIN:VEVENT
+DTSTART:quack
+END:VEVENT
+END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-bad-duration.ics b/src/parser/calendar/ics/__tests__/data/err-bad-duration.ics
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/ics/__tests__/data/err-bad-duration.ics
@@ -0,0 +1,5 @@
+BEGIN:VCALENDAR
+BEGIN:VEVENT
+DURATION:quack
+END:VEVENT
+END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-bad-tzid.ics b/src/parser/calendar/ics/__tests__/data/err-bad-tzid.ics
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/ics/__tests__/data/err-bad-tzid.ics
@@ -0,0 +1,5 @@
+BEGIN:VCALENDAR
+BEGIN:VEVENT
+DTSTART;TZID=quack:20130101
+END:VEVENT
+END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-empty-datetime.ics b/src/parser/calendar/ics/__tests__/data/err-empty-datetime.ics
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/ics/__tests__/data/err-empty-datetime.ics
@@ -0,0 +1,5 @@
+BEGIN:VCALENDAR
+BEGIN:VEVENT
+DTSTART:
+END:VEVENT
+END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-empty-duration.ics b/src/parser/calendar/ics/__tests__/data/err-empty-duration.ics
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/ics/__tests__/data/err-empty-duration.ics
@@ -0,0 +1,5 @@
+BEGIN:VCALENDAR
+BEGIN:VEVENT
+DURATION:
+END:VEVENT
+END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-many-datetime.ics b/src/parser/calendar/ics/__tests__/data/err-many-datetime.ics
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/ics/__tests__/data/err-many-datetime.ics
@@ -0,0 +1,5 @@
+BEGIN:VCALENDAR
+BEGIN:VEVENT
+DTSTART:20130101,20130101
+END:VEVENT
+END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-many-duration.ics b/src/parser/calendar/ics/__tests__/data/err-many-duration.ics
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/ics/__tests__/data/err-many-duration.ics
@@ -0,0 +1,5 @@
+BEGIN:VCALENDAR
+BEGIN:VEVENT
+DURATION:P1W,P2W
+END:VEVENT
+END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-multiple-parameters.ics b/src/parser/calendar/ics/__tests__/data/err-multiple-parameters.ics
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/ics/__tests__/data/err-multiple-parameters.ics
@@ -0,0 +1,5 @@
+BEGIN:VCALENDAR
+BEGIN:VEVENT
+DTSTART;TZID=A,B:20160915T090000
+END:VEVENT
+END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/floating.ics b/src/parser/calendar/ics/__tests__/data/floating.ics
new file mode 100644
--- /dev/null
+++ b/src/parser/calendar/ics/__tests__/data/floating.ics
@@ -0,0 +1,8 @@
+BEGIN:VCALENDAR
+BEGIN:VEVENT
+DTSTART:20150101T000000
+DURATION:P1D
+SUMMARY:New Year's 2015
+DESCRIPTION:This is an event with a floating start time.
+END:VEVENT
+END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/simple.ics b/src/parser/calendar/ics/__tests__/data/simple.ics
--- a/src/parser/calendar/ics/__tests__/data/simple.ics
+++ b/src/parser/calendar/ics/__tests__/data/simple.ics
@@ -4,7 +4,9 @@
BEGIN:VEVENT
CREATED:20160908T172702Z
UID:1CEB57AF-0C9C-402D-B3BD-D75BD4843F68
+DTSTART;TZID=America/Los_Angeles:20160915T090000
DTEND;TZID=America/Los_Angeles:20160915T100000
-SUMMARY:Example Event
+SUMMARY:Simple Event
+DESCRIPTION:This is a simple event.
END:VEVENT
END:VCALENDAR

File Metadata

Mime Type
text/plain
Expires
Sat, Mar 15, 10:26 PM (3 d, 8 h ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7704189
Default Alt Text
D16548.id39831.diff (29 KB)

Event Timeline