Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F15385383
D16548.id39831.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
29 KB
Referenced Files
None
Subscribers
None
D16548.id39831.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D16548: Parse ICS datetimes and durations
Attached
Detach File
Event Timeline
Log In to Comment